Concepts
Errors
One error shape everywhere, a stable category, and a specific code.
Every error response has the same shape:
{
"error": {
"type": "payment_required",
"code": "insufficient_funds",
"message": "The wallet is short $4.50 for this purchase.",
"request_id": "req_4b1c0d8e9f2a3b4c5d6e7f8091a2b3c4",
"shortfall": { "amount": 450, "currency": "USD" }
}
}typeis a stable category, safe to branch on.codeis the specific reason, stable within its type. This is what you handle by name.messageis for humans. Do not parse it; it may be reworded at any time.request_idalso appears as theRequest-Idheader. Log it.
Some codes carry extra fields. insufficient_funds carries shortfall — exactly how
much more the wallet needs, so you can ask for that amount rather than guessing.
Rate-limit errors carry retry_after in seconds, matching the Retry-After header.
Types
type | Status | Meaning |
|---|---|---|
invalid_request_error | 400 | The request is malformed or a parameter is unusable. |
authentication_error | 401 | Missing or unrecognised API key. |
payment_required | 402 | The wallet cannot cover it. Nothing was charged. |
permission_error | 403 | The key is valid but not allowed to do this. |
not_found | 404 | No such resource in this account and mode. |
conflict | 409 | The request contradicts existing state. |
unprocessable | 422 | Well-formed, but semantically wrong. |
rate_limit_error | 429 | Too many requests. Back off by retry_after. |
api_error | 5xx | A fault on Tizon's side. |
upstream_error | 5xx | A provider failed in a way Tizon could not absorb. |
Codes worth handling by name
code | When | What to do |
|---|---|---|
insufficient_funds | A wallet quote exceeds available_to_spend | Read shortfall, fund that much, retry with the same Idempotency-Key. |
account_overdue | An open debit is past its net terms | The overdraft is frozen. Pay down before buying on credit again. See overdraft. |
offer_unavailable | The provider withdrew the offer | Refresh the catalog; the offer is still readable for old orders. |
missing_destination | A mobile_topup offer quoted without destination | Send the phone number in E.164 form. |
invalid_msisdn | destination is malformed | Fix the number; + then country code, no spaces. |
idempotency_key_reuse_mismatch | A key was reused with a different body | Use a fresh key per logical operation. See idempotency. |
test_mode_only | A test helper called with a live key | Sandbox tools never touch live money. |
invalid_webhook_url | Endpoint URL is not public HTTPS | localhost and private ranges are refused. Use a tunnel. |
webhook_url_in_use | That URL is already registered | Reuse the existing endpoint or delete it first. |
Failed orders are not errors
An order that fails fulfillment returns 200 with status: "failed" and a
failure_reason — not an HTTP error. The price is already back in the wallet. You are
never charged for an eSIM that was not delivered, so treat a failed order as a refund that
already happened, not as a debt to chase.