Sales Management API
How to create, cancel, and query sales using the v3.0 Sales Management API — including the new org-wide query endpoints
BetaThe Sales Management API is currently in beta. Endpoints may change. Contact [email protected] to request access.
Introduction
The Sales Management API is the v3.0 surface for ingesting and querying sales. Use it to:
- Push sales from an external system (POS, marketplace, kiosk, ordering platform) into Flipdish so they appear in reporting, reconciliation, and the audit trail.
- Cancel a sale when a customer or restaurant voids an order.
- Update delivery status as an order moves from kitchen to customer.
- Read sales back across an entire org using the org-wide query endpoints — no
salesChannelIdneeded. The previous sales-channel-scoped read endpoints are deprecated; see Reading sales (deprecated) below.
Related guides
- Building a POS analytics or back-office integration (forecasting, scheduling, margin)? Start with Build a POS sales analytics integration — it covers the full onboarding, menu sync, and reconciliation pipeline on top of this API.
- Building a marketplace integration that also needs menu sync, item snooze, and operations status updates? See Build a marketplace integration for the end-to-end flow covering those extra surfaces.
All paths live under https://api.flipdish.co and require Authorization: Bearer <token> (OAuth2 client-credentials). See Getting Started for the token exchange.
Concept model
| Concept | Format | Notes |
|---|---|---|
| Org | org123 | Top-level tenant. Every Sales Management path is scoped to an orgId. |
| Brand | br123 — legacy: fd123, or a plain slug (e.g. mexicancafe) | The distinct identity (name, logo) associated with one or more sales channels. |
| Property | p123 | A physical location. |
| Sales channel | sc123 — legacy: a plain integer with no prefix (e.g. 123) | A named selling surface on a property (POS, kiosk, web, marketplace). Sales belong to a sales channel. |
| Sale | saleId (e.g. AF34FV, Crockford base32) + optional externalId | A transaction. saleId is a Flipdish-globally-unique short ID; the org-wide query endpoints' schema also allows a UUID form. externalId is your reference, round-tripped on every related event. |
| Menu revision | UUID + numeric revisionId | Every sale references the exact menu revision it was placed against. |
Don't parse or validate ID shapes
saleId,brandId, andsalesChannelIdare opaque strings — no format is enforced, and orgs/brands/channels created before the current naming convention may not match the common shape shown above. Compare them by exact string equality only; never assume a prefix, length, or character set.
Authentication
All Sales Management endpoints use the OAuth2 Bearer token:
curl https://api.flipdish.co/salesManagement/orgs/org123/sales \
-H 'Authorization: Bearer <access_token>' \
-H 'User-Agent: <your_app_name>/<version>'Obtain the token via the client-credentials grant (see Getting Started). Send it on every request. Never send it from a browser or mobile client — server-side only.
Creating a sale
POST /salesManagement/orgs/{orgId}/salesRecords a completed sale against the org. The x-idempotency-key header is required; supply a fresh GUID (UUID v4) per logical sale. If a request with the same key was already processed, the original response is returned rather than creating a duplicate — this makes the call safe to retry on network failures.
Minimal example
curl -X POST https://api.flipdish.co/salesManagement/orgs/org123/sales \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-H 'x-idempotency-key: 11111111-1111-1111-1111-111111111111' \
-H 'User-Agent: <your_app_name>/<version>' \
-d '{
"salesChannelId": "sc123",
"requestedFulfillmentTime": "2025-10-28T14:30:00Z",
"desiredAsap": true,
"dispatchType": "TakeAway",
"menuId": "123e4567-e89b-12d3-a456-426614174000",
"menuRevisionId": "23",
"items": [ { "menuItemId": "12a85f64-5717-4562-b3fc-2c963f66afb5", "quantity": 1, "unitPrice": 10.00, "modifierItems": [] } ],
"charges": [],
"discounts": [],
"payments": [ { "type": "Sale", "paymentMethod": "Cash", "amount": 10.00, "paidAt": "2025-10-28T14:30:00Z" } ]
}'Response:
{
"data": {
"saleId": "AF34FV",
"createdAt": "2025-10-28T14:25:10Z",
"dispatchTime": "2025-10-28T14:30:00Z"
}
}Full request with all fields
{
"salesChannelId": "sc123",
"source": "POS",
"requestedFulfillmentTime": "2025-10-28T14:30:00Z",
"desiredAsap": false,
"dispatchType": "DineIn",
"externalId": "your-platform-order-12345",
"displayId": "2A003",
"menuId": "123e4567-e89b-12d3-a456-426614174000",
"menuRevisionId": "23",
"customer": {
"name": "Jane Smith",
"contactPhoneNumber": "+353871234567",
"emailAddress": "[email protected]",
"externalId": "customer_456"
},
"dineIn": {
"tableId": "12",
"guests": 2
},
"notes": "Allergen note: contains nuts",
"items": [
{
"menuItemId": "12a85f64-5717-4562-b3fc-2c963f66afb5",
"quantity": 1,
"unitPrice": 13.99,
"notes": "No onions",
"modifierItems": [
{ "menuItemId": "10a85f64-5717-4562-b3fc-2c963f66afb3", "quantity": 1, "unitPrice": 1.50 }
]
}
],
"charges": [ { "type": "Service", "amount": 1.55 } ],
"discounts": [ { "type": "Spot", "amount": 1.00 } ],
"payments": [
{
"type": "Sale",
"paymentMethod": "Credit",
"amount": 16.04,
"paidAt": "2025-10-28T14:25:00Z",
"description1": "Visa ****4921",
"description2": "VM278412312"
}
],
"metadata": "{\"posTerminal\":\"till-2\"}"
}Field reference
| Field | Required | Notes |
|---|---|---|
salesChannelId | Yes | The channel this sale belongs to. |
requestedFulfillmentTime | Yes | ISO 8601 UTC. When the restaurant hands over the food. |
dispatchType | Yes | DineIn | TakeAway | Collection | Delivery. Note capital A in TakeAway. |
menuId / menuRevisionId | Yes | Must match the published revision in use. Pass the revision the customer ordered against. |
items[] | Yes (min 1) | Recursive — modifierItems[] follow the same shape. menuItemId + quantity + unitPrice required per item. |
charges[] | Yes (can be empty) | type ∈ Delivery, Service, Tip, Other. Optional itemId for line-level charges. |
discounts[] | Yes (can be empty) | type ∈ Voucher, Loyalty, Spot, Other. code required when type is Voucher or Loyalty. |
payments[] | Yes (can be empty — implies unpaid) | type ∈ Sale, Refund. paymentMethod ∈ Cash, Credit, Online, PhonePayment, ExternalPayment. |
externalId | Recommended | Your idempotency key on your own system. Round-tripped on every sale event. |
displayId | No | Customer- and staff-facing short ID (max 15 chars). |
source | No | Free text. E.g. POS, App, ChatGPT. |
desiredAsap | No | If true but requestedFulfillmentTime is more than ~1 hour out, it is overridden to false. |
customer | No | Use contactPhoneNumber (E.164 format, e.g. +353871234567). The phoneNumber field is deprecated. |
delivery | Conditional | Required when dispatchType is Delivery. Set deliveredBy to Client (restaurant-managed) or External (third-party driver). Include location when deliveredBy is Client. |
dineIn | Conditional | Required when dispatchType is DineIn. Supply tableId (string) and guests (int). |
notes | No | Free-text note for restaurant/kitchen staff (e.g. an allergen warning). Not shown to the customer. Distinct from delivery.notes, which carries the customer's own instructions. |
metadata | No | Free-text JSON string for any partner-specific fields. |
Sale object schema
The shapes above (salesChannelId, items, charges, …) form the PublicSale object — one consistent schema used in three places:
- The request body for
POST .../sales. - The response body for every org-wide
GETsale endpoint (the deprecated channel-scopedGETendpoints return the same shape). - The
salefield embedded insale.created.v1/sale.updated.v1webhook events (see Sale webhook events).
This section documents every field of the nested customer, delivery, dineIn, items, charges, discounts, and payments objects. The top-level fields are covered in the field reference above.
customer
customerOptional. Identifies the end customer placing the sale.
| Field | Type | Notes |
|---|---|---|
id | UUID | Flipdish customer ID. If provided, must reference an existing customer — omit or leave empty if unknown. |
externalId | string | Your platform's own customer identifier. |
name | string | Customer's full name. |
contactPhoneNumber | string | E.164 format with a leading + (e.g. +353871234567). The legacy phoneNumber field (no +, no formatting) is deprecated — use contactPhoneNumber for new integrations. |
contactMaskingCode | string | Numeric code some marketplaces (e.g. Deliveroo) issue alongside a relay phone number. Entering this code when calling the relay number routes the call through to the customer's real phone. Only present when the originating marketplace obfuscates the customer's number. |
emailAddress | string | Customer email address. |
delivery
deliveryRequired when dispatchType is Delivery.
| Field | Type | Notes |
|---|---|---|
deliveredBy | enum | Client — the restaurant fulfils the delivery itself. External — a marketplace or third-party courier delivers. |
location | object | The drop-off address. Required when deliveredBy is Client — not needed for External, since the courier's own platform handles routing. See delivery.location below. |
notes | string | Free-text delivery/prep instructions from the customer (e.g. "Leave at door, don't ring the bell"). Distinct from the sale-level notes field, which is for restaurant/kitchen staff rather than the customer. |
delivery.location
delivery.location| Field | Type | Notes |
|---|---|---|
addressFields.line1 | string | Required. First address line. |
addressFields.line2 / addressFields.line3 | string | Optional further address lines. |
addressFields.postCode | string | Postal code. |
countryCode | string | Required. ISO 3166-1 alpha-2 country code (e.g. IE). |
coordinates.latitude / coordinates.longitude | number | Optional geographic coordinates for the drop-off point. |
dineIn
dineInRequired when dispatchType is DineIn.
| Field | Type | Notes |
|---|---|---|
tableId | string | Table identifier (free text). |
guests | integer | Number of guests, >= 0. |
items[]
items[]At least one item is required. Items nest recursively: a modifierItems[] entry has the exact same shape as its parent item, so any depth of modifier (e.g. pizza → size → topping → extra-cheese) is expressed as nested items rather than a separate schema.
| Field | Type | Notes |
|---|---|---|
menuItemId | UUID | Must match an item in the menu/revision referenced by the sale's menuId/menuRevisionId. |
quantity | integer | > 0. |
unitPrice | number | Price of one unit of this item (or modifier). |
notes | string | Free-text instruction for this line, e.g. "No onions please". |
modifierItems[] | array | Modifiers applied to this item — same schema as items[], so entries can themselves carry modifierItems[]. |
charges[]
charges[]Required on create, but the array can be empty. Additional monetary lines beyond item prices.
| Field | Type | Notes |
|---|---|---|
type | enum | Delivery, Service, Tip, Other. |
amount | number | >= 0. |
itemId | UUID | Optional. Set only when the charge is attached to a specific item (e.g. a bag or container deposit) rather than the whole sale. Omit for sale-level charges (e.g. an overall service charge). |
discounts[]
discounts[]Required on create, but the array can be empty. Discounts always apply at the sale level — there is no per-line discount. Every discount carries amount (number, >= 0) and type; whether code is required depends on type:
type | code | Notes |
|---|---|---|
Voucher | Required | The voucher code the customer redeemed. |
Loyalty | Required | The loyalty program code. |
Spot | Optional | An ad-hoc/manual discount applied in the moment; code is just a label if supplied. |
Other | Optional | Catch-all category; code is a free-text label if supplied. |
payments[]
payments[]Required on create, but the array can be empty — an empty array means the sale is unpaid. A refund is recorded as its own line rather than mutating the original payment: expect a Sale-type line for the original charge and a separate Refund-type line when money is returned.
| Field | Type | Notes |
|---|---|---|
type | enum | Sale (money collected) or Refund (money returned to the customer). |
paymentMethod | enum | Cash, Credit, Online, PhonePayment, ExternalPayment. |
amount | number | >= 0. |
paidAt | ISO 8601 UTC | Required. When the payment was made. |
description1 | string | Free-text payment detail, e.g. Visa ****4921. |
description2 | string | Free-text secondary detail, e.g. an authorization code or reference number. |
Cancelling a sale
POST /salesManagement/orgs/{orgId}/sales/{saleId}/cancelMarks an existing sale as cancelled. Cancellation is final — create a new sale if the customer reorders.
curl -X POST https://api.flipdish.co/salesManagement/orgs/org123/sales/AF34FV/cancel \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-H 'User-Agent: <your_app_name>/<version>' \
-d '{
"salesChannelId": "sc123",
"cancellationReason": "Cancelled by customer",
"cancellationNotes": "Customer called to cancel — out of area"
}'cancellationReason is required. Allowed values: Cancelled by customer, Cancelled by restaurant, Cancelled by marketplace, Cancelled by integration partner, Cancelled by system, Other. cancellationNotes is free text (max 255 chars) and is round-tripped on the sales.cancelled.v1 webhook event.
Updating delivery status
POST /salesManagement/orgs/{orgId}/sales/{saleId}/deliveryStatusRecords a delivery state transition as the order progresses through fulfilment. Send transitions in order.
# Mark as dispatched (left kitchen with driver)
curl -X POST https://api.flipdish.co/salesManagement/orgs/org123/sales/AF34FV/deliveryStatus \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-H 'User-Agent: <your_app_name>/<version>' \
-d '{ "salesChannelId": "sc123", "status": "SALE_DISPATCHED" }'
# Mark as delivered (customer received)
curl -X POST https://api.flipdish.co/salesManagement/orgs/org123/sales/AF34FV/deliveryStatus \
-H 'Authorization: Bearer <access_token>' \
-H 'Content-Type: application/json' \
-H 'User-Agent: <your_app_name>/<version>' \
-d '{ "salesChannelId": "sc123", "status": "SALE_DELIVERED" }'status is SALE_DISPATCHED or SALE_DELIVERED. Both transitions surface in operational dashboards and downstream reporting.
Reading sales (deprecated)
DeprecatedThe sales-channel-scoped read endpoints below are deprecated and have been removed from this reference — they no longer appear in the sidebar or in
reference/sales-management-api.json's navigation. They remain callable for existing integrations and will keep working, but won't receive further updates. New integrations should use the org-wide query endpoints instead, which cover the same use cases without requiring asalesChannelId.
GET /salesManagement/orgs/{orgId}/salesChannels/{salesChannelId}/sales?fromDate=YYYY-MM-DD&toDate=YYYY-MM-DD(formerly "List sales by date range") → use List sales (org-wide, full payload).GET /salesManagement/orgs/{orgId}/salesChannels/{salesChannelId}/sales/{saleId}(formerly "Get a sale") → use Get a sale (org-wide).
Org-wide queries (Beta)
BetaThese endpoints are currently in beta — they may change without notice.
The org-wide endpoints do not require a salesChannelId. Use them when you need to query or look up sales across all channels of an org (or scoped to one property), or when you don't know which sales channel a sale belongs to. Every org-wide list/get endpoint returns the full payload — header fields plus the nested sale object — in one call; there's no separate headers-only variant.
List sales (org-wide, full payload)
GET /sales/orgs/{orgId}/salesReturns paginated full sale payloads across all channels of the org, sorted by creation time descending. All query parameters are optional and combinable.
| Parameter | Type | Notes |
|---|---|---|
pageSize | 50–200 | Number of records per page. Default 50. |
cursor | string | Opaque pagination token from the previous response's nextCursor. |
after / before | ISO 8601 date-time | Bound the creation time range. |
# Most-recent 50 sales across the org
curl "https://api.flipdish.co/sales/orgs/org123/sales" \
-H 'Authorization: Bearer <access_token>' \
-H 'User-Agent: <your_app_name>/<version>'
# Last 24 hours
curl "https://api.flipdish.co/sales/orgs/org123/sales?after=2025-10-27T00:00:00Z&before=2025-10-28T00:00:00Z" \
-H 'Authorization: Bearer <access_token>' \
-H 'User-Agent: <your_app_name>/<version>'Response:
{
"pageSize": 50,
"hasMoreRecords": true,
"nextCursor": "<opaque token>",
"data": [
{
"saleId": "AF34FV",
"orgId": "org123",
"brandId": "br123",
"propertyId": "p789",
"salesChannelId": "sc123",
"salesChannelType": "POS",
"externalId": "your-platform-order-12345",
"source": "POS",
"dispatchType": "DineIn",
"status": "created",
"createdAt": "2025-10-28T14:25:10Z",
"updatedAt": "2025-10-28T14:25:10Z",
"sale": {
"salesChannelId": "sc123",
"source": "POS",
"requestedFulfillmentTime": "2025-10-28T14:30:00Z",
"dispatchType": "DineIn",
"menuId": "123e4567-e89b-12d3-a456-426614174000",
"menuRevisionId": "23",
"items": [ { "menuItemId": "12a85f64-5717-4562-b3fc-2c963f66afb5", "quantity": 1, "unitPrice": 13.99, "modifierItems": [] } ],
"charges": [],
"discounts": [],
"payments": [ { "type": "Sale", "paymentMethod": "Cash", "amount": 13.99, "paidAt": "2025-10-28T14:25:00Z" } ]
}
}
]
}Paginating: when hasMoreRecords is true, pass nextCursor as the cursor query parameter on the next request together with the same filter parameters. Cursors are not portable across different endpoints (org-wide vs. property-scoped) or different filter combinations.
List sales for a property (full payload)
GET /sales/orgs/{orgId}/properties/{propertyId}/salesSame shape and query parameters (pageSize, cursor, after, before) as List sales (org-wide, full payload), scoped to one property.
curl "https://api.flipdish.co/sales/orgs/org123/properties/p789/sales?pageSize=100" \
-H 'Authorization: Bearer <access_token>' \
-H 'User-Agent: <your_app_name>/<version>'
Cursor portabilityCursors are tied to both the endpoint and the filters used when they were issued — a cursor from the org-wide list isn't portable to the property-scoped list (or vice versa), and cursors aren't portable between different
propertyIdfilters.
Response fields (org-wide header)
Every item in data above wraps a SaleHeaderResponse plus a nested sale object (the full Sale object schema):
| Field | Type | Notes |
|---|---|---|
saleId | string | Flipdish sale identifier. |
orgId / brandId / propertyId | string | The org, brand, and property the sale belongs to. |
salesChannelId / salesChannelType | string / enum | The channel the sale was placed on, and its type (POS, KIOSK, UberEats, Deliveroo, etc). |
externalId | string | Partner/platform reference, if supplied at create time. |
source | string | Free-text origin set at create time (e.g. POS, App). |
dispatchType | string | Fulfilment mode: DineIn, TakeAway, Collection, or Delivery. |
status | enum | created | preparedByKitchen | dispatched | onTheWay | delivered | cancelled. |
createdAt / updatedAt | ISO 8601 UTC | When the sale was created, and when it last changed. |
cancelledAt | ISO 8601 UTC | Only present once the sale has been cancelled. |
Get a sale (org-wide)
GET /sales/orgs/{orgId}/sales/{saleId}Returns the full projected sale for the given org and saleId without requiring a salesChannelId. Use this when you have a saleId from a webhook event and don't want to look up which sales channel it belongs to.
curl https://api.flipdish.co/sales/orgs/org123/sales/AF34FV \
-H 'Authorization: Bearer <access_token>' \
-H 'User-Agent: <your_app_name>/<version>'Response — the header fields plus a nested sale object (the full Sale object schema):
{
"saleId": "AF34FV",
"orgId": "org123",
"brandId": "br123",
"propertyId": "p789",
"salesChannelId": "sc123",
"salesChannelType": "POS",
"externalId": "your-platform-order-12345",
"source": "POS",
"dispatchType": "DineIn",
"status": "created",
"createdAt": "2025-10-28T14:25:10Z",
"updatedAt": "2025-10-28T14:25:10Z",
"sale": {
"salesChannelId": "sc123",
"source": "POS",
"requestedFulfillmentTime": "2025-10-28T14:30:00Z",
"desiredAsap": false,
"dispatchType": "DineIn",
"externalId": "your-platform-order-12345",
"displayId": "2A003",
"menuId": "123e4567-e89b-12d3-a456-426614174000",
"menuRevisionId": "23",
"customer": {
"name": "Jane Smith",
"contactPhoneNumber": "+353871234567"
},
"dineIn": { "tableId": "12", "guests": 2 },
"items": [
{ "menuItemId": "12a85f64-5717-4562-b3fc-2c963f66afb5", "quantity": 1, "unitPrice": 13.99, "modifierItems": [] }
],
"charges": [ { "type": "Service", "amount": 1.55 } ],
"discounts": [],
"payments": [
{ "type": "Sale", "paymentMethod": "Credit", "amount": 15.54, "paidAt": "2025-10-28T14:25:00Z" }
]
}
}cancelledAt is omitted here since the sale hasn't been cancelled; it appears (ISO 8601 UTC) once status is cancelled.
Webhook events
Flipdish fires sale-related webhook events for every sale created via this API. There are two event families to choose from — a legacy identifier-only family (sales.*) and a newer full-payload family (sale.*, closed beta). See Sale webhook events (beta) for the full comparison, event schemas, and how to subscribe.
Idempotency and retries
| Concern | How to handle it |
|---|---|
| Duplicate creates | Supply a unique x-idempotency-key (GUID) per intended sale. Retrying with the same key returns the original saleId response. |
| Duplicate webhooks | Dedupe on saleId + event type (or on eventId from X-Flipdish-Idempotency-Key if present). Webhooks are at-least-once. |
| Missed webhooks | Run a periodic reconciler using the org-wide list endpoints. |
| Webhook timeout | Return 2xx within 10 seconds. Persist and process asynchronously. Only 2xx counts — redirects do not. |
| Disabled subscriptions | Subscriptions that fail persistently are auto-disabled. Re-enable via POST /webhooks/orgs/{orgId}/subscriptions/{subId} with { "enabled": true }. |
Pre-launch checklist
- Access token obtained via client-credentials grant and stored securely server-side
-
Authorization: Bearer <access_token>sent on every request -
User-Agentheader set on every request - Per-sale
x-idempotency-key(fresh GUID) sent on everyPOST .../salescall -
externalIdset on every sale for round-trip deduplication -
menuIdandmenuRevisionIdmatch the published revision in use -
dispatchType,delivery, anddineInare consistent (delivery location present whendeliveredByisClient;tableIdandguestspresent forDineIn) - Cancel path tested end-to-end on a sandbox sale
- Delivery status transitions tested (
SALE_DISPATCHED→SALE_DELIVERED) on a sandbox sale - Reconciler runs periodically using the org-wide list endpoint (beta)
- Webhook endpoint verifies HMAC over the raw body using a timing-safe compare
- Webhook endpoint returns
2xxwithin 10 seconds; heavy work is queued - Handlers idempotent on
saleId/eventId - Re-enable logic in place if a subscription is auto-disabled
Further reading
- Getting Started — OAuth2 credential setup
- Sale webhook events (beta) — both event families, full schemas, and how to subscribe
- Subscribe to Flipdish Events (v3) — webhook delivery contract and signature verification
- User Agents — required
User-Agentformat - Build a POS sales analytics integration — full analytics pipeline: onboarding, menu sync, canonical schema mapping, App Store distribution
- Build a marketplace integration — marketplace-specific guide (menu sync, operations updates, snooze handling)
- API Reference (v3.0) — every v3 endpoint and model

