Developers

API fundamentals

Markdown

API fundamentals

The response envelope, errors, pagination, ETags and idempotency keys: rules that apply to every endpoint in the API.

Base URL and authentication

HTTP
GET /stores/{storeId}/v1/products HTTP/1.1
Host: larm.sa
Authorization: Bearer lar_at_Q2mV0hN8sYc1kP4tRw7Xa.4f0jK2…
  • Every path starts with /stores/{storeId}/v1, where storeId is the store the token was issued for.
  • Authorization: Bearer is required. Any other scheme returns 401 INVALID_TOKEN.
  • Send requests from a server. Any Cookie header returns 400 INVALID_PUBLIC_REQUEST.
  • Never put tokens or secrets in the URL; access_token, refresh_token, client_secret, code and code_verifier are rejected in the query string.

Response envelope

Every successful response has status 200, including creates. There is no 201 and no 204.

JSON
{
  "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" }
}
FieldMeaning
dataThe resource or list of resources
executionWho executed the request. Its keys are camelCase, unlike the rest of the API
meta.request_idThe request ID. Include it in any support request

Headers on every response:

HeaderValue
x-larm-api-versionThe dated API version, such as 2026-10-15
X-Larm-Request-IdThe same request ID
Cache-Controlprivate, no-store
ETagOn products and categories, as described below

Errors

JSON
{ "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
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"
  • limit is 1 to 100, default 25. Outside that range: 400 INVALID_FILTER.
  • On the last page, meta.next_cursor is null.
  • 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_EXPIRED and must start over.
  • Products, categories and inventory also return has_more and limit in meta.
  • An unknown query key returns 422 UNKNOWN_FIELD.
Node.js
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:

  1. Read the resource. Take the ETag header value from the response.
  2. Send it in If-Match with the PATCH.
cURL
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"}'
CaseResponse
No If-Match428 PRECONDITION_REQUIRED
The resource changed after you read it412 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:

HTTP
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.
CaseResponse
Same key, same request, first one completedThe same stored response
Same key, first one still running409 OPERATION_IN_PROGRESS: wait a second and retry
Same key, different request409 IDEMPOTENCY_CONFLICT

Request limits

LimitValue
Body size65,536 bytes; otherwise 413 BODY_TOO_LARGE
Body typeA JSON object as application/json; otherwise 415 CONTENT_TYPE_REQUIRED
Query string4096 characters, 32 keys, no repeats
Timeout10 seconds per request
MethodsGET, 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 in meta.timezone.
  • Amounts are strings with two decimal places, such as "205.00", and the currency is SAR. Do not convert them to floating-point numbers for arithmetic.
  • Phone numbers are in E.164 format, such as +966512345678.

    ↑↓ moveEnter openEsc close