Receive order updates with webhooks instead of polling
This guide is for an integrator who is currently calling GET /v1/orders on a timer to find
out when something changed, and wants to switch to webhooks: OlaClick pushes a POST to your
endpoint the moment an order event happens, instead of you pulling for it.
The flow at a glance
Why webhooks instead of polling
- Rate limits are per API key, not per endpoint (see Rate Limiting):
every poll spends from the same budget as every other call your integration makes. A
tight polling loop against
GET /v1/orderscompetes with the rest of your traffic for the sameRateLimit-Limit. - Latency. A poll only ever finds a change up to one polling interval late. A webhook delivery fires as soon as the event happens.
Order events and their payloads
Webhook event types are configured per webhook (see event_types below). The ones relevant to
orders are:
| Event | When it fires |
|---|---|
ORDER_CREATED | A customer completes a new order |
ORDER_UPDATED | The order status changes (confirmed, preparing, delivered, cancelled) |
ORDER_DELETED | An order is deleted from the system |
(A webhook can also subscribe to CONNECTION_CREATED, CONNECTION_DESTROYED and
FISCAL_NOTES_REQUEST — not order events, out of scope for this guide.)
Every delivery is the same envelope, regardless of event type:
{
"event_type": "ORDER_CREATED",
"event_id": "018f1c2e-9b7a-7c3e-8a9e-6a6f6e1b2a10",
"merchant_id": "restaurant_042",
"timestamp": "2026-06-20T15:30:00.000Z",
"data": { "...": "the order payload" }
}
data carries the order the event is about. Treat it as a notification that something changed
rather than a pinned schema to parse field-by-field: if your handler needs the authoritative,
complete current state of the order, fetch it with
GET /v1/orders/{id} using the order id, rather than
trusting every field inside data to be present and current. See the
Webhooks overview for the full delivery format (headers, source, etc.).
Registering a webhook
POST /v1/webhooks
Required scope: webhooks:write
curl -X POST https://public-api.olaclick.app/v1/webhooks \
-H "Authorization: Bearer olk_live_..." \
-H "Content-Type: application/json" \
-d '{
"webhook_url": "https://integrations.example.com/olaclick/webhook",
"merchant_id": "restaurant_042",
"event_types": ["ORDER_CREATED", "ORDER_UPDATED", "ORDER_DELETED"],
"max_retry": 5
}'
| Field | Required | Description |
|---|---|---|
webhook_url | Yes | Your HTTPS endpoint. Must use https:// and cannot point at localhost or a private/internal address — both are rejected locally before anything is saved. |
merchant_id | Yes | Your own identifier for this merchant, 1–64 characters. Included in every delivery. |
event_types | No | Which events to receive. Omit it to receive every event type. |
webhook_headers | No | Custom headers sent on every delivery. Defaults to none. |
max_retry | No | Retries on failure, 0–10. Defaults to 3. |
ack_http_codes | No | HTTP status codes considered a successful delivery. Defaults to [200, 201, 202]. |
{
"data": {
"webhook_id": "7a1e9c3a-4b2d-4f7e-9a1b-3c6d8e2f5a90",
"company_id": "5e2a8c1d-6f3b-4a9e-8d2c-1b7f4e9a3c60",
"webhook_url": "https://integrations.example.com/olaclick/webhook",
"merchant_id": "restaurant_042",
"origin": "API",
"webhook_headers": {},
"is_active": true,
"max_retry": 5,
"ack_http_codes": [200, 201, 202],
"event_types": ["ORDER_CREATED", "ORDER_UPDATED", "ORDER_DELETED"],
"signing_secret": "whsec_7f3a1c9e2b4d6f8a0c1e3b5d7f9a2c4e",
"created_at": "2026-06-20T15:00:00.000Z",
"updated_at": "2026-06-20T15:00:00.000Z"
}
}
Manage it afterwards with GET /v1/webhooks /
GET /v1/webhooks/{id} (webhooks:read),
PATCH /v1/webhooks/{id} and
DELETE /v1/webhooks/{id} (webhooks:write).
Verifying the signature
Every delivery carries X-OlaClick-Signature: sha256=<hex-encoded-hmac>, computed over the
raw request body with your webhook's signing_secret. Verify it before trusting the
payload:
const crypto = require('crypto');
function verifySignature(rawBody, secret, signatureHeader) {
if (typeof signatureHeader !== 'string') return false;
const expected = Buffer.from(
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'),
);
const received = Buffer.from(signatureHeader);
// timingSafeEqual throws when lengths differ, so compare lengths first.
return expected.length === received.length && crypto.timingSafeEqual(expected, received);
}
Use the raw, unparsed body (not a re-serialized version) and a timing-safe comparison. See the Webhooks overview for the full Node.js and Python examples.
Retries, and building a fast, idempotent handler
If your endpoint doesn't answer with one of ack_http_codes (200, 201, 202 by default),
OlaClick retries up to max_retry times with exponential backoff (30s, 60s, 120s, ...). That
means your handler has to assume at-least-once delivery: the same event can arrive more than
once, and a slow handler risks a delivery being retried even though it was actually being
processed.
- Respond fast, but store first. Verify the signature, durably store or enqueue the event
(with its
event_id), then acknowledge with 2xx and do the real work (recalculating something, writing to your own database, notifying a rider) from that queue. Once retries are exhausted nothing replays the event, so an event you acknowledged but never stored is lost. - Dedupe by
event_id. Keep a record of processedevent_ids for at least the whole retry window and skip anything you've already handled, so a retried delivery is a no-op instead of a double-processed order. - Make the handler idempotent. Design the update itself (e.g. "set this order's local status to X") so applying it twice has the same result as applying it once.
- Don't let an older event overwrite a newer one. Retries can deliver an older
ORDER_UPDATEDafter a newer one, so treat the event as a signal: re-fetch the order withGET /v1/orders/{id}and apply its current state instead of the state in the payload.
When a GET /v1/orders reconciliation is still appropriate
Webhooks assume your endpoint is reachable. If it was down — a deploy, an outage, a
misconfigured URL — events that fired during that window are gone once max_retry is
exhausted; nothing replays them later. After your endpoint is back up, backfill the gap with
GET /v1/orders, filtered to the downtime window:
curl "https://public-api.olaclick.app/v1/orders?filter[start_date]=2026-06-20&filter[end_date]=2026-06-20&filter[time_type]=updated_at" \
-H "Authorization: Bearer olk_live_..."
filter[time_type]=updated_at catches orders that changed during the window, not just ones
created in it — closer to what a missed ORDER_UPDATED would have told you. filter[status]
narrows it further if you only care about certain statuses. Page through with page /
per_page if the window has more than one page of orders.
Outside of a reachability gap, prefer the webhook: a reconciliation poll on a schedule is exactly the pattern this guide is meant to replace.
Doing it through the MCP server
| Tool | What it does |
|---|---|
list_webhooks | List all webhook configurations for your company |
get_webhook | Get a single webhook configuration by UUID |
create_webhook | Register a new webhook URL |
update_webhook | Partially update a webhook configuration |
delete_webhook | Remove a webhook subscription |
Needed scopes: webhooks:read (list/get), webhooks:write (create/update/delete).
Example prompts:
"Register a webhook at https://integrations.example.com/olaclick/webhook for order events"
"List my webhooks"
"Stop sending ORDER_DELETED events to my webhook"
See the MCP Server page for every tool's full parameters.