Skip to content
Help

Webhooks

Getting order events pushed to your server, verifying they came from us, and what happens when your server is down.

Public API3 min read

Webhooks push order events to your own HTTPS endpoint as they happen. If you need to react to orders, this is the right mechanism — polling /orders on a timer costs you latency and rate limit for no benefit.

#Adding an endpoint

Settings → Developers → Webhooks → Add endpoint.

  • URL — must be https. Plain HTTP is refused.
  • Events — which ones to send. Pick only what you handle.
  • Description — for you.

On creation, Restro shows the signing secret once:

text
whsec_c29tZXRoaW5nc2VjcmV0aGVyZXRoYXRpc2xvbmc

Copy it. Like API keys, it is shown once. If you lose it, Rotate secret issues a new one — deliveries in flight use the new secret from the moment you rotate, so deploy the new value promptly.

#Events

EventFires when
order.createdA new order is placed — counter, website, or app
order.confirmedAn order is accepted, including automatically on successful online payment
order.readyThe order is ready for pickup or hand-off
order.completedThe order is done
order.cancelledThe order is cancelled or voided
order.refundedA refund is recorded, whether through the gateway or manually

Events fire from every path that changes an order: the dashboard, the point of sale, the kitchen display, customer checkout, and the status endpoint.

#The request

http
POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-Restro-Event: order.completed
X-Restro-Delivery: cmd8dl5g30009abcdefghijkl
X-Restro-Signature: t=1785312000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
json
{
  "id": "evt_9f8c1d2e3b4a5c6d7e8f9a0b1c2d3e4f",
  "type": "order.completed",
  "createdAt": "2026-07-28T10:31:22.000Z",
  "restaurantId": "cmd8w1j8o0000abcdefghijkl",
  "data": {
    "order": {
      "id": "cmd8x2k9p0001abcdefghijkl",
      "number": 1042,
      "status": "completed",
      "total": "184.00"
    }
  }
}

data.order is the same shape the orders endpoint returns, in full.

#Verifying the signature

Verify every request before you act on it. Your endpoint is public; anyone can post to it.

X-Restro-Signature carries a timestamp and one or more signatures:

text
t=1785312000,v1=5257a869e7ecebeda...

The signature is HMAC-SHA256 over {timestamp}.{raw request body}, keyed with your signing secret, hex-encoded.

Three rules:

  1. Sign the raw body, byte for byte. Parsing to JSON and re-serialising changes the bytes and the signature will not match.
  2. Compare in constant time. A plain === leaks the secret to a patient attacker.
  3. Reject anything where t is more than five minutes from now, or a captured request can be replayed at you forever.
typescript
import { createHmac, timingSafeEqual } from 'node:crypto'

const verify = (rawBody: string, header: string, secret: string) => {
  const parts = Object.fromEntries(
    header.split(',').map((part) => part.split('=') as [string, string])
  )

  const timestamp = Number(parts.t)
  if (!Number.isFinite(timestamp)) return false
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false

  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')

  const a = Buffer.from(parts.v1)
  const b = Buffer.from(expected)

  return a.length === b.length && timingSafeEqual(a, b)
}
python
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    try:
        timestamp = int(parts["t"])
    except (KeyError, ValueError):
        return False

    if abs(time.time() - timestamp) > 300:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(parts.get("v1", ""), expected)

#Responding

Return any 2xx within 10 seconds. Anything else — a 4xx, a 5xx, a timeout, a refused connection — counts as a failure and Restro retries.

Do the minimum inline: verify the signature, put the event on your own queue, return 200. Processing the order before responding is how you end up timing out and collecting duplicate deliveries.

#Retries

A failed delivery is retried up to six attempts total, backing off each time:

AttemptDelay after the previous failure
230 seconds
32 minutes
410 minutes
51 hour
66 hours

After the sixth failure the delivery is marked gave up and stays in the log. Roughly eight hours of downtime is survivable without losing an event.

#Delivery log

Settings → Developers → Webhooks lists every delivery with its status, attempt count, the HTTP status your server returned, the error if any, and the full payload. Failed deliveries can be retried by hand from there, at any time, regardless of whether the automatic attempts are exhausted.

Use Send test to fire a sample event at an endpoint while you are building against it.

#Duplicates and ordering

Delivery is at least once. A retry after a response that was actually successful but slow will send the same event twice.

Every event carries a unique id. Record the ids you have processed and ignore repeats — that is the whole of what idempotency requires here.

Events are also not guaranteed to arrive in order: a retried order.confirmed can land after order.ready. Trust data.order.status for the current state rather than inferring it from the sequence of events you received.

#Disabling an endpoint

Toggle it off to stop deliveries without deleting it and losing the log. Deleting the endpoint removes its delivery history too.

Did this page miss something? Tell us.

Back to top