Build a marketplace integration

How to integrate a third-party marketplace (delivery aggregator, ordering platform) with Flipdish using the v3.0 public API and webhooks

Introduction

This guide is for third-party marketplaces — delivery aggregators, ordering platforms, virtual brand operators — who want to push sales into Flipdish on behalf of merchants, keep their menu in sync with the merchant's source of truth, and report availability and fulfilment status back.

You will work entirely against Flipdish's v3.0 public surface area:

📘

Audience

Engineers at a marketplace platform building an integration that links Flipdish merchants to their delivery / ordering surface. Familiarity with HTTP, JSON, and HMAC signatures is assumed.

🚧

Beta

Several v3.0 endpoints used here (Sales API, Operations Updates, v3 webhooks) are currently in closed beta. Email [email protected] to request access and credentials.

Concept model

ConceptFormatNotes
Orgorg123Top-level tenant — a chain, franchise, or independent business.
Brandbr123A trading brand within an org.
Propertyp123A physical location (a venue / store).
Sales channelsc123 with salesChannelType (e.g. ExternalApp)Where orders flow through. Your marketplace will be represented as a sales channel on each property that opts in.
MenuUUID, with numeric revisionIdOwned by an org and published to one or more sales channels.
SalesaleId (e.g. AF34FV) plus an optional externalIdYour reference to the order on your platform.

Webhook event types are versioned with a .v1 suffix (e.g. sales.created.v1).

Prerequisites

Before writing any code:

  1. Request API access by emailing [email protected]. You will receive an API key and an orgId to test against.
  2. Read The Flipdish Order Flow to understand the sale lifecycle.
  3. Read Subscribe to Flipdish Events (v3) for the webhook delivery contract, headers, and signature verification.
  4. Pick a User-Agent and follow User Agents.

Authentication

All v3.0 endpoints use an API key passed in the FD-Authorization header:

curl https://api.flipdish.co/menuManagement/orgs/org123/menus/headers \
  -H 'FD-Authorization: <your_api_key>' \
  -H 'User-Agent: <your_app_name>/<version>'

The Webhook Service API uses the Authorization header instead — see its reference page for details.

Rules of thumb:

  • Always use HTTPS — plain HTTP is rejected.
  • Treat the API key like a password: server-side only, never in mobile apps or browsers.
  • Rotate keys via Flipdish if they leak.

Architecture overview

A typical marketplace integration is built around three loops, all driven by v3.0 endpoints and webhooks:

LoopTriggerDirection
OnboardingMerchant enables your sales channelMerchant → Flipdish portal → your platform
Menu syncmenu.published.v1 webhookFlipdish → your platform
Sale lifecycleCustomer orders on your marketplaceYour platform ↔ Flipdish

End-to-end:

Merchant adds your marketplace as a sales channel in the Flipdish portal
       │
       ▼
Flipdish ──menu.published.v1 webhook──▶ your platform
       │
       ▼
Your platform GETs the menu revision from Menu Management API,
transforms it, and publishes to your marketplace listing
       │
       ▼
Your platform reports back via Operations Updates API:
  POST .../menu-publish-status

Customer orders on your marketplace
       │
       ▼
Your platform ──POST /sales/orgs/{orgId}/sales──▶ Flipdish
       │
       ▼
Flipdish ──sales.created.v1 / sales.pos.status.updated.v1
        / sales.delivery.status.updated.v1 / sales.cancelled.v1──▶ your platform
       │
       ▼
Update marketplace order, dispatch driver, etc.

Three patterns are non-negotiable, regardless of your stack:

  • Acknowledge webhooks fast. Verify the signature, persist the event, return 2xx. Do real work asynchronously. Flipdish times out after 10 seconds and retries.
  • Be idempotent on eventId (and on saleId for sale events). Webhooks are at-least-once.
  • Use externalId on sales to dedupe replays from your own system — it is your reference and is round-tripped on every sale event.

Onboarding a merchant

