Skip to main content
POST
/
api
/
v1
/
webhooks
/
aggregators
/
order-status
Aggregator order status
curl --request POST \
  --url https://app.fire.rest/api/v1/webhooks/aggregators/order-status \
  --header 'Content-Type: application/json' \
  --header 'x-api-key: <x-api-key>' \
  --data '
{
  "channelCode": "<string>",
  "status": "<string>",
  "providerEventId": "<string>",
  "occurredAt": "<string>",
  "orderId": "<string>",
  "externalOrderId": "<string>",
  "metadata": {}
}
'
{
  "received": true,
  "duplicate": false,
  "eventId": "b1f2c3d4-5e6f-5a7b-8c9d-0e1f2a3b4c5d",
  "webhookEventId": "9e6c8af8-af80-4967-9422-c096ab43c0e7",
  "status": "queued",
  "firstReceivedAt": "2026-06-14T18:46:00.512Z",
  "message": "Event accepted and queued for processing."
}
This endpoint is inbound — your delivery aggregator (Rappi, Uber, Didi, iFood, PedidosYa, Glovo…) POSTs to it whenever it advances the order on its side: a courier was assigned, the order was picked up, it is on route, it was delivered, and so on. Fire authenticates the request, resolves the order, mirrors the latest status onto orders.aggregator, and records the event in its aggregator event log for observability.
This endpoint is asynchronous. Fire authenticates, resolves the order (tenant + channel guards), deduplicates, and enqueues the event — then returns 202 Accepted with a webhookEventId (typically under 100 ms). The order mirror is updated a moment later by a background worker (usually within ~2 seconds). To check the outcome, poll GET /v1/webhooks/events/{webhookEventId}. Client-fixable problems (bad payload, order not found, wrong tenant or channel, conflicting ids) are rejected synchronously with 4xx before the 202.
Status is passthrough. Aggregators don’t share a status vocabulary, so Fire does not impose an enum: status is stored verbatim as the event type (courier_assigned, on_route, entregue, whatever your channel uses). The order’s current status is the one with the latest occurredAt — not a fixed lifecycle order. There is no anti-regression gate: a later timestamp wins, period. Friendly, translated labels are a display concern resolved from the channel catalog, never enforced here.
No Fire-emitted eventId to echo. Unlike the fiscal callback and KDS statuses — which echo an event.id Fire emitted — an aggregator status is a spontaneous external event. Fire is not the source of truth here, so there is no source-of-truth check on an eventId. Instead you tell Fire which order the status belongs to, via order resolution below. Idempotency is keyed on your providerEventId (see Idempotency).

Order resolution

You must tell Fire which order this status belongs to. There are two paths, and you may send either or both — at least one is required:
FieldResolves byWhen to use
orderIdOur orders.id (UUID)You stored Fire’s order id (e.g. echoed from an outbound event or the injection response)
externalOrderIdThe external (XMART/aggregator) order id, matched against the order’s metadata.order_idYou only know your own order reference
Both are vendor-scoped: the resolved order must belong to the account + vendor bound to your API key, and its channel must match channelCode.
If you send both ids, they must point to the same order. Fire resolves each independently; if orderId and externalOrderId resolve to different orders, the request is rejected with 409 Conflict — Fire will not guess which one you meant. Send one, or send both pointing at the same order.

Authentication

This endpoint requires a vendor-scoped API key with the webhooks:aggregator scope (account + vendor binding). Fire enforces that the resolved order belongs to that vendor. Keys without the scope, or without a vendor binding, are rejected with 403 Forbidden.
x-api-key
string
required
Your Fire vendor-scoped API key with scope webhooks:aggregator. Generate one from Developers → API Management for the account/vendor whose orders this key may report on.
Authorization
string
Optional Bearer <token> — accepted as a legacy alternative to x-api-key. Send one or the other.

Request body

channelCode
string
required
The aggregator/channel code — it must equal the order’s metadata.channel.code (channels.code, e.g. RAPPI, UBER, or the numeric channel id like 99). If it doesn’t match the resolved order’s channel, Fire responds 403.
status
string
required
The raw delivery status, passthrough — stored verbatim as the event type. Any non-empty string is accepted (accepted, courier_assigned, picked_up, on_route, delivered, cancelled, or your channel’s own labels). No enum is enforced.
providerEventId
string
required
Your aggregator’s own id for this delivery — the idempotency key (together with channelCode). Fire maps (channelCode, providerEventId) to a stable internal event id, so resending the same pair with the same status is a safe replay. Use your native event id if available; otherwise a UUID.
occurredAt
string
required
ISO 8601 UTC timestamp of when the status changed on the aggregator side — not when it was sent. This is what orders the journey: the status with the latest occurredAt is the order’s current status.
orderId
string
UUID of the order in Fire. Required if externalOrderId is absent. See Order resolution.
externalOrderId
string
The external (XMART/aggregator) order id, matched against the order’s metadata.order_id. Required if orderId is absent. See Order resolution.
metadata
object
Optional free-form bag of extra fields (courier name, tracking url, etc.). Stored as-is, not validated.

Examples

