SALAF EMSDevelopers
salafems.com

Errors

The error envelope, every code, and what to do with each.

Every failure — from a malformed key to a database conflict — comes back in one shape. Write one error handler, not twelve.

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Order not found.",
    "request_id": "4f1c9b02-7d3e-4a85-9c16-b0e2f7a34d59"
  }
}

The error object

FieldAlways presentWhat it is for
codeYesA stable machine-readable identifier. Branch on this.
messageYesA human-readable sentence. Show it to a developer; never parse it.
fieldsOnly on validation failuresA map of field name to the list of things wrong with it.
request_idWhenever a request id was stampedCorrelates with our logs. Echoed in the X-Request-Id header.

There is no success field and no envelope around the error. The HTTP status and the code are the answer.

Every error code

Authentication and authorization

HTTPCodeWhen
401UNAUTHORIZEDNo Authorization header, a token that is not salaf_sk_…, an unknown key, a plan without API access, or a store or company that is not live.
401KEY_REVOKEDThe key exists and was deliberately revoked.
401KEY_EXPIREDThe key's expiry date passed, or its rotation grace window closed.
403FORBIDDEN_SCOPEThe key is valid but was not granted the scope this endpoint requires. Scopes are fixed at creation — make a new key.

Request problems

HTTPCodeWhen
404NOT_FOUNDNo such record in the data this key can see. Also what a record belonging to another tenant returns.
422VALIDATION_ERRORA parameter or body field was missing, the wrong type, out of range, or not in the allowed set. Carries fields.
400VALIDATION_ERRORThe one case that stays 400: a required Idempotency-Key header is absent. fields names the header.
409CONFLICTThe write collides with an existing record — a duplicate unique value, or a reference to something that is not there.
429RATE_LIMITEDToo many requests for this key in the current window. See Rate limits.

Ours, not yours

HTTPCodeWhen
500INTERNALSomething failed on our side. The message is always "Something went wrong on our end." — deliberately generic, because the alternative is leaking our internals into your logs. The real cause is recorded against the request_id.

Retry a 500 once, with a short delay. If it persists, send us the request_id.

On the write endpoints

HTTPCodeWhen
400INSUFFICIENT_STOCKNot enough stock to satisfy the request. fields.items names the offending line, with what was requested and what is available.
400INVALID_STATUS_TRANSITIONThe order cannot move from its current status to the requested one. fields.status says which move was refused.
422IDEMPOTENCY_CONFLICTThe Idempotency-Key was reused with a different request body.
409IDEMPOTENCY_IN_FLIGHTThe same Idempotency-Key is still being processed. Retry shortly, with the same key.

Full detail on Writes & idempotency.

Validation errors

fields maps each parameter to everything wrong with it. Ordinary validation failures are normalised to 422, so there is one status meaning "your request was wrong" — the exceptions are the three 400s named above, which carry their own codes.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request was not valid.",
    "fields": {
      "limit": [
        "limit must not be greater than 100",
        "limit must be an integer number"
      ]
    },
    "request_id": "4f1c9b02-7d3e-4a85-9c16-b0e2f7a34d59"
  }
}

Request ids

Every request through the public API is stamped with an id, returned in the X-Request-Id response header and included in the body of any error.

HTTP/1.1 404 Not Found
X-Request-Id: 4f1c9b02-7d3e-4a85-9c16-b0e2f7a34d59
Content-Type: application/json

Log it on every failure. It is the only handle that finds your exact request in our logs — far more useful than a timestamp and an endpoint name, particularly for a sync that makes thousands of calls an hour.

You can also supply your own: send an X-Request-Id header and we will use your value instead of generating one. That lets you carry a trace id from your own system straight through ours, so one identifier spans both sides.

Handling errors well

  • Switch on code, with a default branch. New codes may be added in v1 — treat the list as open, and let anything unrecognised fall through to "unexpected error, log and alert".
  • Do not retry 4xx. A 401, 403, 404 or 422 will fail exactly the same way the second time. The only retryable statuses are 429 (wait for Retry-After) and 500 (once, briefly).
  • Distinguish KEY_REVOKED from UNAUTHORIZED in your alerting. A revoked key means a human deliberately turned your integration off; it should page someone, not silently retry forever.
  • Keep request_id in your own error records, not just in a log line that rotates out after a week.