A merchant who lives on Flipdish opts into your marketplace from the Flipdish portal, which surfaces your integration as a sales channel on the chosen properties. Once that is done, your platform discovers what to work with using the Org Management API:

  • GET /orgManagement/orgs — list orgs your key has access to
  • GET /orgManagement/orgs/{orgId}/properties — list properties (locations) under an org
  • GET /orgManagement/orgs/{orgId}/properties/{propertyId}/salesChannels — list the sales channels on a property; pick the ones for your salesChannelType

Persist the mapping per linked store:

{
  "orgId":          "org123",
  "brandId":        "br123",
  "propertyId":     "p789",
  "salesChannelId": "sc456",
  "marketplaceStoreId": "your-platform-store-id"
}

Syncing menus

The Flipdish menu is the source of truth. Pull on demand and listen for changes — never store and mutate independently.

Listen for menu.published.v1

When a menu is (re)published to a sales channel that belongs to your marketplace, Flipdish fires menu.published.v1. The payload includes the full published snapshot so you usually don't need a follow-up GET:

{
  "orgId": "org123",
  "brandId": "br456",
  "propertyId": "p789",
  "salesChannelId": "sc123",
  "salesChannelType": "ExternalApp",
  "menuId": "123e4567-e89b-12d3-a456-426614174000",
  "menuRevisionId": 1,
  "menuPublishId": "223e4567-e89b-12d3-a456-426614174000",
  "menu": { /* categories, items, modifiers, pricingProfiles, charges, … */ }
}

Subscribe via the Webhook Service API:

curl -X POST https://api.flipdish.co/webhooks/orgs/org123/subscriptions \
  -H 'Authorization: <your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "orgId": "org123",
    "callbackUrl": "https://your-platform.example.com/flipdish/webhooks",
    "eventTypes": ["menu.published.v1", "menu.validation.failed.v1"],
    "secret": "<a strong random string you generate>",
    "propertyIds": ["p789"]
  }'

The secret is the value Flipdish will use to sign deliveries. Store it alongside the subscription.

Pull on demand

If you need to fetch a menu directly (initial import, recovery, audit):

  • GET /menuManagement/orgs/{orgId}/menus/headers — list menus (lightweight)
  • GET /menuManagement/orgs/{orgId}/menus/{menuId}/current — the current published revision
  • GET /menuManagement/orgs/{orgId}/menus/{menuId}/revisions/{revisionId} — a specific revision

The full menu endpoint is heavier than the headers endpoint — only call it when you actually need the full tree.

What to map

Flipdish conceptTypical marketplace concept
categoryCategory
category.items[]Product / item
modifier (group)Modifier group
modifier.items[]Modifier option
pricingProfiles[]Per-dispatch-type pricing (collectionPrice, deliveryPrice, dineInPrice, takeawayPrice)
charges[]Service / delivery / tip charges
availabilityOverrides[]Day-and-time availability rules

Use productId and externalId on items to round-trip your marketplace IDs.

Confirming the menu was applied

After you have processed the menu and pushed it to your marketplace, report success or failure back to Flipdish so the merchant sees the publish status in the portal:

curl -X POST \
  "https://api.flipdish.co/operations-updates/orgs/org123/properties/p789/salesChannels/sc123/menu-publish-status" \
  -H 'FD-Authorization: <your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "menuPublishId": "223e4567-e89b-12d3-a456-426614174000",
    "success": true
  }'

On failure, set success to false and include errorMessage.

Pushing sales to Flipdish

When a customer orders on your marketplace, create a sale on Flipdish via the Sales API:

