Skip to content
Help

Webhooks

Receive order events at your own HTTPS endpoint, verify their signatures, and understand retries and duplicates.

Public API3 min read

Webhooks push order events to your own HTTPS endpoint as they happen. If you need to react to orders, use webhooks rather than polling /orders on a timer, which adds latency and consumes your rate limit.

#Adding an endpoint

Settings → Developers → Webhooks → Add endpoint.

  • URL: must be https. Plain HTTP is refused.
  • Events: which ones to send. Pick only the ones you handle.
  • Description: a label 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, from the 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": "52.41"
    }
  }
}

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, so 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-serializing changes the bytes, and the signature will not match.
  2. Compare in constant time. A plain === comparison leaks timing information about the secret.
  3. Reject anything where t is more than five minutes from now, so a captured request cannot be replayed later.
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, and return 200. Processing the order before responding risks a timeout, which leads to 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 can pass 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, whether or not 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 successful but slow sends the same event twice.

Every event carries a unique id. Record the ids you have processed and ignore repeats.

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