Skip to main content

The envelope

Every response from this API has the same shape. Two shapes, in fact: one for success, one for failure. Parse once, handle everywhere.

{ "requestId": "01JB2K9WQ4X7YH3M8N5P6R7T8V", "code": "OK", "data": { } }
{ "requestId": "01JB2K9WQ4X7YH3M8N5P6R7T8V", "code": "PIN_EXPIRED",
"message": "The PIN has expired. Request a new one.", "retryable": false }

The four fields

requestId: unique to this call, and also returned as the X-Request-Id header. It resolves to a specific row in our logs. Store it against your own record of the call: quoting it in a support request is the difference between us finding your exact call in seconds and asking you a dozen questions.

code: this is the contract. A stable string. Branch on it.

message: written for a human reading a log. It may change without notice, and it is not translated. If your code inspects it, that code will break.

retryable: whether trying the identical request again could possibly succeed. false means the answer will not change; the fix is upstream of the call.

A failure can carry data too

On the two charging endpoints, a refusal returns data alongside the four fields, because the attempt produced a record worth having even though it did not succeed:

{ "requestId": "01JB2K9WQ4X7YH3M8N5P6R7T8V", "code": "INSUFFICIENT_BALANCE",
"message": "The subscriber has insufficient balance. Nothing was charged.",
"retryable": false,
"data": { "purchaseId": "01JB…", "amount": 500, "currency": "IQD", "chargedAt": null } }

Use it for reconciliation. It is the same shape the success case returns, so an SP that stores data on both outcomes has one record per attempt rather than a gap wherever a charge failed.

data is absent on every other refusal. Read it defensively: its presence depends on the endpoint and the outcome, so treat a missing data as normal rather than as a malformed response.

Three rules that save a rewrite

1. Branch on code, never on message or on the HTTP status alone. The status is derived from the code, not the other way round: several distinct codes share a status, and telling them apart is usually the whole point.

2. Treat an unknown code as a failure, not a success. New codes can appear. A client that falls through to "assume it worked" on an unrecognised code will one day charge nothing and tell its user everything is fine. Default to refusing.

3. retryable: false means stop. Retrying an identical non-retryable request wastes your budget, ours, and (where a message is involved) the subscriber's patience. Only five codes in the entire API are retryable; see Error codes.

HTTP status

The status follows the code, and is there so that generic tooling behaves sensibly. It is not the contract. Two things worth knowing:

  • A 2xx always means the operation succeeded. There is no "200 with an error inside".
  • A 4xx is your side, a 5xx is ours, and of the 5xx responses, both are explicitly retryable.