Merchandising
Promotions, banners and QR tables as API resources. Available on every plan with an active subscription; plan limits apply where noted.
Hybrid identity (:ref)#
Every resource carries an immutable opaque id and an optional
caller-defined externalId (set it on create). Wherever a path or body
takes a reference — :ref, productRef, linkedProductRef — you may
pass either value; externalId is matched first. Creating with an
externalId that already exists on that resource type →
409 EXTERNAL_ID_ALREADY_EXISTS. Unknown refs → 404 with
PROMOTION_NOT_FOUND / BANNER_NOT_FOUND / TABLE_NOT_FOUND /
PRODUCT_NOT_FOUND.
All list endpoints use the same envelope as
orders: { items, total, page, limit, totalPages }
(page ≥1, limit 1–100).
Promotions#
GET /v1/promotions
POST /v1/promotions
PATCH /v1/promotions/:ref
DELETE /v1/promotions/:ref
POST /v1/promotions/:ref/items
PATCH /v1/promotions/:ref/items/:productRef
DELETE /v1/promotions/:ref/items/:productRefA promotion is a named group of products with sale prices, toggled by
isActive. Create/update fields: name (≤100), schedule (same
day/HH:MM shape as settings hours), isActive
(update only), externalId (create only).
Items are addressed by (promotion, product) — no separate item ids.
Adding an item: { "productRef": "...", "salePriceMinor": 9900 } (minor
units). While a promotion is active, its items' sale prices are applied
to the products automatically (and cleared on deactivate/delete, unless
another active promotion still covers the product). A product can appear
in a promotion once — re-adding it is a 400.
curl -X POST https://api.duck-hub.com/v1/promotions \
-H "Authorization: Bearer dk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "name": "Happy hour", "externalId": "promo-happy-hour" }'
curl -X POST https://api.duck-hub.com/v1/promotions/promo-happy-hour/items \
-H "Authorization: Bearer dk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "productRef": "prod-margherita", "salePriceMinor": 15000 }'const HEADERS = {
Authorization: 'Bearer dk_live_your_api_key',
'Content-Type': 'application/json',
}
await fetch('https://api.duck-hub.com/v1/promotions', {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ name: 'Happy hour', externalId: 'promo-happy-hour' }),
})
await fetch(
'https://api.duck-hub.com/v1/promotions/promo-happy-hour/items',
{
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ productRef: 'prod-margherita', salePriceMinor: 15000 }),
},
)
// activate
await fetch('https://api.duck-hub.com/v1/promotions/promo-happy-hour', {
method: 'PATCH',
headers: HEADERS,
body: JSON.stringify({ isActive: true }),
})Banners#
GET /v1/banners
POST /v1/banners
PATCH /v1/banners/:ref
DELETE /v1/banners/:ref
POST /v1/banners/:ref/imageFields: title (≤60), description (≤2000), isActive (update),
linkedProductRef (opens that product when the banner is tapped;
null clears the link), externalId (create only). Attach the image via
the media ingest, and translate title/description via
PUT /v1/banners/:ref/translations. Banners appear
on the public menu after the next publish.
Creating past your plan's banner cap → 400 BANNER_LIMIT_REACHED
(see your cap in GET /v1/reference).
Tables & QR#
GET /v1/tables
POST /v1/tables
PATCH /v1/tables/:ref
DELETE /v1/tables/:refCreate with { "name": "Table 1", "externalId": "table-1" } — the table
gets a public id and a short link automatically; responses include
qrUrl (the short URL your printed QR codes should encode) plus the
shortLink details. Update fields: name, isActive, capacity,
positionX/positionY.
Creating past your plan's table cap → 400 TABLE_LIMIT_REACHED.
curl -X POST https://api.duck-hub.com/v1/tables \
-H "Authorization: Bearer dk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "name": "Table 1", "externalId": "table-1" }'const response = await fetch('https://api.duck-hub.com/v1/tables', {
method: 'POST',
headers: {
Authorization: 'Bearer dk_live_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'Table 1', externalId: 'table-1' }),
})
const { qrUrl } = await response.json() // encode this in the printed QRErrors#
404PROMOTION_NOT_FOUND/BANNER_NOT_FOUND/TABLE_NOT_FOUND/PRODUCT_NOT_FOUND/PROMOTION_ITEM_NOT_FOUND409EXTERNAL_ID_ALREADY_EXISTS— duplicateexternalIdon create409DUPLICATE_PROMOTION_ITEM— the product is already in that promotion400BANNER_LIMIT_REACHED/TABLE_LIMIT_REACHED— plan caps400 Validation failed— schema violations