Sales Management API

How to create, cancel, and query sales using the v3.0 Sales Management API — including the new org-wide query endpoints

🚧

Beta

The 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 salesChannelId needed. 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

ConceptFormatNotes
Orgorg123Top-level tenant. Every Sales Management path is scoped to an orgId.
Brandbr123 — legacy: fd123, or a plain slug (e.g. mexicancafe)The distinct identity (name, logo) associated with one or more sales channels.
Propertyp123A physical location.
Sales channelsc123 — 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.
SalesaleId (e.g. AF34FV, Crockford base32) + optional externalIdA 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 revisionUUID + numeric revisionIdEvery sale references the exact menu revision it was placed against.
📘

Don't parse or validate ID shapes

saleId, brandId, and salesChannelId are 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}/sales

Records 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

FieldRequiredNotes
salesChannelIdYesThe channel this sale belongs to.
requestedFulfillmentTimeYesISO 8601 UTC. When the restaurant hands over the food.
dispatchTypeYesDineIn | TakeAway | Collection | Delivery. Note capital A in TakeAway.
menuId / menuRevisionIdYesMust 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)typeDelivery, Service, Tip, Other. Optional itemId for line-level charges.
discounts[]Yes (can be empty)typeVoucher, Loyalty, Spot, Other. code required when type is Voucher or Loyalty.
payments[]Yes (can be empty — implies unpaid)typeSale, Refund. paymentMethodCash, Credit, Online, PhonePayment, ExternalPayment.
externalIdRecommendedYour idempotency key on your own system. Round-tripped on every sale event.
displayIdNoCustomer- and staff-facing short ID (max 15 chars).
sourceNoFree text. E.g. POS, App, ChatGPT.
desiredAsapNoIf true but requestedFulfillmentTime is more than ~1 hour out, it is overridden to false.
customerNoUse contactPhoneNumber (E.164 format, e.g. +353871234567). The phoneNumber field is deprecated.
deliveryConditionalRequired when dispatchType is Delivery. Set deliveredBy to Client (restaurant-managed) or External (third-party driver). Include location when deliveredBy is Client.
dineInConditionalRequired when dispatchType is DineIn. Supply tableId (string) and guests (int).
notesNoFree-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.
metadataNoFree-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 GET sale endpoint (the deprecated channel-scoped GET endpoints return the same shape).
  • The sale field embedded in sale.created.v1 / sale.updated.v1 webhook 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

Optional. Identifies the end customer placing the sale.

FieldTypeNotes
idUUIDFlipdish customer ID. If provided, must reference an existing customer — omit or leave empty if unknown.
externalIdstringYour platform's own customer identifier.
namestringCustomer's full name.
contactPhoneNumberstringE.164 format with a leading + (e.g. +353871234567). The legacy phoneNumber field (no +, no formatting) is deprecated — use contactPhoneNumber for new integrations.
contactMaskingCodestringNumeric 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.
emailAddressstringCustomer email address.

delivery

Required when dispatchType is Delivery.

FieldTypeNotes
deliveredByenumClient — the restaurant fulfils the delivery itself. External — a marketplace or third-party courier delivers.
locationobjectThe drop-off address. Required when deliveredBy is Client — not needed for External, since the courier's own platform handles routing. See delivery.location below.
notesstringFree-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

FieldTypeNotes
addressFields.line1stringRequired. First address line.
addressFields.line2 / addressFields.line3stringOptional further address lines.
addressFields.postCodestringPostal code.
countryCodestringRequired. ISO 3166-1 alpha-2 country code (e.g. IE).
coordinates.latitude / coordinates.longitudenumberOptional geographic coordinates for the drop-off point.

dineIn

Required when dispatchType is DineIn.

FieldTypeNotes
tableIdstringTable identifier (free text).
guestsintegerNumber of guests, >= 0.

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.

