API fundamentals
API fundamentals
The response envelope, errors, pagination, ETags and idempotency keys: rules that apply to every endpoint in the API.
Base URL and authentication
GET /stores/{storeId}/v1/products HTTP/1.1
Host: larm.sa
Authorization: Bearer lar_at_Q2mV0hN8sYc1kP4tRw7Xa.4f0jK2…- Every path starts with
/stores/{storeId}/v1, wherestoreIdis the store the token was issued for. Authorization: Beareris required. Any other scheme returns 401INVALID_TOKEN.- Send requests from a server. Any
Cookieheader returns 400INVALID_PUBLIC_REQUEST. - Never put tokens or secrets in the URL;
access_token,refresh_token,client_secret,codeandcode_verifierare rejected in the query string.
Response envelope
Every successful response has status 200, including creates. There is no 201 and no 204.
{
"data": { "id": 311, "name": "Top-up card 100 SAR" },
"execution": {
"appId": "…", "versionId": "…", "installationId": "…", "storeId": "…",
"requestId": "…", "generation": 1, "routeEpoch": 4, "appAuthEpoch": 1, "grantRevision": 1
},
"meta": { "request_id": "3f0c2b8e-7a51-4c1e-9d0a-5b6f1e2d4c3a" }
}| Field | Meaning |
|---|---|
data | The resource or list of resources |
execution | Who executed the request. Its keys are camelCase, unlike the rest of the API |
meta.request_id | The request ID. Include it in any support request |
Headers on every response:
| Header | Value |
|---|---|
x-larm-api-version | The dated API version, such as 2026-10-15 |
X-Larm-Request-Id | The same request ID |
Cache-Control | private, no-store |
ETag | On products and categories, as described below |
Errors
{ "error": { "code": "SCOPE_REQUIRED" }, "meta": { "request_id": "…" } }An error is a stable code only, with no message and no field details. Branch on error.code; the full list is in Error codes. Every 429 and 503 from the gateway carries Retry-After.
Pagination
Lists use cursor pagination, not page numbers:
curl "https://larm.sa/stores/$STORE_ID/v1/products?limit=50" -H "Authorization: Bearer $ACCESS_TOKEN"
# then
curl "https://larm.sa/stores/$STORE_ID/v1/products?limit=50&cursor=$NEXT_CURSOR" -H "Authorization: Bearer $ACCESS_TOKEN"limitis 1 to 100, default 25. Outside that range: 400INVALID_FILTER.- On the last page,
meta.next_cursorisnull. - A cursor is bound to the filters it was issued with. Changing a filter while reusing an old cursor returns 400
CURSOR_MISMATCH. - A cursor is valid for 15 minutes from the first page, not from the latest page. Finish the list within that window; otherwise you get 400
CURSOR_EXPIREDand must start over. - Products, categories and inventory also return
has_moreandlimitinmeta. - An unknown query key returns 422
UNKNOWN_FIELD.
async function* all(path) {
let cursor = null
do {
const url = new URL(`https://larm.sa/stores/${storeId}/v1${path}`)
url.searchParams.set('limit', '100')
if (cursor) url.searchParams.set('cursor', cursor)
const response = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } })
if (!response.ok) throw new Error((await response.json()).error.code)
const { data, meta } = await response.json()
yield* data
cursor = meta.next_cursor
} while (cursor)
}ETag and If-Match
Updating a product or category requires proof that you have seen the latest version (optimistic concurrency), so your app never overwrites the merchant's changes:
- Read the resource. Take the
ETagheader value from the response. - Send it in
If-Matchwith thePATCH.
curl -X PATCH "https://larm.sa/stores/$STORE_ID/v1/products/311/prices" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'If-Match: "p1.7.Xk2…"' \
-H "Content-Type: application/json" \
-d '{"price":"95.00","old_price":"110.00"}'| Case | Response |
|---|---|
No If-Match | 428 PRECONDITION_REQUIRED |
| The resource changed after you read it | 412 RESOURCE_CHANGED: read it again and rebuild the update |
If-Match is required on: product content, product media, product SEO, product prices, and category updates. Inventory, page, coupon and review updates do not use it.
Idempotency keys
A create can succeed and its response can be lost. Send an Idempotency-Key and LARM returns the stored response instead of creating a second copy:
Idempotency-Key: 5f2c1a9e-3b7d-4e60-8a14-c9d2e7f0b358- Required on: create product, create category, create page, adjust inventory. Without it: 400
IDEMPOTENCY_KEY_REQUIRED. - Optional on other writes, and not used with webhook subscriptions.
- Up to 128 bytes, no whitespace. Scoped to the store and app, and kept for 24 hours.
| Case | Response |
|---|---|
| Same key, same request, first one completed | The same stored response |
| Same key, first one still running | 409 OPERATION_IN_PROGRESS: wait a second and retry |
| Same key, different request | 409 IDEMPOTENCY_CONFLICT |
Request limits
| Limit | Value |
|---|---|
| Body size | 65,536 bytes; otherwise 413 BODY_TOO_LARGE |
| Body type | A JSON object as application/json; otherwise 415 CONTENT_TYPE_REQUIRED |
| Query string | 4096 characters, 32 keys, no repeats |
| Timeout | 10 seconds per request |
| Methods | GET, POST, PATCH and DELETE. A wrong method returns 405 with Allow |
Rate limits are covered in Rate limits.
Dates and amounts
- Dates are ISO 8601 in UTC, such as
2026-10-15T09:12:44Z. Analytics use Riyadh time and say so inmeta.timezone. - Amounts are strings with two decimal places, such as
"205.00", and the currency isSAR. Do not convert them to floating-point numbers for arithmetic. - Phone numbers are in E.164 format, such as
+966512345678.