Skip to content
Developer preview: the API may change before general availability. Request access

Onboarding merchants

This guide takes a restaurant from your system to Loops: the merchant, its business profile, its brands and its locations. Menus are covered in Publishing menus.

Create merchant ──▶ Business profile ──▶ Submit ──▶ Loops reviews ──▶ approved
│ details + documents │
│ └─▶ rejected: fix, submit again
└──▶ Brands and locations (any time, in parallel)
approved ──▶ Loops connects locations to delivery apps ──▶ channel_connection.status_changed (active)
POST /v1/merchants
Idempotency-Key: 6f1c2a9e-3b7d-4c55-9a40-2d8e1f7b9c31
{
"external_ref": "CUST-10021",
"name": { "en": "Authentic Taste", "ar": "المذاق الأصيل" },
"owner": { "name": "Sara Al-Harbi", "email": "sara@example.sa", "phone": "+966501234567" }
}
  • external_ref is your ID for this customer. It must be unique among your merchants.
  • Loops emails the owner an invitation to sign in to Loops. owner.invitation shows sent until they accept.
  • The merchant is active straight away, and connected to your application. Send its ID as Loops-Merchant-Id on every later call.

If the owner’s email or phone already belongs to a Loops account, you get 409 merchant_exists. Don’t create a second account: the restaurant already uses Loops, and connects to you instead (see Merchants).

The business profile is the only thing Loops reviews. Delivery apps require these details before a restaurant can sell.

Details:

PUT /v1/merchant/business-profile
{
"legal_name": "شركة المذاق الأصيل للوجبات السريعة",
"cr_number": "1010123456",
"vat_number": "310123456700003",
"national_address": {
"building_number": "7421",
"street": "Prince Turki Street",
"district": "Al Olaya",
"city": "Riyadh",
"postal_code": "12214",
"additional_number": "3156"
}
}
  • legal_name exactly as on the commercial registration.
  • cr_number has 10 digits. vat_number has 15 digits and starts and ends with 3.

Documents: upload each type listed in required_documents (from GET /v1/merchant/business-profile), one request per file:

Terminal window
curl -X POST https://sandbox.partners.loops.sa/v1/merchant/business-profile/documents \
-H "Authorization: Bearer $TOKEN" \
-H "Loops-Merchant-Id: mer_7Hq2Lw8Z" \
-H "Idempotency-Key: $(uuidgen)" \
-F type=commercial_registration \
-F file=@cr-certificate.pdf
type Document
commercial_registration Commercial registration certificate
vat_certificate VAT registration certificate
national_address_certificate National address certificate
iban_certificate Bank IBAN certificate (letter from the bank)
owner_id Owner’s national ID or iqama

Files are PDF, JPG or PNG, up to 4 MB. Uploading the same type again replaces the earlier file.

POST /v1/merchant/business-profile/submit

If anything is missing, you get 400 invalid_request listing each missing field or document. Otherwise the status becomes in_review, and the profile can’t be edited until Loops decides.

Status Meaning What you can do
draft Being filled in Edit details, upload documents, submit
in_review Loops is reviewing it Wait for the outcome
approved Approved Nothing. Locations can go live. To change an approved profile, contact Loops.
rejected Something needs fixing Read review.notes and each document’s rejection_reason, fix it, submit again

You receive business_profile.approved or business_profile.rejected when Loops decides. Show the outcome to the restaurant in your system, especially the rejection reasons.

You don’t have to wait for the review. Create brands and locations as soon as the merchant exists.

POST /v1/brands
{ "external_ref": "BRAND-1", "name": { "en": "Authentic Burger", "ar": "برجر الأصيل" } }
POST /v1/locations
{
"external_ref": "BR-01",
"brand_id": "brd_9Lk3Vb",
"name": { "en": "Olaya", "ar": "العليا" },
"phone": "+966112345678",
"address": { "line1": "King Fahd Road", "district": "Al Olaya", "city": "Riyadh", "latitude": 24.7136, "longitude": 46.6753 },
"opening_hours": {
"sunday": [{ "opens": "12:00", "closes": "02:00" }],
"monday": [{ "opens": "12:00", "closes": "02:00" }],
"tuesday": [{ "opens": "12:00", "closes": "02:00" }],
"wednesday": [{ "opens": "12:00", "closes": "02:00" }],
"thursday": [{ "opens": "12:00", "closes": "02:00" }],
"friday": [{ "opens": "13:30", "closes": "02:00" }],
"saturday": [{ "opens": "12:00", "closes": "02:00" }]
}
}
  • latitude and longitude are where drivers pick up orders. Take them from the restaurant, not from a geocoded address.
  • Opening hours are in the location’s time zone (Asia/Riyadh by default). A closing time earlier than the opening time means the next day. An empty list means closed that day. A day can have several ranges, for example [{ "opens": "06:00", "closes": "11:00" }, { "opens": "13:00", "closes": "23:00" }].
  • Change hours with PATCH /v1/locations/{location_id}. opening_hours replaces the whole week.

If an external_ref is already used, you get 409 external_ref_conflict with the existing object’s ID in existing_id. This usually means an earlier request succeeded; use that object.

After the business profile is approved, Loops connects each location to its delivery apps. That involves the delivery apps themselves, so it takes time.

  • Read a location’s connections with GET /v1/locations/{location_id}/channel-connections.
  • Each connection starts as pending. You receive channel_connection.status_changed when it becomes active.
  • Publish the brand’s menu before or after. Delivery apps show it once the connection is active.