The reports of the same delivery share a channelCode and resolve to the same order; each carries its own status, providerEventId, and occurredAt:
courier_assigned (by Fire orderId)
{
  "channelCode": "RAPPI",
  "status": "courier_assigned",
  "providerEventId": "evt-7af3-0001",
  "occurredAt": "2026-06-14T18:46:00.000Z",
  "orderId": "7a3a7d6b-1234-4abc-9def-0123456789ab"
}
on_route (by external order id)
{
  "channelCode": "RAPPI",
  "status": "on_route",
  "providerEventId": "evt-7af3-0002",
  "occurredAt": "2026-06-14T18:52:00.000Z",
  "externalOrderId": "RP-2026-558831"
}
delivered (both ids — must point to the same order)
{
  "channelCode": "RAPPI",
  "status": "delivered",
  "providerEventId": "evt-7af3-0003",
  "occurredAt": "2026-06-14T19:07:00.000Z",
  "orderId": "7a3a7d6b-1234-4abc-9def-0123456789ab",
  "externalOrderId": "RP-2026-558831",
  "metadata": { "courier": "Ana P.", "trackingUrl": "https://rappi.example/t/abc" }
}

Response

On success the endpoint returns 202 Accepted — the event was authenticated, resolved, deduplicated, and enqueued. A 202 does not mean the order mirror was updated yet; that happens asynchronously. Use the status endpoint to confirm. The body carries two distinct ids: eventId is Fire’s stable internal id for this (channelCode, providerEventId) pair, webhookEventId is Fire’s id for the queued record. Same shape as the fiscal callback.
received
boolean
Always true when the request was accepted and enqueued.
duplicate
boolean
true when this exact status report was already ingested (same order, same (channelCode, providerEventId), same status) — the existing record is returned and nothing is re-enqueued. false for a fresh report (including a different status of the same delivery — that’s a new step, not a duplicate).
eventId
string
Fire’s stable internal id derived from (channelCode, providerEventId).
webhookEventId
string
Fire’s id for the queued record. Pass it to GET /v1/webhooks/events/{webhookEventId} to poll the outcome. On a duplicate it is the same id returned the first time.
status
string
Current queue status — queuedprocessingprocessed (and retry / failed / dead / ignored).
firstReceivedAt
string
ISO 8601 UTC timestamp of when Fire first received this report. Stable across retries.
message
string
Human-readable summary.
{
  "received": true,
  "duplicate": false,
  "eventId": "b1f2c3d4-5e6f-5a7b-8c9d-0e1f2a3b4c5d",
  "webhookEventId": "9e6c8af8-af80-4967-9422-c096ab43c0e7",
  "status": "queued",
  "firstReceivedAt": "2026-06-14T18:46:00.512Z",
  "message": "Event accepted and queued for processing."
}

Checking the outcome

Because processing is asynchronous, the 202 only confirms the event was queued. To see whether the order mirror was updated, poll the companion status endpoint with the webhookEventId returned by the 202:
GET https://app.fire.rest/api/v1/webhooks/events/{webhookEventId}
x-api-key: <your webhooks:aggregator key>
status
string
Queue lifecycle: queuedprocessingprocessed (done) · failed / dead (gave up after retries) · retry (waiting for the next attempt) · ignored (handled, no action needed — e.g. a duplicate).
attempts
number
Processing attempts so far.
result
object | null
On success, the worker outcome — e.g. { "kind": "merged", "current": "delivered" }.
error
object | null
{ "message": "…" } when the last attempt failed; null otherwise.
A 404 is returned for unknown ids — or ids that belong to another tenant — with no existence leak. Authenticate with the same webhooks:aggregator key you used for the event.

Idempotency & the journey

Fire deduplicates on the order plus (channelCode, providerEventId) and the statusnot on a Fire-emitted eventId. This is what makes the journey work while keeping retries clean:
ScenarioResult
courier_assigned, then on_route, then delivered — different status (and providerEventId), same ordereach is a new step202 duplicate:false
The same report resent — same order, same (channelCode, providerEventId), same status202 duplicate:true — existing record returned, not re-processed
Neither orderId nor externalOrderId sent400
Order not found for this vendor404
orderId and externalOrderId resolve to different orders409
channelCode ≠ the order’s channel, or key not for this vendor403
Auth store momentarily unreachable503 — transient, retry
A different status is never a duplicate. Because aggregators legitimately report many statuses for one delivery, two reports with the same (channelCode, providerEventId) but a different status are two distinct steps — Fire stores both. Keep providerEventId unique per status report to avoid accidentally replaying a step.

The order mirror

Once processed, the latest status is mirrored onto the order’s aggregator block, with the full journey kept in history (ordered by occurredAt). The current status is the entry with the latest occurredAt:
orders.aggregator
{
  "channelCode": "RAPPI",
  "status": "delivered",
  "occurredAt": "2026-06-14T19:07:00.000Z",
  "history": [
    { "status": "courier_assigned", "occurredAt": "2026-06-14T18:46:00.000Z" },
    { "status": "on_route",         "occurredAt": "2026-06-14T18:52:00.000Z" },
    { "status": "delivered",        "occurredAt": "2026-06-14T19:07:00.000Z" }
  ]
}
The raw status is stored as-is; any friendly, translated label is resolved at display time from the channel’s status catalog — the stored value never changes.

Inject order

The order injection endpoint that creates the order this status references.

KDS order status

The sibling inbound webhook for kitchen state — same async + queue model, but Fire-emitted eventId and anti-regression.

Fiscal callback

The fiscal inbound webhook — same async + idempotency envelope.

Authentication

How API keys, scopes, and vendor binding work.