curl -X POST https://api.flipdish.co/sales/orgs/org123/sales \
  -H 'FD-Authorization: <your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "salesChannelId": "sc123",
    "source": "YourMarketplace",
    "externalId": "your-platform-order-12345",
    "requestedFulfillmentTime": "2025-10-28T14:30:00Z",
    "desiredAsap": true,
    "dispatchType": "Delivery",
    "menuId": "123e4567-e89b-12d3-a456-426614174000",
    "menuRevisionId": "1",
    "customer": {
      "phoneNumber": "+353871234567",
      "name": "John Smith",
      "emailAddress": "[email protected]",
      "externalId": "customer_123"
    },
    "delivery": {
      "deliveredBy": "External",
      "notes": "Leave at door",
      "location": {
        "countryCode": "IE",
        "addressFields": { "line1": "38 Pearse Street", "line2": "Dublin", "postCode": "D02 XX00" },
        "coordinates": { "latitude": 53.343, "longitude": -6.255 }
      }
    },
    "items": [
      {
        "menuItemId": "12a85f64-5717-4562-b3fc-2c963f66afb5",
        "quantity": 1,
        "unitPrice": 13.99,
        "modifierItems": [
          { "menuItemId": "10a85f64-5717-4562-b3fc-2c963f66afb3", "quantity": 1, "unitPrice": 1.50 }
        ]
      }
    ],
    "charges": [ { "type": "Delivery", "amount": 2.50 } ],
    "discounts": [],
    "payments": [
      {
        "type": "Sale",
        "paymentMethod": "Online",
        "amount": 18.49,
        "paidAt": "2025-10-28T14:25:00Z",
        "description1": "Visa ****4921"
      }
    ]
  }'

The response gives you Flipdish's saleId, plus createdAt and dispatchTime.

Field rules of thumb:

  • salesChannelId — the sales channel your marketplace is registered as on this property.
  • menuId / menuRevisionId — must match the revision you consumed via menu.published.v1 (or pulled via Menu Management).
  • externalId — your reference. Flipdish includes it on every related sale webhook.
  • dispatchTypeDineIn, TakeAway, Collection, or Delivery.
  • payments[] — list how the customer paid. Sum of payments should equal the total of items + charges − discounts.
  • requestedFulfillmentTime — ISO-8601 UTC. If desiredAsap is true and the time is more than ~1 hour out, Flipdish will treat it as scheduled.

Subscribing to sale events

Once a sale is in Flipdish, you keep your marketplace in sync by subscribing to the sale lifecycle events:

EventMeaning
sales.created.v1A sale was created (mirror of your push, useful for audit / replay)
sales.pos.status.updated.v1Kitchen / POS state changed (e.g. SALE_PREPARED_BY_KITCHEN)
sales.delivery.status.updated.v1Delivery state changed (e.g. SALE_DISPATCHED), with dispatchTime
sales.cancelled.v1Sale was cancelled, with cancellationReason and cancellationNotes

Example of sales.pos.status.updated.v1:

{
  "source": "rms",
  "detail-type": "sales.pos.status.updated.v1",
  "detail": {
    "properties": {
      "orgId": "org123",
      "brandId": "br123",
      "propertyId": "p123",
      "salesChannelId": "sc123",
      "salesChannelType": "ExternalApp",
      "saleId": "sale-123",
      "externalId": "your-platform-order-12345",
      "status": "SALE_PREPARED_BY_KITCHEN"
    },
    "metadata": { "actor": { "id": "user-123", "email": "[email protected]", "name": "Jane" } }
  }
}

Add these to your subscription:

{
  "eventTypes": [
    "menu.published.v1",
    "menu.validation.failed.v1",
    "menu.item.snoozed.v1",
    "menu.item.unsnoozed.v1",
    "sales_channel.snoozed.v1",
    "sales_channel.unsnoozed.v1",
    "sales.created.v1",
    "sales.cancelled.v1",
    "sales.pos.status.updated.v1",
    "sales.delivery.status.updated.v1"
  ]
}

Item and store availability

Two directions:

Receiving snooze events from Flipdish

When a merchant marks an item or sales channel as unavailable on the Flipdish portal:

  • menu.item.snoozed.v1 / menu.item.unsnoozed.v1 — single menu item, with menuId, menuRevisionId, menuItemId, optional expiry.
  • sales_channel.snoozed.v1 / sales_channel.unsnoozed.v1 — entire sales channel (your marketplace listing for that property), with optional expiry.

Mirror these onto your marketplace listing.

Reporting snooze status back

If a snooze on your marketplace originates from your side (e.g. driver reports an item out), report it via the Operations Updates API:

