This is the full developer documentation for Loops Partner API
# Authentication
> Client credentials, access tokens and the merchant header.
The Loops Partner API uses **OAuth 2.0 client credentials**. Your application has a client ID and secret for each environment.
## Get a token
[Section titled “Get a token”](#get-a-token)
Your **token URL**, client ID and client secret are in the Loops Partners portal, under **Applications**. Each environment has its own.
```bash
curl -X POST $LOOPS_TOKEN_URL \
-d grant_type=client_credentials \
-d client_id=$LOOPS_CLIENT_ID \
-d client_secret=$LOOPS_CLIENT_SECRET
```
```json
{ "access_token": "eyJhbGciOi…", "expires_in": 900, "token_type": "Bearer" }
```
How tokens work:
* Tokens last **15 minutes**. There is no refresh token; when a token expires, request a new one.
* Cache the token and reuse it until shortly before it expires. Don’t request a token for every call.
* Send it on every request: `Authorization: Bearer `.
## Name the merchant
[Section titled “Name the merchant”](#name-the-merchant)
One token works for all the merchants connected to your application. Every request says which merchant it is for:
```http
GET /v1/orders HTTP/1.1
Host: api.partners.loops.sa
Authorization: Bearer eyJhbGciOi…
Loops-Merchant-Id: mer_7Hq2Lw8Z
```
If you built a **direct integration** for your own business, you have a single merchant and the header is optional.
## Keep your secret safe
[Section titled “Keep your secret safe”](#keep-your-secret-safe)
* Store the client secret in a secrets manager, never in source code or in an app installed on devices.
* Call the API from your servers, not from browsers or POS terminals.
* Rotate the secret in the Loops Partners portal. The old secret keeps working for 24 hours so you can deploy the change.
* If a secret leaks, rotate it immediately and tell us at .
## When access is refused
[Section titled “When access is refused”](#when-access-is-refused)
| Status | `code` | What to do |
| ------ | ----------------------- | ------------------------------------------------------------------------- |
| 401 | `unauthorized` | Get a new token |
| 403 | `insufficient_scope` | Your application isn’t approved for this scope. Request it in the portal. |
| 403 | `scope_not_granted` | The merchant didn’t grant this scope |
| 403 | `connection_not_active` | The merchant revoked access. Stop calling for this merchant. |
| 400 | `merchant_required` | Add the `Loops-Merchant-Id` header |
See [Errors](/guides/errors/) for the full list.
# How Loops Partners works
> The model behind the Loops Partner API — partners, merchants, connections and channels.
Loops connects merchants to the channels they sell on: delivery apps, marketplaces and e-commerce platforms. The **Loops Partner API** gives your system access to that network for the merchants who choose to connect to you.
```plaintext
Your system Loops Channels
(POS, ERP, tool) (delivery apps, marketplaces)
┌────────┐ Partner API ┌──────────────────────┐ ┌──── Channel A
│ You │ ◀─────────────▶ │ normalize · route │ ◀─┼──── Channel B
└────────┘ webhooks │ merchant consent │ └──── Channel C
└──────────────────────┘
```
## The pieces
[Section titled “The pieces”](#the-pieces)
**Application** : Your integration. It has a client ID and secret, the scopes Loops approved for it, and your webhook endpoints. Sandbox and production each have their own application.
**Merchant** : A business that uses Loops (`mer_…`). A merchant has brands and **locations** (`loc_…`), which are its branches or stores.
**Channel connection** : A location’s link to one channel account (`chc_…`). The merchant manages these in Loops. You can read them, so you know where a location’s orders come from.
**Connection** : The merchant’s approval for your application (`con_…`). It says which locations you may access and with which scopes. The merchant can change or revoke it at any time.
**Order** : An order from any channel, in one channel-neutral format (`ord_…`). It keeps the channel’s own order number as `channel_ref`.
**Event** : A record of something that happened, such as `order.created`. Events are sent to your webhook endpoints and stay listable for 30 days.
## How a typical integration works
[Section titled “How a typical integration works”](#how-a-typical-integration-works)
1. **You get approved** and receive sandbox credentials.
2. **A merchant connects to you.** They approve your access and choose their locations. You receive a `connection.created` event with their merchant ID and your own reference for them.
3. **You sync existing data.** List the merchant’s locations and recent orders.
4. **You stay in sync.** Webhooks deliver new orders and status changes as they happen.
5. **You recover from downtime.** If your endpoint was unavailable, list events after the last one you processed.
## What’s available today
[Section titled “What’s available today”](#whats-available-today)
The developer preview covers reading data:
* locations and their channel connections;
* orders, with full detail and incremental sync;
* order events by webhook;
* the Events API.
Writing back to Loops is on the roadmap: accepting and rejecting orders, menus, availability and hours. See [What’s coming](/resources/whats-coming/).
# Quickstart
> From credentials to your first order in a few minutes.
This guide uses the **sandbox**, which has test merchants and can create test orders. You need your sandbox token URL, client ID and client secret from the Loops Partners portal (**Applications**). [Request access](/resources/request-access/) if you don’t have them yet.
1. **Get an access token**
Exchange your client ID and secret for an access token. Tokens last 15 minutes; request a new one when it expires.
```bash
curl -X POST $LOOPS_TOKEN_URL \
-d grant_type=client_credentials \
-d client_id=$LOOPS_CLIENT_ID \
-d client_secret=$LOOPS_CLIENT_SECRET
```
```json
{ "access_token": "eyJhbGciOi…", "expires_in": 900, "token_type": "Bearer" }
```
2. **Check a merchant connection**
Every request names the merchant in the `Loops-Merchant-Id` header. Your sandbox comes with a test merchant.
```bash
curl https://sandbox.partners.loops.sa/v1/connection \
-H "Authorization: Bearer $TOKEN" \
-H "Loops-Merchant-Id: mer_7Hq2Lw8Z"
```
The response shows the scopes and locations that merchant granted you.
3. **List the merchant’s locations**
```bash
curl https://sandbox.partners.loops.sa/v1/locations \
-H "Authorization: Bearer $TOKEN" \
-H "Loops-Merchant-Id: mer_7Hq2Lw8Z"
```
4. **Create a test order** (sandbox only)
```bash
curl -X POST https://sandbox.partners.loops.sa/v1/sandbox/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Loops-Merchant-Id: mer_7Hq2Lw8Z" \
-H "Content-Type: application/json" \
-d '{ "location_id": "loc_2Xf9K" }'
```
5. **Read orders**
```bash
curl "https://sandbox.partners.loops.sa/v1/orders?location_id=loc_2Xf9K&limit=10" \
-H "Authorization: Bearer $TOKEN" \
-H "Loops-Merchant-Id: mer_7Hq2Lw8Z"
```
6. **Receive webhooks**
Add an endpoint in the Loops Partners portal and subscribe to `order.*`. Verify each request’s signature before trusting it:
* Node.js
```js
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.LOOPS_WEBHOOK_SECRET);
app.post("/loops/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const event = wh.verify(req.body, req.headers); // throws if invalid
queue.push(event); // acknowledge fast, process later
res.sendStatus(200);
});
```
* Python
```python
from standardwebhooks.webhooks import Webhook
wh = Webhook(os.environ["LOOPS_WEBHOOK_SECRET"])
@app.post("/loops/webhooks")
def loops_webhook(request):
event = wh.verify(request.body, request.headers) # raises if invalid
queue.put(event) # acknowledge fast, process later
return "", 200
```
Next
Read [Syncing orders](/guides/syncing-orders/) for the recommended sync loop, and [Webhooks](/guides/webhooks/) for retries, duplicates and recovery.
# Errors
> Error format, codes, and how to handle each one.
Errors use [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457), with a stable `code` you can switch on:
```json
{
"type": "https://docs.partners.loops.sa/errors/scope_not_granted",
"title": "Scope not granted",
"status": 403,
"code": "scope_not_granted",
"detail": "The merchant has not granted orders:read to this application.",
"request_id": "req_01J9X7P2QK"
}
```
* Switch on **`code`**, not on `title` or `detail`; those may be reworded.
* Log **`request_id`** (also sent as the `Loops-Request-Id` header) and include it when you contact support.
## Codes
[Section titled “Codes”](#codes)
| Status | `code` | Meaning | What to do |
| ------ | ----------------------- | ---------------------------------------------- | ------------------------------------------- |
| 400 | `invalid_request` | A parameter is invalid; see `errors[]` | Fix the request |
| 400 | `merchant_required` | `Loops-Merchant-Id` missing | Add the header |
| 401 | `unauthorized` | Token missing, expired or invalid | Get a new token |
| 403 | `insufficient_scope` | Your application isn’t approved for this scope | Request the scope in the portal |
| 403 | `scope_not_granted` | The merchant didn’t grant this scope | Ask the merchant, or skip the feature |
| 403 | `connection_not_active` | The merchant revoked access | Stop calling for this merchant |
| 403 | `application_suspended` | Loops suspended your application | Contact |
| 403 | `channel_not_permitted` | The connection doesn’t include this channel | Skip this channel |
| 404 | `not_found` | Doesn’t exist, **or isn’t shared with you** | Check the ID and the connection’s locations |
| 404 | `merchant_not_found` | Unknown merchant ID | Check the header value |
| 429 | `rate_limited` | Too many requests | Wait `Retry-After` seconds |
| 503 | `upstream_unavailable` | A Loops service is temporarily unavailable | Retry with exponential backoff |
## Retrying safely
[Section titled “Retrying safely”](#retrying-safely)
* Retry **`429`, `503` and network errors** with exponential backoff and jitter.
* Don’t retry other `4xx` responses without changing the request.
* `GET` requests are always safe to retry.
# Syncing orders
> Initial load, real-time updates and recovery — the recommended sync loop.
Orders from every channel arrive in one format. This guide shows how to load them and keep them in sync.
## The order object
[Section titled “The order object”](#the-order-object)
```json
{
"id": "ord_01J9QW3T8M",
"version": 3,
"merchant_id": "mer_7Hq2Lw8Z",
"location_id": "loc_2Xf9K",
"channel": "delivery_app_a",
"channel_connection_id": "chc_5Rm1Pa",
"channel_ref": "CH-88213",
"type": "delivery",
"status": "accepted",
"status_history": [
{ "status": "placed", "at": "2026-10-04T12:01:05Z" },
{ "status": "accepted", "at": "2026-10-04T12:02:10Z" }
],
"items": [
{
"name": { "en": "Chicken Burger", "ar": "برجر دجاج" },
"sku": "CB-01",
"quantity": 2,
"unit_price": { "amount": 2500, "currency": "SAR" },
"modifiers": [
{
"name": { "en": "Extra cheese", "ar": "جبنة إضافية" },
"quantity": 1,
"unit_price": { "amount": 300, "currency": "SAR" }
}
],
"notes": "No onion"
}
],
"totals": {
"subtotal": { "amount": 5600, "currency": "SAR" },
"delivery_fee": { "amount": 900, "currency": "SAR" },
"tax": { "amount": 975, "currency": "SAR" },
"total": { "amount": 7475, "currency": "SAR" }
},
"payment": { "method": "online", "paid": true },
"created_at": "2026-10-04T12:01:05Z",
"updated_at": "2026-10-04T12:02:10Z",
"extensions": {}
}
```
* **`channel_ref`** is the order number shown on the channel. Display it to staff; they’ll need it when talking to the channel or the customer.
* **Amounts** are in minor units: `7475` means 74.75 SAR.
* **Fields a channel doesn’t provide** are `null`, never made up. `GET /v1/channels` tells you which fields each channel supplies.
* **`extensions`** holds channel-specific extras. You can ignore it safely.
## Statuses
[Section titled “Statuses”](#statuses)
```plaintext
placed ──▶ accepted ──▶ preparing ──▶ ready ──▶ picked_up ──▶ delivered
│ │ │ │
└▶ rejected └────────────┴───────────┴──▶ cancelled
```
Not every channel reports every step. Be ready for an order to skip from `accepted` straight to `delivered`.
## The sync loop
[Section titled “The sync loop”](#the-sync-loop)
1. **When a merchant connects**, load recent orders:
```plaintext
GET /v1/orders?created_from=2026-10-01T00:00:00Z&limit=100
```
Follow `next_cursor` until it is `null`.
2. **Then rely on webhooks.**
* `order.created` carries the full order.
* `order.status_changed` carries the new status and `version`.
* `order.updated` means items, notes or totals changed. Fetch the order again.
3. **Ignore stale updates.** Keep the highest `version` you’ve seen for each order, and drop anything lower. Webhooks are not guaranteed to arrive in order.
4. **Recover after downtime** with the Events API, from the last event you processed:
```plaintext
GET /v1/events?after=evt_01J9C2X4RS&types=order.*
```
5. **Optional safety net.** Every few minutes, list `GET /v1/orders?updated_since=` to catch anything missed.
## Filtering
[Section titled “Filtering”](#filtering)
| Parameter | Example |
| ----------------------------- | ------------------------------------ |
| `location_id` | `loc_2Xf9K` |
| `channel` | A channel ID from `GET /v1/channels` |
| `status` | `accepted` |
| `updated_since` | `2026-10-04T09:00:00Z` |
| `created_from` / `created_to` | ISO 8601 times, UTC |
# Webhooks
> Receive, verify and process events reliably.
Loops sends an HTTPS `POST` to your endpoint when something happens: a new order, a status change, a merchant connecting or leaving.
## Set up an endpoint
[Section titled “Set up an endpoint”](#set-up-an-endpoint)
In the Loops Partners portal, open **Webhooks → Add endpoint**:
* give an HTTPS URL;
* choose the events to receive (wildcards such as `order.*` work);
* copy the endpoint’s signing secret.
Use **Send test event** to check your endpoint end to end.
## What you receive
[Section titled “What you receive”](#what-you-receive)
```http
POST /loops/webhooks HTTP/1.1
Content-Type: application/json
webhook-id: evt_01J9C2X4RS
webhook-timestamp: 1791100800
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"id": "evt_01J9C2X4RS",
"type": "order.status_changed",
"created_at": "2026-10-04T12:02:11Z",
"merchant_id": "mer_7Hq2Lw8Z",
"location_id": "loc_2Xf9K",
"connection_id": "con_4Tz8Yp1W",
"origin": { "type": "channel", "application_id": null },
"data": {
"order_id": "ord_01J9QW3T8M",
"channel_ref": "CH-88213",
"previous_status": "placed",
"status": "accepted",
"version": 3
}
}
```
## Verify the signature
[Section titled “Verify the signature”](#verify-the-signature)
Requests are signed with the [Standard Webhooks](https://www.standardwebhooks.com/) scheme, so you can use the official libraries. Always verify before trusting the body. Reject requests whose timestamp is more than 5 minutes old; the libraries do this for you.
* Node.js
```js
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.LOOPS_WEBHOOK_SECRET);
// Use the raw body: re-serialized JSON breaks the signature.
app.post("/loops/webhooks", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = wh.verify(req.body, req.headers);
} catch {
return res.sendStatus(400);
}
queue.push(event);
res.sendStatus(200);
});
```
* Python
```python
from standardwebhooks.webhooks import Webhook, WebhookVerificationError
wh = Webhook(os.environ["LOOPS_WEBHOOK_SECRET"])
@app.post("/loops/webhooks")
def loops_webhook(request):
try:
event = wh.verify(request.body, request.headers)
except WebhookVerificationError:
return "", 400
queue.put(event)
return "", 200
```
* PHP
```php
use StandardWebhooks\Webhook;
$wh = new Webhook(getenv('LOOPS_WEBHOOK_SECRET'));
$payload = file_get_contents('php://input');
try {
$event = $wh->verify($payload, getallheaders());
} catch (Exception $e) {
http_response_code(400);
exit;
}
// enqueue $event, then:
http_response_code(200);
```
## Process reliably
[Section titled “Process reliably”](#process-reliably)
* **Reply fast.** Return a `2xx` within 10 seconds, then process the event in the background. Slow responses count as failures.
* **Expect duplicates.** Delivery is at least once. Store each `webhook-id`, and skip IDs you’ve already handled.
* **Expect any order.** Use `version` (orders) or `created_at` to ignore stale events.
* **Ignore your own changes.** When `origin.application_id` is your application, the event reflects something you did.
## Retries
[Section titled “Retries”](#retries)
If your endpoint fails or times out, Loops retries. Attempt 1 is sent immediately; each later attempt waits after the previous one:
| Attempt | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| ------- | ---- | ----- | ----- | ------ | --- | --- | ---- |
| Wait | 10 s | 1 min | 5 min | 30 min | 2 h | 6 h | 12 h |
* After about 21 hours the delivery is marked failed. The event stays available through the Events API for 30 days.
* An endpoint that keeps failing for 3 days is disabled, and your team is emailed. Re-enable it in the portal, then catch up with `GET /v1/events?after=…`.
Caution
Webhooks are the fast path, not the only path. Always be able to recover from the Events API, because no webhook system can guarantee your endpoint is up.
See the [event reference](/reference/events/) for every event type.
# Environments
> Sandbox for building and testing, production for live merchants.
| | Sandbox | Production |
| ------------ | ----------------------------------------- | -------------------------------------------------- |
| API base URL | `https://sandbox.partners.loops.sa/v1` | `https://api.partners.loops.sa/v1` |
| Token URL | Shown in the portal, under Applications | Shown in the portal, under Applications |
| Merchants | Test merchants created for you | Real merchants who connect to you |
| Channels | Simulated; nothing reaches a real channel | Live |
| Credentials | Sandbox application | Production application, issued after certification |
| Availability | Best effort, no SLA | Per your partner agreement |
Credentials never work across environments.
## Test orders
[Section titled “Test orders”](#test-orders)
The sandbox can create realistic orders on a test location, so you can exercise your whole flow, webhooks included:
```bash
curl -X POST https://sandbox.partners.loops.sa/v1/sandbox/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Loops-Merchant-Id: mer_7Hq2Lw8Z" \
-H "Content-Type: application/json" \
-d '{ "location_id": "loc_2Xf9K" }'
```
## Going to production
[Section titled “Going to production”](#going-to-production)
Before Loops issues production credentials, we review your integration against a short checklist. Your integration:
* [ ] gets tokens and renews them when they expire;
* [ ] sends `Loops-Merchant-Id` and handles revoked connections;
* [ ] verifies webhook signatures;
* [ ] ignores duplicate and out-of-order events;
* [ ] catches up through the Events API after downtime;
* [ ] backs off on `429`, using `Retry-After`;
* [ ] ignores unknown fields and enum values;
* [ ] logs `Loops-Request-Id`.
# Merchants and connections
> How merchants approve your access, and what happens when they change it.
You can only access a merchant’s data after the merchant approves it. That approval is a **connection**.
## What a connection contains
[Section titled “What a connection contains”](#what-a-connection-contains)
```json
{
"id": "con_4Tz8Yp1W",
"merchant_id": "mer_7Hq2Lw8Z",
"external_ref": "BRQ-10021",
"status": "active",
"scopes": ["locations:read", "orders:read", "events:read"],
"location_ids": ["loc_2Xf9K", "loc_8Pn4R"],
"include_future_locations": false,
"created_at": "2026-10-01T09:12:44Z"
}
```
* **`scopes`**: what the merchant granted. This is at most what Loops approved for your application, and often less.
* **`location_ids`**: the branches you may access. Anything else returns `404`.
* **`include_future_locations`**: whether branches the merchant adds later are shared automatically.
* **`external_ref`**: *your* identifier for this merchant, so you can match the connection to the account in your system.
Get the current connection at any time with `GET /v1/connection`.
## How merchants connect
[Section titled “How merchants connect”](#how-merchants-connect)
**During the developer preview:**
1. Send Loops the merchant’s name and your reference for them.
2. The merchant signs a consent form choosing branches and data.
3. Loops creates the connection.
4. You receive `connection.created`, with the merchant ID and your reference.
**Coming next:** a self-serve link. You create a link from your system; the merchant signs in to Loops, reviews your request, chooses branches and approves. See [What’s coming](/resources/whats-coming/).
## When a merchant changes their mind
[Section titled “When a merchant changes their mind”](#when-a-merchant-changes-their-mind)
| Event | What it means | What to do |
| -------------------- | --------------------------- | ----------------------------------------------------------------------------- |
| `connection.updated` | Scopes or locations changed | Fetch `GET /v1/connection` and adjust what you sync |
| `connection.revoked` | Access withdrawn | Stop calling for this merchant. Further calls return `connection_not_active`. |
## IDs you will see
[Section titled “IDs you will see”](#ids-you-will-see)
| Prefix | What |
| ------ | -------------------------- |
| `mer_` | Merchant |
| `brd_` | Brand |
| `loc_` | Location (branch or store) |
| `chc_` | Channel connection |
| `con_` | Connection |
| `ord_` | Order |
| `evt_` | Event |
IDs are opaque strings. Store them as given, and don’t parse them.
# Scopes
> What your application can access, and who decides.
Access is decided at three levels, and all three must allow a request:
1. **Loops approves** the scopes your application may use.
2. **The merchant grants** some or all of them, for the locations they choose.
3. **The endpoint requires** a specific scope.
## Available in the developer preview
[Section titled “Available in the developer preview”](#available-in-the-developer-preview)
| Scope | Allows |
| ---------------- | -------------------------------------------- |
| `merchant:read` | Read the merchant’s profile and brands |
| `locations:read` | Read locations and their channel connections |
| `orders:read` | Read orders and receive order events |
| `events:read` | List past events and request redelivery |
## Planned
[Section titled “Planned”](#planned)
| Scope | Will allow |
| ------------------------------------ | -------------------------------------------- |
| `catalog:read` / `catalog:write` | Read and update menus |
| `orders:write` | Accept, reject, mark ready and cancel orders |
| `inventory:read` / `inventory:write` | Read and update item availability |
| `hours:read` / `hours:write` | Read and update opening hours; pause stores |
A write scope always needs its read scope. A merchant granting `orders:write`, for example, also grants `orders:read`.
## Asking for more
[Section titled “Asking for more”](#asking-for-more)
Request additional scopes in the Loops Partners portal, with a short explanation of what you’ll use them for. After Loops approves a scope, each merchant has to approve it before it applies to their connection.
# Loops Partners
> Integrate once with the Loops Partner API and get your merchants' orders from every delivery app and marketplace they use, in one format, in real time.
## Why Loops Partners
[Section titled “Why Loops Partners”](#why-loops-partners)
Restaurants and stores sell through many channels at once. Integrating your POS or ERP with each channel separately is slow, fragile, and never finished. Loops already connects merchants to those channels. With Loops Partners, you connect to Loops once.
Every channel, one format
Orders from all of a merchant’s channels arrive in the same shape, with the channel’s own order number kept for your staff.
Real time
Signed webhooks tell you the moment an order is created or its status changes. An Events API lets you catch up after downtime.
Merchant-approved access
Merchants choose which branches and data you can see, and can change their mind at any time. You only ever get what they granted.
Built for the region
Bilingual item names, SAR amounts in halalas, Riyadh time zones, and documentation in English and Arabic.
## Who it’s for
[Section titled “Who it’s for”](#who-its-for)
POS providers
Show channel orders on your terminals and keep their status in sync.
ERP and accounting
Pull every channel’s orders into your back office without per-channel work.
Management tools
Kitchen displays, analytics, and operations tools that need the full order picture.
Merchants
Running your own systems? Build a direct integration for your own business.
## Start here
[Section titled “Start here”](#start-here)
[How Loops Partners works](/getting-started/overview/)Merchants, connections, and the data you can access.
[Quickstart](/getting-started/quickstart/)From credentials to your first order in a few minutes.
[Webhooks](/guides/webhooks/)Receive and verify order events.
[API reference](/api/)Every endpoint, with request and response examples.
# API conventions
> Versioning, formats, pagination, rate limits and headers.
## Versioning
[Section titled “Versioning”](#versioning)
* The major version is in the path: `/v1`.
* **Additive changes are not breaking:** new endpoints, fields, event types and enum values. Ignore fields you don’t recognize and handle unknown enum values gracefully.
* Breaking changes get a new major version, with at least 6 months of overlap. Every change is announced in the [changelog](/resources/changelog/).
## Formats
[Section titled “Formats”](#formats)
| Topic | Convention |
| --------------------- | ------------------------------------------------------------------------------------- |
| Encoding | JSON, UTF-8 |
| Field names | `snake_case` |
| IDs | Opaque prefixed strings (`ord_…`). Don’t parse them. |
| Time | ISO 8601 in UTC, e.g. `2026-10-04T12:30:00Z`. Locations also have an IANA `timezone`. |
| Money | `{ "amount": 4550, "currency": "SAR" }`. The amount is in minor units (halalas). |
| Text in two languages | `{ "en": "…", "ar": "…" }`; either may be `null` |
| Missing data | `null`, never invented |
## Pagination
[Section titled “Pagination”](#pagination)
List endpoints take `limit` (1–100, default 50) and return a cursor:
```json
{ "data": [ … ], "next_cursor": "eyJpZCI6Im9yZF8…" }
```
Pass `next_cursor` as `cursor` to get the next page. It is `null` on the last page.
## Rate limits
[Section titled “Rate limits”](#rate-limits)
Limits apply per application. The default in the developer preview is **20 requests per second, with bursts up to 50**. Every response carries:
| Header | Meaning |
| --------------------- | -------------------------------------- |
| `RateLimit-Limit` | Requests allowed in the current window |
| `RateLimit-Remaining` | Requests left |
| `RateLimit-Reset` | Seconds until the window resets |
Over the limit, you get `429` with `Retry-After`. Prefer webhooks to frequent polling; they’re faster and don’t use your limit.
## Request headers
[Section titled “Request headers”](#request-headers)
| Header | When |
| ------------------------------- | ----------------------------------------------- |
| `Authorization: Bearer ` | Always |
| `Loops-Merchant-Id: mer_…` | Always for platform applications |
| `Idempotency-Key: ` | On write requests (coming with write endpoints) |
## Response headers
[Section titled “Response headers”](#response-headers)
| Header | Meaning |
| ------------------ | -------------------------------------------------- |
| `Loops-Request-Id` | Unique request ID. Log it and quote it to support. |
# Event types
> Every event Loops sends, and the scope needed to receive it.
All events share the same envelope (see [Webhooks](/guides/webhooks/)). You only receive events for locations and scopes the merchant granted you.
## Available now
[Section titled “Available now”](#available-now)
| Event | When | Scope |
| ----------------------------------- | ----------------------------------------------------------------------- | ---------------- |
| `order.created` | A new order arrived from any channel. `data` is the full order. | `orders:read` |
| `order.status_changed` | An order moved to a new status | `orders:read` |
| `order.updated` | Items, notes or totals changed. Fetch the order again. | `orders:read` |
| `order.cancelled` | The channel, merchant or customer cancelled the order | `orders:read` |
| `connection.created` | A merchant connected to your application | — |
| `connection.updated` | The merchant changed scopes or locations | — |
| `connection.revoked` | The merchant withdrew access | — |
| `location.updated` | A location’s name, status or address changed | `locations:read` |
| `channel_connection.status_changed` | A location’s channel connection changed status, e.g. needs reconnecting | `locations:read` |
## Coming later
[Section titled “Coming later”](#coming-later)
| Event | When |
| ---------------------------------------------------------------------- | ---------------------------------------------- |
| `catalog.updated` | A location’s menu changed |
| `order.delivery_status_changed` | Delivery progress for an order |
| `order.handler_timeout` | An order wasn’t accepted in time |
| `sync_job.completed` / `sync_job.partially_failed` / `sync_job.failed` | Result of a menu, availability or hours update |
## Example: `order.created`
[Section titled “Example: order.created”](#example-ordercreated)
```json
{
"id": "evt_01J9C2W8QD",
"type": "order.created",
"created_at": "2026-10-04T12:01:06Z",
"merchant_id": "mer_7Hq2Lw8Z",
"location_id": "loc_2Xf9K",
"connection_id": "con_4Tz8Yp1W",
"origin": { "type": "channel", "application_id": null },
"data": {
"id": "ord_01J9QW3T8M",
"version": 1,
"channel_ref": "CH-88213",
"status": "placed",
"items": [ … ],
"totals": { "total": { "amount": 7475, "currency": "SAR" } }
}
}
```
# Use with AI tools
> Give AI assistants accurate context about the Loops Partner API.
Using an AI assistant or AI code editor to build your integration? Point it at these files. They are kept in sync with the documentation, so the assistant works from the real API, not from guesses.
| File | Use it for |
| ------------------------------------ | --------------------------------------------------------------------------------------------- |
| [`/llms.txt`](/llms.txt) | A short index of the documentation, following the [llms.txt](https://llmstxt.org/) standard |
| [`/llms-full.txt`](/llms-full.txt) | The complete documentation in one plain-text file. Best for giving an assistant full context. |
| [`/llms-small.txt`](/llms-small.txt) | A compact version for tools with a small context window |
| [`/openapi.yaml`](/openapi.yaml) | The OpenAPI 3.1 specification, for generating clients and checking request shapes |
## Tips
[Section titled “Tips”](#tips)
* Paste the link to `llms-full.txt` into your assistant, or add it as a documentation source in your editor.
* Share `openapi.yaml` when you want the assistant to generate a typed client or validate requests.
* The API is in developer preview: only the endpoints in the reference exist. If an assistant suggests an endpoint you can’t find there, it doesn’t exist yet.
# Changelog
> Every change to the Loops Partner API.
Changes are listed newest first. Breaking changes are marked, and announced to partners by email before release.
## 2026-10 — Developer preview
[Section titled “2026-10 — Developer preview”](#2026-10--developer-preview)
* First public draft of the Loops Partner API `v1`.
* Endpoints: connection, locations, channel connections, channels, orders, events.
* Webhooks: order, connection, location and channel connection events, signed with Standard Webhooks.
# Request access
> How to become a Loops partner.
Loops Partners is in developer preview, and we’re onboarding a small number of partners. We review each request so we can support you properly.
## Who we’re looking for
[Section titled “Who we’re looking for”](#who-were-looking-for)
* **POS providers** serving restaurants or retail in Saudi Arabia.
* **ERP, accounting and back-office** software.
* **Management tools**: kitchen displays, analytics, operations.
* **Merchants** who want to connect their own systems.
## How to apply
[Section titled “How to apply”](#how-to-apply)
Email **** with:
1. your company name, website and commercial registration number;
2. what you want to build, and the data you need;
3. roughly how many merchants you expect to connect in the next 6 months;
4. a technical contact.
## What happens next
[Section titled “What happens next”](#what-happens-next)
1. We review your request, usually within 5 business days.
2. We agree the partner terms.
3. Your team is invited to the Loops Partners portal.
4. You build against the sandbox.
5. We review your integration, then issue production credentials.
# What's coming
> What we're building next for Loops Partners.
The developer preview covers reading orders and locations and receiving order events. Next, in no particular order:
* **Order handling**: accept, reject, mark ready and cancel orders from your system, with a clear owner per branch so two systems never act on the same order.
* **Menus**: read a location’s menu, and push updates to every channel at once, with a result per channel.
* **Availability and hours**: mark items unavailable, change opening hours, and pause a store across channels.
* **Delivery status**: follow delivery progress for orders delivered through Loops.
* **E-commerce marketplaces**: orders from the marketplaces and online stores merchants run through Loops.
* **Self-serve merchant linking**: send merchants a link from your product; they approve in Loops with no manual steps.
Have a use case that isn’t here? Tell us at .