Skip to main content

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/orders competes with the rest of your traffic for the same RateLimit-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:

EventWhen it fires
ORDER_CREATEDA customer completes a new order
ORDER_UPDATEDThe order status changes (confirmed, preparing, delivered, cancelled)
ORDER_DELETEDAn 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
}'
FieldRequiredDescription
webhook_urlYesYour HTTPS endpoint. Must use https:// and cannot point at localhost or a private/internal address — both are rejected locally before anything is saved.
merchant_idYesYour own identifier for this merchant, 1–64 characters. Included in every delivery.
event_typesNoWhich events to receive. Omit it to receive every event type.
webhook_headersNoCustom headers sent on every delivery. Defaults to none.
max_retryNoRetries on failure, 0–10. Defaults to 3.
ack_http_codesNoHTTP 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 processed event_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_UPDATED after a newer one, so treat the event as a signal: re-fetch the order with GET /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​

ToolWhat it does
list_webhooksList all webhook configurations for your company
get_webhookGet a single webhook configuration by UUID
create_webhookRegister a new webhook URL
update_webhookPartially update a webhook configuration
delete_webhookRemove 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.