# Snooze a single menu item on this sales channel
curl -X POST \
  "https://api.flipdish.co/operations-updates/orgs/org123/properties/p789/salesChannels/sc123/item-snooze-status" \
  -H 'FD-Authorization: <your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "menuId": "123e4567-e89b-12d3-a456-426614174000",
    "menuRevisionId": "1",
    "menuItemId": "12a85f64-5717-4562-b3fc-2c963f66afb5",
    "requestedStatus": "Snooze",
    "success": true
  }'

# Snooze the entire sales channel
curl -X POST \
  "https://api.flipdish.co/operations-updates/orgs/org123/properties/p789/salesChannels/sc123/store-snooze-status" \
  -H 'FD-Authorization: <your_api_key>' \
  -H 'Content-Type: application/json' \
  -d '{ "requestedStatus": "Snooze", "success": true }'

requestedStatus is Snooze or UnSnooze.

Verifying webhooks

Every v3 webhook delivery includes:

X-Flipdish-Event-Type: sales.created.v1
X-Flipdish-Event-Version: v1
X-Flipdish-Timestamp: 2025-08-14T12:34:56.789Z
X-Flipdish-Signature: t=2025-08-14T12:34:56.789Z,sha256=<hex>

Verification:

  1. Read the raw request body — do not parse JSON before verifying.
  2. Compute HMAC-SHA256( secret, "${timestamp}.${rawBody}" ).
  3. Compare the result to the sha256= value using a constant-time comparison.
  4. Reject with 401 on mismatch.
import crypto from 'node:crypto';

export function verifySignature(
  rawBody: string,
  timestamp: string,
  signature: string,
  secret: string,
): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`, 'utf8')
    .digest('hex');
  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(signature, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

The secret is the one you supplied when creating the subscription via the Webhook Service API.

Idempotency, retries and failure handling

  • At-least-once delivery. Flipdish retries failed webhook deliveries. Dedupe on the eventId from X-Flipdish-Idempotency-Key (when present), or on saleId + event type for sale events.
  • 10-second budget. If your handler can't finish in 10 seconds, persist the event and process it asynchronously.
  • externalId on the way in. Use it as your idempotency key on POST /sales/orgs/{orgId}/sales retries.
  • Auto-disable. Subscriptions that fail persistently are disabled. Re-create them via the Webhook Service API, or update via POST /webhooks/orgs/{orgId}/subscriptions/{subId} with enabled: true.
  • 3xx is not success. Only 2xx counts.

Testing

The Webhook Service API includes a synthetic test trigger:

curl https://api.flipdish.co/webhooks/orgs/org123/subscriptions/sub_<uuid>/trigger \
  -H 'Authorization: <your_api_key>'

This sends a synthetic delivery for every event type on the subscription, so you can validate signature verification and your routing without waiting for real activity.

For sale-side testing, push a real sale via POST /sales/orgs/{orgId}/sales against a staging org and confirm you receive sales.created.v1 followed by sales.pos.status.updated.v1 as the merchant's POS / kitchen progresses.

Pre-launch checklist

  • API key stored securely server-side; sent as FD-Authorization (or Authorization on the Webhook Service API)
  • User-Agent set on every request
  • Per-merchant mapping persisted: orgId, propertyId, salesChannelId, your store id
  • Subscribed to menu.published.v1 (and menu.validation.failed.v1) and applying menu changes from the payload
  • Menu publish status reported back via Operations Updates API on every applied / failed publish
  • Subscribed to sales.created.v1, sales.cancelled.v1, sales.pos.status.updated.v1, sales.delivery.status.updated.v1
  • Subscribed to menu.item.snoozed.v1 / unsnoozed.v1 and sales_channel.snoozed.v1 / unsnoozed.v1
  • POST /sales/orgs/{orgId}/sales integrated with correct salesChannelId, menuId, menuRevisionId, dispatchType, and payments
  • externalId set on every sale and used as your idempotency key
  • 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
  • Trigger-test (/subscriptions/{subId}/trigger) run end-to-end on staging
  • Re-enable / monitor logic in place if a subscription is auto-disabled

Further reading