تخطَّ إلى المحتوى
نسخة معاينة للمطورين: قد تتغير الواجهة قبل الإطلاق العام. اطلب الانضمام

Publishing menus

هذا المحتوى غير متوفر بلغتك بعد.

Your POS is where the restaurant manages its menu. Loops takes a copy of it and publishes it to the delivery apps. Read Menus first for the model.

PUT /brands/{id}/menu ──▶ import job ──▶ job.completed (succeeded | failed)
POST /brands/{id}/menu/publish ──▶ publish job ──▶ job.completed (succeeded | partially_failed | failed)

Map your POS menu to the Loops structure:

In your POS In Loops
Menu group, section Category
Item with one price Product with price
Item with sizes Product with variants
Add-ons, choices, extras Modifier group with options, attached to products
{
"categories": [
{ "external_ref": "CAT-BURGERS", "name": { "en": "Burgers", "ar": "برجر" }, "sort_order": 1 }
],
"modifier_groups": [
{
"external_ref": "MG-EXTRAS",
"name": { "en": "Extras", "ar": "إضافات" },
"min_selections": 0,
"max_selections": 3,
"options": [
{ "external_ref": "OPT-CHEESE", "name": { "en": "Extra cheese", "ar": "جبنة إضافية" }, "price": { "amount": 300, "currency": "SAR" } },
{ "external_ref": "OPT-JALAPENO", "name": { "en": "Jalapeño", "ar": "هالبينو" }, "price": { "amount": 200, "currency": "SAR" } }
]
}
],
"products": [
{
"external_ref": "PRD-CHICKEN-BURGER",
"category_external_ref": "CAT-BURGERS",
"name": { "en": "Chicken Burger", "ar": "برجر دجاج" },
"description": { "en": "Crispy chicken, lettuce, house sauce", "ar": "دجاج مقرمش، خس، صوص خاص" },
"image_url": "https://cdn.example.sa/menu/chicken-burger.jpg",
"calories": 540,
"variants": [
{ "external_ref": "VAR-CB-REG", "name": { "en": "Regular", "ar": "عادي" }, "price": { "amount": 2500, "currency": "SAR" } },
{ "external_ref": "VAR-CB-LRG", "name": { "en": "Large", "ar": "كبير" }, "price": { "amount": 3200, "currency": "SAR" } }
],
"modifier_group_external_refs": ["MG-EXTRAS"]
}
]
}

Rules Loops checks on import:

  • Every external_ref is unique within its type, and every reference points to something in the same request.
  • A product has either price or at least one variant, not both.
  • Names have both en and ar.
  • max_selections is at least min_selections, and a modifier group has at least one option.
  • Images download, and are JPG or PNG of at least 800 × 800 px.
  • The request is at most 5 MB.
PUT /v1/brands/brd_9Lk3Vb/menu
{ "id": "job_01J9R2K7TN", "type": "menu_import", "status": "pending", "created_at": "2026-10-06T10:00:00Z" }
  • An import replaces the whole menu. Anything you leave out is removed. Always send the complete menu.
  • It is all or nothing. If anything is wrong, nothing changes, and the job’s errors list every problem:
{
"id": "job_01J9R2K7TN",
"type": "menu_import",
"status": "failed",
"errors": [
{ "external_ref": "PRD-FRIES", "field": "price", "code": "price_or_variants_required", "message": "Give a price or at least one variant." },
{ "external_ref": "PRD-CHICKEN-BURGER", "field": "image_url", "code": "image_unreachable", "message": "The image URL returned 404." }
]
}

Wait for job.completed, or check GET /v1/jobs/{job_id}. Importing doesn’t change the delivery apps.

POST /v1/brands/brd_9Lk3Vb/menu/publish
{ "location_ids": ["loc_2Xf9K"] }

Leave out location_ids to publish to every location of the brand. Loops sends the menu to each active channel connection, and the job reports one result per location and delivery app:

{
"id": "job_01J9R3B1XQ",
"type": "menu_publish",
"status": "partially_failed",
"results": [
{ "location_id": "loc_2Xf9K", "channel_connection_id": "chc_5Rm1Pa", "channel": "delivery_app_a", "status": "succeeded" },
{
"location_id": "loc_2Xf9K", "channel_connection_id": "chc_7Tq3Mn", "channel": "delivery_app_b", "status": "failed",
"errors": [{ "external_ref": "PRD-CHICKEN-BURGER", "code": "channel_rejected_item", "message": "Description is too long for this delivery app." }]
}
]
}
  • Delivery apps review menus on their own schedule. A publish can take minutes, and results arrive as each app answers.
  • partially_failed means some delivery apps rejected some items. The rest were applied. Fix the items, import, and publish again.
  • Locations without an active channel connection are skipped; publish again after they go live.

When the restaurant changes its menu in your POS, import and publish again. That includes:

  • prices;

  • new and removed items;

  • items that are temporarily unavailable (available: false).

  • Debounce changes. Publish once after a burst of edits, not after each keystroke.

  • Don’t publish more than once a minute per brand. Delivery apps limit how often a menu can change.

  • To check what Loops has, GET /v1/brands/{brand_id}/menu returns the current menu and its version.