FieldTypeNotes
menuItemIdUUIDMust match an item in the menu/revision referenced by the sale's menuId/menuRevisionId.
quantityinteger> 0.
unitPricenumberPrice of one unit of this item (or modifier).
notesstringFree-text instruction for this line, e.g. "No onions please".
modifierItems[]arrayModifiers applied to this item — same schema as items[], so entries can themselves carry modifierItems[].

charges[]

Required on create, but the array can be empty. Additional monetary lines beyond item prices.

FieldTypeNotes
typeenumDelivery, Service, Tip, Other.
amountnumber>= 0.
itemIdUUIDOptional. 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[]

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:

typecodeNotes
VoucherRequiredThe voucher code the customer redeemed.
LoyaltyRequiredThe loyalty program code.
SpotOptionalAn ad-hoc/manual discount applied in the moment; code is just a label if supplied.
OtherOptionalCatch-all category; code is a free-text label if supplied.

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.

FieldTypeNotes
typeenumSale (money collected) or Refund (money returned to the customer).
paymentMethodenumCash, Credit, Online, PhonePayment, ExternalPayment.
amountnumber>= 0.
paidAtISO 8601 UTCRequired. When the payment was made.
description1stringFree-text payment detail, e.g. Visa ****4921.
description2stringFree-text secondary detail, e.g. an authorization code or reference number.

Cancelling a sale

POST /salesManagement/orgs/{orgId}/sales/{saleId}/cancel

Marks 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}/deliveryStatus

Records 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)

🚧

Deprecated

The 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 a salesChannelId.

  • 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)

🚧

Beta

These 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}/sales

Returns paginated full sale payloads across all channels of the org, sorted by creation time descending. All query parameters are optional and combinable.

ParameterTypeNotes
pageSize50–200Number of records per page. Default 50.
cursorstringOpaque pagination token from the previous response's nextCursor.
after / beforeISO 8601 date-timeBound 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}/sales

Same 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 portability

Cursors 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 propertyId filters.

Response fields (org-wide header)

Every item in data above wraps a SaleHeaderResponse plus a nested sale object (the full Sale object schema):

FieldTypeNotes
saleIdstringFlipdish sale identifier.
orgId / brandId / propertyIdstringThe org, brand, and property the sale belongs to.
salesChannelId / salesChannelTypestring / enumThe channel the sale was placed on, and its type (POS, KIOSK, UberEats, Deliveroo, etc).
externalIdstringPartner/platform reference, if supplied at create time.
sourcestringFree-text origin set at create time (e.g. POS, App).
dispatchTypestringFulfilment mode: DineIn, TakeAway, Collection, or Delivery.
statusenumcreated | preparedByKitchen | dispatched | onTheWay | delivered | cancelled.
createdAt / updatedAtISO 8601 UTCWhen the sale was created, and when it last changed.
cancelledAtISO 8601 UTCOnly 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

ConcernHow to handle it
Duplicate createsSupply a unique x-idempotency-key (GUID) per intended sale. Retrying with the same key returns the original saleId response.
Duplicate webhooksDedupe on saleId + event type (or on eventId from X-Flipdish-Idempotency-Key if present). Webhooks are at-least-once.
Missed webhooksRun a periodic reconciler using the org-wide list endpoints.
Webhook timeoutReturn 2xx within 10 seconds. Persist and process asynchronously. Only 2xx counts — redirects do not.
Disabled subscriptionsSubscriptions 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-Agent header set on every request
  • Per-sale x-idempotency-key (fresh GUID) sent on every POST .../sales call
  • externalId set on every sale for round-trip deduplication
  • menuId and menuRevisionId match the published revision in use
  • dispatchType, delivery, and dineIn are consistent (delivery location present when deliveredBy is Client; tableId and guests present for DineIn)
  • Cancel path tested end-to-end on a sandbox sale
  • Delivery status transitions tested (SALE_DISPATCHEDSALE_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 2xx within 10 seconds; heavy work is queued
  • Handlers idempotent on saleId / eventId
  • Re-enable logic in place if a subscription is auto-disabled

Further reading