Errors and rate limits

Every merchant endpoint answers with the same JSON envelope and a machine-readable code. This page lists every code the merchant endpoints return, with its HTTP status, what it means and what to do, plus the rate limits and when to retry.

Response envelope

Every response, success or error, uses the same envelope:

{
  "success": true,
  "uuid": "6b1f0c3e-2d4a-4f8b-9c1e-7a5d3b2f1e0c",
  "code": "SESSION_CREATED",
  "message": "...",
  "data": { "...": "..." },
  "errors": null
}

On an error, success is false, code is the error code, data is usually null, and errors lists problems by field (an empty list when there are none):

{
  "success": false,
  "uuid": "0e9d8c7b-...",
  "code": "VALIDATION_ERROR",
  "message": "One or more validation errors occurred.",
  "data": null,
  "errors": [
    { "field": "TotalAmount", "message": "Total amount must be greater than zero" },
    { "field": "Items[0].Quantity", "message": "Item quantity must be greater than zero." }
  ]
}
  • Write your code against code and the HTTP status, not message. Messages are for people and can change.
  • Field names in errors follow the request structure in PascalCase, for example ShippingAddress, Items[1].TotalAmount, MerchantUrl.Success.
  • uuid identifies the request. Every response also carries X-Request-Id and X-Correlation-Id headers. If you send your own X-Correlation-Id (a UUID), it is echoed back and used as uuid, so you can match our logs to yours. Quote uuid when you contact support.

Responses without an envelope

A few responses have no JSON body. Don't assume one exists:

HTTPWhen
401Missing, unknown, revoked or expired API key, a key from the other environment, or a publishable pk_ key in X-Api-Key. The header WWW-Authenticate: ApiKey realm="merchant-api" is set
403The caller isn't a merchant at all (for example, a token from another product). A merchant key that only lacks a scope gets the envelope with FORBIDDEN instead
413The request body is larger than 10 MB
400The body isn't valid JSON

HTTP statuses

HTTPMeaningRetry?
200Success—
201Created (sessions, links, webhook endpoints)—
202Accepted (webhook ping and replay are queued)—
400The request is invalid, or the action isn't allowed in the order's statusNo. Fix the request
401Authentication failedNo. Fix the key
403The key lacks the scopeNo. Use a key with the scope
404Not found, or it belongs to another merchantNo
409Conflict with the current stateOnly REQUEST_IN_PROGRESS
413Body too largeNo
422Idempotency key reused with a different bodyNo
429Rate limitedYes, after Retry-After
500Unexpected server errorWith care, see below
502, 503, 504Temporarily unavailableYes, with backoff

Error codes

Any endpoint

HTTPCodeMeaningWhat to do
400VALIDATION_ERRORA field is missing or invalid; errors names each one. Also returned for a page below 1 or a negative pageSize, and an empty or over-long orderReferenceId on lookup by referenceFix the fields listed in errors
400INVALID_REQUESTThe request had no bodySend the JSON body
401UNAUTHORIZEDThe caller is signed in but has no merchant account behind itUse a merchant API key
403FORBIDDENThe key doesn't have the scope this endpoint needsUse a key with the right scope
404NOT_FOUNDThe order, link, webhook endpoint, delivery or contract doesn't exist, or belongs to another merchantCheck the id; use the Qasimha id, not your reference
409INVALID_STATEThe operation conflicts with the current state, for example a change made at the same moment by another requestRead the resource again; if it persists, contact support with the uuid
409REQUEST_IN_PROGRESSA request with this Idempotency-Key is still runningWait and retry with the same key
422IDEMPOTENCY_KEY_REUSEDThis Idempotency-Key was used with a different bodyUse a new key for a new request. See Idempotency
429TOO_MANY_REQUESTSYou exceeded the rate limitWait for Retry-After seconds, then retry
500INTERNAL_ERRORUnexpected server errorSee Which errors to retry

Creating orders (/pay/session/create, /pay/pos/create, /pay/qr/create)

HTTPCodeMeaningWhat to do
400VALIDATION_ERRORMissing or invalid field, a basket that doesn't add up, a currencyCode other than SAR on a payment session, or an invalid merchantUrlFix the fields in errors. See the basket rules
400MERCHANT_REQUIREDThe caller has no merchant of its own and named none. Not returned to API keysUse a merchant API key
409DUPLICATE_ORDER_REFERENCEorderReferenceId is already used (payment session). If the order is yours, data has its installmentId and paymentUrlResume the existing checkout, or use a new reference
409MERCHANT_SUSPENDEDYour account can't create orders right nowContact support
422ORDER_DECLINEDOnly from the earlier provider-based checkout: the order was created but not approved. data has the installmentId. Today's hosted checkout declines on the page insteadOffer another payment method

Checkout links (/merchant/checkout-links)

HTTPCodeMeaningWhat to do
400INVALID_AMOUNTNo positive amount and no items to compute one, or more than 9999999999.99Send a positive amount
400AMOUNT_ITEMS_MISMATCHamount doesn't equal the sum of quantity × unitPriceSend one or the other, or make them agree
400INVALID_ITEMAn item has quantity below 1, a negative unitPrice, or a field that is too longFix the item
400PHONE_REQUIREDchannel is Link and there is no phoneSend the customer's phone, or use channel: "Qr"
400INVALID_ORDER_REFERENCEorderReferenceId is longer than 255 charactersShorten it
400EJAR_CONTRACT_NUMBER_REQUIREDReal-estate merchant without ejarContractNumberSend the Ejar contract number
400INVALID_EJAR_CONTRACT_NUMBERejarContractNumber is longer than 50 charactersCheck the number
400VALIDATION_ERRORmerchantUrl breaks a ruleFix the URL named in errors
404MERCHANT_NOT_FOUNDYour merchant account wasn't foundContact support
409DUPLICATE_ORDER_REFERENCEorderReferenceId is already used. data is empty hereUse a new reference, or look the order up by reference
409MERCHANT_SUSPENDEDYour account can't create orders right nowContact support
409NOT_OPENCancel only: the link was already completed, cancelled or has expiredRead the order to see its status

