Sale webhook events
The sale.* event family delivers full sale payloads inline for every sale recorded on Flipdish, regardless of channel.
Overview
The sale.* event family fires for every sale recorded on Flipdish — regardless of channel (POS, kiosk, web, marketplace, or API). The full sale object is embedded in the event payload, so no follow-up GET is required.
| Event | Description |
|---|---|
sale.created.v1 | A sale was first recorded. Carries the full PublicSale object inline, including the current status field. |
sale.accepted.v1 | A pending sale has been accepted by the receiver. Carries the full PublicSale object inline without a status field — acceptance is implied by the event name. |
sale.updated.v1 | A sale's content changed materially (items, charges, discounts modified) or a lifecycle transition occurred. Carries the updated PublicSale object inline, including the current status field. |
sale.status.updated.v1 | A thin lifecycle status change event (kitchen prepared, dispatched, cancelled, delivered, accepted, rejected, etc.). Does not carry a full sale object. |
Important — accepted-first lifecycle change
sale.created.v1no longer implies the sale has been accepted.With the accepted-first lifecycle (enabled per org via a feature flag), a sale is created in a pending state before the restaurant accepts it.
sale.created.v1fires as soon as the sale is created — whether it is pending or already accepted.What to use instead:
- To know when a sale is accepted, listen for
sale.accepted.v1(simplest — dedicated event, no status field needed), orsale.status.updated.v1wherestatus=SALE_ACCEPTED, or check thestatusfield onsale.updated.v1.- To know when a sale is rejected before acceptance, listen for
sale.status.updated.v1wherestatus=SALE_REJECTED.- For orgs not yet on the accepted-first lifecycle,
sale.created.v1continues to represent an already-accepted sale (status=SALE_ACCEPTED).
Sale lifecycle
The sale goes through the following lifecycle states, visible in the status field on PublicSale and in sale.status.updated.v1 events:
| Status | Meaning | Terminal? |
|---|---|---|
SALE_CREATED | Pending — created but not yet accepted by the restaurant | No |
SALE_ACCEPTED | Accepted by the restaurant; fulfilment in progress | No |
SALE_PREPARED_BY_KITCHEN | Kitchen has finished preparing the sale | No |
SALE_DISPATCHED | Dispatched / handed to the courier | No |
SALE_ON_THE_WAY | En route to the customer | No |
SALE_DELIVERED | Delivered to the customer | Yes |
SALE_REJECTED | Rejected before acceptance — sale will not be fulfilled | Yes |
SALE_CANCELLED | Cancelled after acceptance | Yes |
Simplified lifecycle flow:
SALE_CREATED ──accept──► SALE_ACCEPTED ──► SALE_PREPARED_BY_KITCHEN ──► ...
│
└──reject──► SALE_REJECTED (terminal — never fulfilled)
SALE_ACCEPTED ──► (any fulfilment state) ──cancel──► SALE_CANCELLED (terminal)
SALE_REJECTEDvsSALE_CANCELLED
SALE_REJECTED— the sale was never accepted. The restaurant rejected the pending order before acceptance. These sales must not be counted as accepted revenue or cancelled fulfilments.SALE_CANCELLED— the sale was accepted but was subsequently cancelled.To distinguish them, listen to
sale.status.updated.v1: thestatusfield will beSALE_REJECTEDorSALE_CANCELLEDaccordingly.
Note: thestatusfield inPublicSale(onsale.created.v1/sale.updated.v1) showsSALE_CANCELLEDfor both;sale.status.updated.v1is the authoritative source for the rejection signal.
Events
sale.created.v1
sale.created.v1Fires when a sale is first recorded. With the accepted-first lifecycle enabled, this fires for pending sales (before acceptance). Check the data.sale.status field to determine whether the sale is pending (SALE_CREATED) or already accepted (SALE_ACCEPTED).
The data.sale field carries the complete PublicSale object at the moment of creation: items, modifiers, charges, discounts, payments, customer, delivery/dine-in details, menu reference, and the current status. See the Sale status table and Sale object schema for details.
{
"eventType": "sale.created.v1",
"eventId": "01HXYZ...",
"eventCreatedAt": "2025-10-28T14:00:00.000Z",
"data": {
"orgId": "org123",
"brandId": "br123",
"propertyId": "p123",
"salesChannelId": "sc123",
"salesChannelType": "UberEats",
"saleId": "3N356",
"sale": {
"status": "SALE_CREATED",
"dispatchType": "Delivery",
"externalId": "sale-external-123",
"displayId": "2A003",
"customer": { "..." },
"items": [ "..." ],
"charges": [ "..." ]
}
}
}
sale.created.v1withstatus: "SALE_CREATED"— what this meansThe sale has been recorded and is awaiting acceptance. It is not yet accepted and must not be counted as confirmed revenue. Listen for
sale.status.updated.v1withstatus: "SALE_ACCEPTED"(or checksale.updated.v1when the same sale'sstatusbecomesSALE_ACCEPTED) to know when fulfilment begins.For orgs without the accepted-first lifecycle,
sale.created.v1will carrystatus: "SALE_ACCEPTED"— behaviour is unchanged.
sale.accepted.v1
sale.accepted.v1Fires when a pending sale has been accepted by the receiver. This is the simplest event to listen to for acceptance — the sale status is implied by the event name (SALE_ACCEPTED) and is not included as a field in the payload. No status field to check.
The embedded sale carries the full sale payload at the moment of acceptance: items, charges, customer details, delivery/dine-in info, and monetary totals.
{
"eventType": "sale.accepted.v1",
"eventCreatedAt": "2025-10-28T14:05:00.000Z",
"data": {
"orgId": "org123",
"saleId": "3N356",
"sale": {
"dispatchType": "Delivery",
"externalId": "sale-external-123",
"customer": { "..." },
"items": [ "..." ],
"charges": [ "..." ]
}
}
}
Choosing betweensale.accepted.v1,sale.updated.v1, andsale.status.updated.v1for acceptanceAll three events fire on acceptance. Which to use:
Event When to use sale.accepted.v1You only need to act on acceptance and want the full sale payload. Simplest choice — no status field to check. sale.updated.v1You already subscribe for content changes and want to handle acceptance in the same handler. Check data.sale.status === 'SALE_ACCEPTED'.sale.status.updated.v1You only need identifiers and the transition time, not the full sale payload.
sale.updated.v1
sale.updated.v1Fires when a sale's content changes materially — items added or removed, charges or discounts modified — or when a lifecycle transition occurs (e.g. accepted, rejected). The embedded sale reflects the post-update state, including the current status field.
Check data.sale.status to understand the current lifecycle position when handling this event.
{
"eventType": "sale.updated.v1",
"eventCreatedAt": "2025-10-28T14:05:00.000Z",
"data": {
"orgId": "org123",
"saleId": "3N356",
"sale": {
"status": "SALE_ACCEPTED",
"..."
}
}
}sale.status.updated.v1
sale.status.updated.v1A unified thin status event covering all lifecycle transitions: pending creation, acceptance, rejection, kitchen preparation, dispatch, delivery, and cancellation. Does not carry a full sale object — only the lifecycle identifiers and the relevant timestamp field.
All sale.status.updated.v1 events carry saleStatusUpdatedTime — a single ISO 8601 UTC timestamp for when the transition occurred. SALE_DISPATCHED and SALE_ON_THE_WAY also carry a deprecated dispatchTime; SALE_DELIVERED carries a deprecated deliveryTime — use saleStatusUpdatedTime in preference.
SALE_CREATEDis not asale.status.updated.v1status
SALE_CREATED(pending sale) is signalled bysale.created.v1alone — nosale.status.updated.v1is emitted at creation.sale.status.updated.v1fires only for transitions: acceptance, rejection, kitchen preparation, dispatch, delivery, and cancellation.
Status values:
| Status | Meaning |
|---|---|
SALE_ACCEPTED | Sale accepted by restaurant |
SALE_REJECTED | Never-accepted sale rejected (terminal) |
SALE_PREPARED_BY_KITCHEN | Kitchen finished preparing |
SALE_DISPATCHED | Dispatched to courier |
SALE_ON_THE_WAY | En route |
SALE_DELIVERED | Delivered |
SALE_CANCELLED | Cancelled after acceptance |
SALE_REJECTED additionally carries: rejectionReason (required — one of the SaleRejectionReason values), rejectionNotes (optional free-text), rejectedBy (optional identifier of the actor).
externalIdon status events
sale.status.updated.v1does not carry asalesub-object.externalIdis present as a top-level field when known. If you need additional sale fields for a givensaleId, correlate via the org-wide GET endpoint.
Webhook body structure
All sale.* webhook deliveries share the same outer envelope:
| Field | Notes |
|---|---|
eventType | Event name, e.g. sale.created.v1. |
eventId | Unique delivery ID for this event instance. |
eventCreatedAt | ISO 8601 UTC timestamp of when the event was produced. |
data | Event-specific payload (see table below). |
data fields
data fieldsThe following fields appear inside data for all sale.* events:
| Field | Type | Notes |
|---|---|---|
orgId | string | Org the sale belongs to. |
brandId | string | Brand the sale belongs to. |
propertyId | string | Property (physical location) the sale belongs to. |
salesChannelId | string | ID of the sales channel (e.g. sc123). |
salesChannelType | enum | Platform that originated the sale: UberEats, FlipdishKIOSK, POS, etc. |
saleId | string | Flipdish sale identifier (Crockford base32, e.g. 3N356). |
sale | object | Full PublicSale payload, including status. Present on sale.created.v1 and sale.updated.v1 only. |
Integration guide
Listening for sale acceptance
Old approach (pre-accepted-first lifecycle): Subscribe to sale.created.v1 — all created sales were already accepted.
New approach — simplest: Subscribe to sale.accepted.v1. No status field to check — acceptance is implied by the event name:
function handleSaleAccepted(event) {
if (event.eventType === 'sale.accepted.v1') {
// Sale has been accepted — begin fulfilment processing
// data.sale carries the full sale payload, without a status field
processSaleAccepted(event.data);
}
}Alternatively, if you already subscribe to multiple events:
function handleSaleEvent(event) {
const { eventType, data } = event;
if (eventType === 'sale.accepted.v1') {
// Dedicated acceptance event — no status field check needed
processSaleAccepted(data);
return;
}
if (eventType === 'sale.created.v1') {
// May be pending (SALE_CREATED) or already accepted (legacy path)
storePendingSale(data);
}
if (eventType === 'sale.status.updated.v1') {
const { status, saleId } = data;
if (status === 'SALE_REJECTED') handleRejection({ saleId, ...data });
if (status === 'SALE_CANCELLED') handleCancellation({ saleId, ...data });
}
}Identifying pending vs accepted sales
Use the status field in data.sale on sale.created.v1 and sale.updated.v1:
status value | Meaning |
|---|---|
SALE_CREATED | Pending — not yet accepted. Do not count as revenue. |
SALE_ACCEPTED | Accepted — fulfilment may begin. |
Reporting and analytics
- Do not count
SALE_CREATEDorSALE_REJECTEDsales as accepted revenue. These represent pending/failed orders. - Filter your analytics pipeline to only process sales with
status === 'SALE_ACCEPTED'(or a post-acceptance status). - Use
sale.status.updated.v1(SALE_REJECTED)to identify and separately track never-accepted rejections.
Subscribing
Subscribe to sale.* events via the Webhook Service API or the Flipdish portal. See Subscribe to Flipdish Events (v3) for the full delivery contract, headers, and signature verification.
Related
- Subscribe to Flipdish Events (v3) — delivery contract and signature verification
- Sales Management API — full
PublicSaleschema, creating/cancelling sales, org-wide queries
Updated 19 days ago

