Skip to content

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.

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.

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.
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.

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.

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.
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.

429 → wait Retry-After, then retry
503, 504 → exponential backoff with jitter, cap the attempts
500 → same, and report the request_id
partner_timeout → do not blind-retry: read the transaction first
every other 4xx → do not retry; fix the request or the account

On every retry of a state-changing call, send the original Idempotency-Key. That single rule is what makes an aggressive retry policy safe here.