Errors
Errors are RFC 9457 Problem Details. Framework validation failures use the same shape, so there is one format to parse, not two.
{ "type": "https://docs.paysigna.com/api/errors/#limit_exceeded", "title": "Limit exceeded", "status": 409, "detail": "This charge would exceed your daily collection limit of GHS 50,000.", "code": "limit_exceeded", "request_id": "01JB3K2N8QZX4V7M9P0R5T6W8Y"}Branch on code, never on detail or title. The codes are a closed set,
defined as constants, and treated as a published contract — changing one is a
breaking API change on our side. detail is written for a human and may be
reworded at any time.
request_id is also on the X-Request-Id response header of every request,
successful or not. Log it. It is the one string that lets us find your exact call.
Authentication and authorisation
Section titled “Authentication and authorisation”| Code | Status | Meaning |
|---|---|---|
unauthenticated |
401 | No credential, or unparseable. Also returned for a wrong secret, on purpose. |
api_key_revoked |
403 | Valid once. Rotate. |
api_key_expired |
403 | The key’s expiry has passed. |
ip_not_allowed |
403 | Source address not on this key’s allowlist. |
insufficient_scope |
403 | Right key, missing scope. |
forbidden |
403 | In scope, but not for this resource. |
environment_mismatch |
403 | Test key against live, or the reverse. |
See Authentication for what to do about each.
Request validity
Section titled “Request validity”| Code | Status | Meaning |
|---|---|---|
invalid_request |
400 | Malformed request. |
missing_field |
400 | A required field was absent. |
invalid_field |
400 / 422 | A field was present and unusable. |
invalid_currency |
422 | Not a currency we support, or not one you are enabled for. |
not_found |
404 | No such resource — or not one you can see. |
conflict |
409 | The request contradicts the resource’s current state. |
unsupported |
422 | A valid request for something this account or channel cannot do. |
idempotency_key_reused |
409 | Same key, different body. See Idempotency. |
request_in_progress |
409 | A concurrent request holds this key. Retry shortly. |
Your account’s state
Section titled “Your account’s state”| Code | Status | Meaning |
|---|---|---|
merchant_suspended |
403 | The account is suspended. Contact support. |
merchant_not_live |
403 | Live credentials used before the account was activated. |
kyc_required |
403 | Verification outstanding. See Going live. |
capability_disabled |
403 | This channel or feature is not enabled for you. |
api_integration_not_enabled |
403 | API access is a separate permission; turn it on in the portal. |
settlement_destination_missing |
403 | No settlement account on file, so money would have nowhere to go. |
None of these are retryable. Every one needs an action in the portal or a conversation with us.
Money and limits
Section titled “Money and limits”| Code | Status | Meaning |
|---|---|---|
amount_too_small |
422 | Below the minimum for this channel. |
amount_too_large |
422 | Above the maximum for this channel or your account. |
limit_exceeded |
409 | A velocity or volume limit on your account. |
insufficient_funds |
402 | The customer’s wallet, or your balance on a payout. |
unpriced_combination |
409 | No fee is configured for this merchant and channel. Our configuration gap — tell us. |
margin_floor_breached |
409 | The transaction would price below the permitted floor. |
channel_prohibited |
403 | This channel is not permitted for this transaction. |
refund_exceeds_original |
422 | Refunds would total more than the charge. |
| Code | Status | Meaning |
|---|---|---|
risk_blocked |
403 | A risk rule refused it. |
counterparty_blocklisted |
403 | The counterparty is blocklisted. |
manual_review_pending |
403 | Held for a human decision. |
manual_review_pending is not a failure. The transaction exists, in a
non-terminal under_review status, and will move to a terminal status when the
review completes — you will get a webhook. Do not retry it, and do not tell the
customer it failed.
Routing and partners
Section titled “Routing and partners”| Code | Status | Retry | Meaning |
|---|---|---|---|
no_route_available |
503 | Yes, with backoff | Nothing is configured to carry this, or everything that is has been taken out of service. Our problem, which is why it is not a 4xx. |
partner_rejected |
409 | No | The partner refused it. A terminal answer about this attempt. |
partner_unavailable |
503 | Yes, with backoff | The partner is down or unreachable. |
partner_timeout |
504 | Carefully | We did not hear back in time. |
Rate limiting
Section titled “Rate limiting”| Code | Status | Meaning |
|---|---|---|
rate_limited |
429 | Too many requests. |
A 429 carries Retry-After, and every response carries X-RateLimit-Limit and
X-RateLimit-Remaining. Honour Retry-After rather than retrying on a fixed
timer — a tight retry loop against a rate limit extends the limit rather than
clearing it.
The usual cause of an unexpected 429 is polling every open transaction on a
timer. Build on webhooks and read individual transactions on demand.
| Code | Status | Retry | Meaning |
|---|---|---|---|
internal_error |
500 | Yes, with backoff and the same key | Something broke on our side. |
service_unavailable |
503 | Yes, with backoff | We are refusing work, usually briefly. |
Send us the request_id for any internal_error. It is logged on our side against
the full cause.
A retry policy that works
Section titled “A retry policy that works”429 → wait Retry-After, then retry503, 504 → exponential backoff with jitter, cap the attempts500 → same, and report the request_idpartner_timeout → do not blind-retry: read the transaction firstevery other 4xx → do not retry; fix the request or the accountOn every retry of a state-changing call, send the original Idempotency-Key.
That single rule is what makes an aggressive retry policy safe here.