Orders (/merchant/orders)

HTTPCodeMeaningWhat to do
400INVALID_STATUSOn list or export: an unknown status filter. On capture, fulfil, refund or cancel: the action isn't allowed in the order's current status; message names itCheck the allowed actions per status
400INVALID_SHIPPED_AMOUNTFulfil: shippedAmount is 0, negative, or more than the outstanding amountSend a positive amount up to totalAmount − fulfilledAmount, or leave it out to fulfil the rest
400INVALID_AMOUNTRefund: amount is 0 or negativeSend a positive amount
400AMOUNT_EXCEEDEDRefund: amount is more than refundableAmount; message states the available amountRefund at most refundableAmount
404NOT_FOUNDNo such order for your account, or no contract on the orderCheck the id

Webhook endpoints (/merchant/webhooks)

HTTPCodeMeaningWhat to do
400URL_REQUIREDNo urlSend the URL
400URL_INVALIDurl isn't a valid absolute URLFix the URL
400URL_SCHEMEurl isn't https://Use HTTPS
400URL_PRIVATEurl points at a private or local addressUse a public host
400URL_TOO_LONGurl is longer than 1,024 charactersShorten it
400URL_USERINFOurl contains a user name or passwordRemove the credentials
400URL_FRAGMENTurl contains a #fragmentRemove the fragment
400INVALID_EVENTAn entry in events isn't a known event type; message lists the known onesUse names from Events
400LIMIT_EXCEEDEDYou already have 5 endpointsDelete one you no longer use
400VALIDATION_ERRORdescription is longer than 200 charactersShorten it
400INVALID_STATUSDelivery log: status isn't Pending, Succeeded or FailedUse one of those

Plans widget (/widget/plans, publishable key)

HTTPCodeMeaningWhat to do
401PUBLIC_KEY_REQUIREDNo X-Public-Key headerSend your publishable key
401INVALID_PUBLIC_KEYThe publishable key is unknown, revoked or expiredCheck the key and its environment
400INVALID_AMOUNTamount is missing, not positive, or has more than two decimalsSend a positive amount with at most two decimals
400UNSUPPORTED_CURRENCYcurrency isn't SARSend SAR or leave it out

Success codes

Successful responses also carry a code: SESSION_CREATED (sessions, POS, QR), LINK_CREATED, LINK_CANCELLED, WEBHOOK_CREATED, WEBHOOK_UPDATED, WEBHOOK_DELETED, SECRET_ROTATED, PING_QUEUED, DELIVERY_REPLAYED, OK (plan quotes) and SUCCESS (everything else). Check success and the HTTP status; don't rely on the exact success code.

Rate limits

EndpointsLimitCounted per
Every merchant endpoint (/pay/…, /merchant/…) together100 requests per minuteAPI key (a portal session counts per merchant)
GET /widget/plans600 requests per minutePublishable key
  • The window is a fixed one minute. Requests over the limit are refused at once, not queued.
  • Each API key has its own budget, so one integration can't use up another's. A separate key per system also separates their limits.
  • A refused request gets 429 with the code TOO_MANY_REQUESTS and a Retry-After header in seconds:
HTTP/1.1 429 Too Many Requests
Retry-After: 37
Content-Type: application/json

{ "success": false, "uuid": "...", "code": "TOO_MANY_REQUESTS", "message": "Too many requests. Try again in 37 second(s).", "data": null, "errors": [] }

On 429:

  1. Stop sending requests with that key until Retry-After has passed.
  2. Then retry with exponential backoff and random jitter if you are refused again.
  3. When retrying a POST, reuse the same Idempotency-Key. A 429 is refused before the key is recorded, so the retry runs normally.

To stay under the limit, use webhooks instead of polling orders, and cache plan quotes per page.

Which errors to retry

RetryDon't retry
Network errors and timeouts, 429, 409 REQUEST_IN_PROGRESS, 502, 503, 504400, 401, 403, 404, 413, 422, and every other 409: fix the request first
  • Retry with the same Idempotency-Key and the same body.
  • Back off exponentially, for example min(2^attempt × 500 ms, 8 s) plus up to 250 ms of random jitter, and honour Retry-After on 429. Give up after a few attempts.
  • A 500 on a write may or may not have done its work. Before you retry a create, look the order up with GET /api/v1/merchant/orders/by-reference/{orderReferenceId}. Before you retry a refund or fulfilment, read the order and compare refundedAmount or fulfilledAmount. If you retry, use the same Idempotency-Key; your orderReferenceId also protects you from a second order.
  • The server SDKs follow these rules for you: they retry network errors, 429, 502, 503, 504 and 409 REQUEST_IN_PROGRESS, and never retry other 4xx or 500.

Troubleshooting

My error handler crashes on 401

A 401 from a bad API key has an empty body. Check the HTTP status before parsing JSON.

I get 404 for an order I just created

Order endpoints take the Qasimha order id (installmentId from a session, id from a checkout link), not your orderReferenceId. To look up by your reference, use GET /api/v1/merchant/orders/by-reference/{orderReferenceId}. Also check that the key belongs to the same environment and merchant as the order.

I get 429 even though my traffic is low

All merchant endpoints share one budget per key. A polling loop on orders or checkout links can use it up. Switch to webhooks, or spread the work across a separate key per system.

Related


Did this page help you?