Cleanup menu

http
POST /v1/cleanup

Removes synced items that are not in the lists you send — the inverse of sync. For each array you provide, you pass the externalIds to keep; everything else that was synced from your system is deactivated or deleted:

Array providedWhat happens to items NOT in the list
productExternalIdsProducts are deactivated (menuVisible: false) — recoverable by re-syncing
categoryExternalIdsCategories are deleted; their products are kept and unlinked (no category)
ingredientExternalIdsIngredients are deleted, including from product compositions and modifier options

Two important scoping rules:

  • Only items that have an externalId (i.e. came through the API) are affected. Items created manually in the DuckHub app are never touched.
  • Arrays you omit are left completely alone — send only the entity types you want to clean.

Sending no arrays at all is a no-op (success: true with a warning).

Deletes are permanent; empty lists need force

Category and ingredient deletion cannot be undone. An empty array means "keep nothing" — it would wipe every synced item of that type, so the API rejects it with 400 unless you also send force: true. Deactivated products are the one soft case: re-sync them to restore.

Request body#

FieldTypeRequiredMeaning
productExternalIdsstring[]NoProducts to keep visible
categoryExternalIdsstring[]NoCategories to keep
ingredientExternalIdsstring[]NoIngredients to keep
forcebooleanNoDefault false; must be true when any provided array is empty

Dry run — preview before you destroy#

POST /v1/cleanup?dryRun=true runs the same validation and returns the counts and refs of everything that WOULD be hidden/deleted — with zero writes (same contract as sync's dry run):

json
{
  "success": true,
  "dryRun": true,
  "deactivatedProducts": 3,
  "deletedCategories": 1,
  "deletedIngredients": 0,
  "warnings": [],
  "preview": {
    "products": ["prod-old-1", "prod-old-2", "prod-old-3"],
    "categories": ["cat-winter"],
    "ingredients": []
  }
}

Preview ref lists are capped at 100 per section (a warning notes the truncation). Recommended flow: always dry-run first, check the preview matches your intent, then repeat the exact call without dryRun. Category/ingredient deletions are permanent.

Example#

Typical full-sync flow — after pushing the current catalog with sync, clean up anything your POS no longer has, then publish:

bash
curl -X POST https://api.duck-hub.com/v1/cleanup \
  -H "Authorization: Bearer dk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "productExternalIds": ["prod-margherita", "prod-pepperoni"],
    "categoryExternalIds": ["cat-pizza"],
    "ingredientExternalIds": ["ing-mozzarella", "ing-olives"]
  }'

Response#

201 Created:

json
{
  "success": true,
  "deactivatedProducts": 3,
  "deletedCategories": 1,
  "deletedIngredients": 2,
  "warnings": []
}
FieldMeaning
deactivatedProductsSynced products hidden from the menu (menuVisible: false)
deletedCategoriesSynced categories removed (products unlinked first)
deletedIngredientsSynced ingredients removed everywhere
warningsNon-fatal notes

Cleanup changes the draft menu — run POST /v1/publish afterwards to update what guests see.

Deleting a product (frees plan capacity)#

Deactivation only hides a product — it still counts toward your plan limit. To actually free capacity, delete the product:

http
DELETE /v1/products/:ref

:ref is your externalId or the opaque id. The product is soft-deleted (recoverable only by the DuckHub team), disappears from GET /v1/menu, and stops counting toward maxProducts. Repeating the call returns 404 PRODUCT_NOT_FOUND (the ref no longer resolves).

json
{
  "success": true,
  "id": "cln8y2k...",
  "externalId": "prod-margherita",
  "deletedAt": "2026-07-05T12:00:00.000Z"
}

Rule of thumb: hide (cleanup) items that may come back — seasonal dishes, out-of-stock; delete items that are gone for good and are taking up plan capacity.

Errors#

  • 404 PRODUCT_NOT_FOUND — unknown/already-deleted :ref on DELETE

  • 400 Passing an empty array would affect ALL items. Set force: true to confirm this action. — an empty array without force

  • 401 — see Authentication