Appearance
Webhooks
Webhooks let Wasal push order events to your server in real time, so you don't need to poll.
How it works
- Register an HTTPS endpoint URL.
- When a matching event occurs, Wasal sends a
POSTrequest to your URL with a signed JSON payload. - Respond with any
2xxstatus within 8 seconds to acknowledge delivery.
Delivery is attempted once. Each event is sent a single time with an 8-second timeout — there is no automatic retry queue. If your endpoint is down, reconcile missed events by polling
GET /orderor the order's status history.
Payload format
Every webhook request has this shape:
json
{
"event": "order.status_changed",
"timestamp": "2025-06-01T10:30:00.000Z",
"data": { /* event-specific payload */ }
}Headers sent on every request:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Wasal-Event | Event name (e.g. order.status_changed) |
X-Wasal-Signature | sha256=<hmac-hex> |
Verifying the signature
Compute HMAC-SHA256 of the raw request body using your webhook's secret, then compare with the X-Wasal-Signature header.
js
const crypto = require('crypto');
function isValidSignature(rawBody, secret, signatureHeader) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}Always use
timingSafeEqualto prevent timing attacks. Reject any request where the signature doesn't match.
Available events
| Event | Triggered when |
|---|---|
order.created | A new order is placed. |
order.status_changed | An order's status changes (e.g. pending → assigned). |
order.delivered | An order is delivered. |
order.cancelled | An order is cancelled. |
If you register a webhook without specifying events, all four events are subscribed by default.
Event data fields:
| Event | data payload |
|---|---|
order.created | { orderId, orderNumber, status } |
order.status_changed | { orderId, orderNumber, status, previousStatus } |
order.delivered | { orderId, orderNumber, status, previousStatus? } |
order.cancelled | { orderId, orderNumber } |
Order statuses
The status and previousStatus fields use the following values. See the order lifecycle for their meanings and normal transition sequence.
| Status | Sent by |
|---|---|
draft | order.created when an order is created for later activation. |
pending | order.created or order.status_changed. |
assigned | order.created when immediately auto-assigned, or order.status_changed. |
on_way_to_merchant | order.status_changed. |
picked_up | order.status_changed. |
in_transit | order.status_changed. |
on_hold | order.status_changed. |
delivered | order.delivered. |
failed | order.status_changed. |
returned | order.status_changed. |
cancelled | order.status_changed for driver-led or demo transitions. Merchant-initiated cancellation sends order.cancelled; its event name implies the resulting status. |
Treat previousStatus as optional. It is included for order.status_changed and may be included for order.delivered; consumers should not require it for terminal events.
Register a webhook
POST /integration/merchant/webhook
json
{
"url": "https://yourstore.com/api/wasal-webhook",
"events": ["order.created", "order.status_changed"]
}| Field | Required | Notes |
|---|---|---|
url | Yes | Must start with https://. |
events | No | Array of event names. Omit to subscribe to all events. |
Response: 201 Created
json
{
"success": true,
"data": {
"webhook": {
"_id": "665f...",
"url": "https://yourstore.com/api/wasal-webhook",
"events": ["order.created", "order.status_changed"],
"active": true,
"secret": "a3f8b2...",
"createdAt": "2025-06-01T10:00:00.000Z"
}
}
}Save the
secret. It is shown only once at creation. You will need it to verify incoming requests.
List webhooks
GET /integration/merchant/webhook
Returns all registered webhooks for your merchant. The secret is not included in list or get responses.
Response: 200 OK — not paginated (merchants typically register only a handful of webhooks).
json
{
"success": true,
"data": {
"list": [
{
"_id": "665f...",
"url": "https://yourstore.com/api/wasal-webhook",
"events": ["order.created", "order.status_changed"],
"active": true,
"createdAt": "2026-06-01T09:00:00.000Z"
}
]
}
}Update a webhook
PUT /integration/merchant/webhook/:webhookId
Updates the URL, events, or active state. Only send the fields you want to change.
json
{
"url": "https://yourstore.com/api/wasal-v2",
"events": ["order.delivered"],
"active": false
}Response: 200 OK — updated webhook object (without secret).
Delete a webhook
DELETE /integration/merchant/webhook/:webhookId
Removes the webhook. Deliveries in flight will still complete.
Send a test ping
POST /integration/merchant/webhook/:webhookId/test
Sends a test ping event to the registered URL. Useful to verify your endpoint is reachable and your signature verification logic is correct.
Test payload delivered to your URL:
json
{
"event": "ping",
"webhookId": "665f1e2a9b3c4d5e6f708192",
"timestamp": "2025-06-01T10:30:00.000Z",
"data": { "message": "This is a test ping from Wasal" }
}Returns 200 if your endpoint responded with a 2xx, or 502 WEBHOOK_UNREACHABLE if it was unreachable.
Errors
| HTTP | code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED | url missing or not https://, or invalid event names — see errors. |
| 404 | WEBHOOK_NOT_FOUND | Webhook does not exist or is not owned by your merchant. |
| 502 | WEBHOOK_UNREACHABLE | Test ping could not be delivered to the webhook URL. |
