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
2xxalways means the operation succeeded. There is no "200 with an error inside". - A
4xxis your side, a5xxis ours, and of the5xxresponses, both are explicitly retryable.