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#

http
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/:productRef

A 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.

bash
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 }'

Banners#

http
GET    /v1/banners
POST   /v1/banners
PATCH  /v1/banners/:ref
DELETE /v1/banners/:ref
POST   /v1/banners/:ref/image

Fields: 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#

http
GET    /v1/tables
POST   /v1/tables
PATCH  /v1/tables/:ref
DELETE /v1/tables/:ref

Create 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.

bash
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" }'

Errors#

  • 404 PROMOTION_NOT_FOUND / BANNER_NOT_FOUND / TABLE_NOT_FOUND / PRODUCT_NOT_FOUND / PROMOTION_ITEM_NOT_FOUND
  • 409 EXTERNAL_ID_ALREADY_EXISTS — duplicate externalId on create
  • 409 DUPLICATE_PROMOTION_ITEM — the product is already in that promotion
  • 400 BANNER_LIMIT_REACHED / TABLE_LIMIT_REACHED — plan caps
  • 400 Validation failed — schema violations