Skip to content

Status webhook

If you give us a callback URL during onboarding, we POST a notification to it every time the status of an order you created changes, after the change is saved. There is no notification when you create the order — the create response covers that. The callback URL must use https://. The same URL also receives stock webhooks.

Every order endpoint and the webhook use the same five statuses:

Status Meaning What you do
PENDING Order received, not started yet Nothing — this is what Create order returns. You get no webhook for it, unless our team puts an order back on hold.
PROCESSING The warehouse is picking and packing it Optional — show “being prepared” to your customer
READY_TO_SHIP Packed and waiting for the courier Book the courier and send the AWB via Attach shipping label — the order cannot leave without it
SHIP Handed to the courier Tell your customer it’s shipped; awb and courier are in the payload
CANCELED Cancelled — can happen at any point Refund or release the order on your side

You are notified when an order’s status changes, not for every internal warehouse step — so an order usually sends PROCESSING, READY_TO_SHIP, SHIP. A PROCESSING notification can occasionally be skipped when several steps happen at once; READY_TO_SHIP, SHIP and CANCELED are always sent, and Get order always returns the current status. You may occasionally receive the same status twice (with a higher version) — treat it as a refresh of the same state.

{
"version": 1,
"type": "order_update",
"id": "5b8f0d7e-2c1a-4b7e-9d0f-3a6e1c2b4d5f",
"timestamp": "2026-09-30T03:00:00.000Z",
"data": {
"outboundId": "a1b2c3d4-...",
"orderNumber": "PERSIJA-ORD-10245",
"warehouseCode": "WH-JKT",
"status": "READY_TO_SHIP",
"version": 6,
"awb": "JNE1234567890",
"courier": "JNE"
}
}

id is unique per event and identical on every retry. data is the order’s current state; data.version only ever goes up for an order. awb and courier are omitted until known.

Every delivery carries two headers:

x-revota-timestamp: <Unix seconds when we sent this attempt>
x-revota-signature: <hex-encoded HMAC-SHA256>

Compute HMAC-SHA256 of <x-revota-timestamp>.<raw body> using your x-secret-key as the key, hex-encode it, and compare against the signature header with a constant-time comparison. Reject the request if they don’t match, or if the timestamp is more than 5 minutes from your clock.

Use the raw request body exactly as received. Parsing the JSON and serialising it again changes the bytes (spacing, key order, escaped characters) and the signature will not match. Example in Node.js:

// rawBody: the request body as a string, before JSON parsing
// (in Express: express.raw({ type: 'application/json' }) on this route)
const crypto = require('crypto');
function isFromRestock(rawBody, headers, secretKey) {
const timestamp = headers['x-revota-timestamp'];
const signature = headers['x-revota-signature'] || '';
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac('sha256', secretKey)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return (
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}
  • Drop an id you have already processed — delivery is at least once.
  • Apply data only if data.version is higher than the version you stored for that outboundId — events can arrive out of order.
  • Respond with 2xx within 10 seconds, then do the slow work.
Your response What we do
2xx Delivered, done
408, 429, 5xx, timeout, connection error Retried for about 3 hours with growing gaps
3xx, any other 4xx Not retried (redirects are not followed)

Ask your Restock contact to set or change your callback URL.