Webhooks
Receive order events at your own HTTPS endpoint, verify their signatures, and understand retries and duplicates.
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:
whsec_c29tZXRoaW5nc2VjcmV0aGVyZXRoYXRpc2xvbmcCopy 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
| Event | Fires when |
|---|---|
order.created | A new order is placed, from the counter, website, or app |
order.confirmed | An order is accepted, including automatically on successful online payment |
order.ready | The order is ready for pickup or hand-off |
order.completed | The order is done |
order.cancelled | The order is cancelled or voided |
order.refunded | A 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
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{
"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:
t=1785312000,v1=5257a869e7ecebeda...The signature is HMAC-SHA256 over {timestamp}.{raw request body}, keyed with your signing secret, hex-encoded.
Three rules:
- Sign the raw body, byte for byte. Parsing to JSON and re-serializing changes the bytes, and the signature will not match.
- Compare in constant time. A plain
===comparison leaks timing information about the secret. - Reject anything where
tis more than five minutes from now, so a captured request cannot be replayed later.
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)
}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:
| Attempt | Delay after the previous failure |
|---|---|
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
| 6 | 6 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