Reference
Errors and codes
Two kinds of codes, and they answer different questions. A request error says why the gateway refused what you sent: it comes back at once, with an HTTP status. An outcome code says why an operation it accepted did not succeed: a declined payment, a biller that refused a bill. It arrives later, in the resource's failure_code and in its webhook, while the request itself got a 2xx. 134 request codes and 51 outcome codes are listed below.
How errors look
Every request error is an RFC 9457 problem document (application/problem+json) with a stable, machine-readable code. Branch on the code, never on the detail: details are for people and may change. Two exceptions keep the format their standard sets: POST /oauth/token answers in OAuth 2.0's (RFC 6749), and the signature check answers {"error": code}.
An error · application/problem+json
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"code": "limit_exceeded",
"detail": "The amount is over this PSP's per-transaction limit.",
"rule": "max_amount"
}Until the old envelope's sunset
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"code": "insufficient_balance",
"detail": "the balance (10.00 MZN) does not cover 255.00 MZN",
"error": { // the old envelope, until its sunset
"code": "insufficient_balance",
"message": "the balance (10.00 MZN) does not cover 255.00 MZN"
}
}A validation problem lists each field that failed in errors[], with its own field code. Every response carries X-Request-ID: quote it to support.
Handling them
| Retry | What it means |
|---|---|
| Fix the request | The same request fails again. Change it (the body, the key's scope, the amount) or ask the person for something else. |
| Retry as is | Transient. Retry the same request with the same Idempotency-Key, with exponential backoff: it can never act twice. |
| Retry later | Wait the number of seconds in Retry-After (or back off), then retry. |
| After an action | Retry once something else has happened: the payer's consent, a second person's approval, an endpoint enabled, KYB approved. |
A 5xx without a specific code is ours. Money-moving calls are safe to retry with the same Idempotency-Key; if a payment's state is unclear, read it (GET /v1/payments/{id}) or wait for its webhook rather than charging again.
Authentication and access
API keys, scopes, signatures, client certificates, CSRF.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
already_rolledKey already rolled problem |
409 | POST /v1/api-keys/{id}/roll was called on a key that has already been rolled or revoked, possibly by a concurrent request. | List the account's keys and roll the current one instead. | Fix the request |
client_certificate_requiredClient certificate required problem |
401, 409 | 401: the account requires mTLS and the request did not present a valid client certificate issued to it. 409: PUT /v1/mtls {required: true} was called without presenting such a certificate, which would lock the caller out. | Call with a valid client certificate issued to the account (issue one via POST /v1/client-certificates). | Fix the request |
csrf_invalidCSRF token invalid problem |
403 | A state-changing form post on the hosted checkout, its test toolbar or (when CSRF is enabled) the operator console carried no CSRF token, a token that does not match the session cookie, or a token this gateway did not issue. | Reload the page to get a fresh token and submit the form again. | Fix the request |
csrf_missingCSRF cookie missing problem |
403 | A state-changing form post on the hosted checkout, its test toolbar or the operator console arrived without the CSRF cookie that the page render sets. | Reload the page so the cookie is set, then submit again. | Fix the request |
customer_not_allowedCustomer not allowed for this key problem |
403 | The key is restricted to a subset of customers (organisation or user ids) and the charge being created, or the payment being acted on, belongs to a customer outside that subset. | Use a key whose customer restriction includes this customer. | Fix the request |
insufficient_scopeInsufficient scope problem |
403 | The API key lacks the scope the endpoint requires (the body names it in missing_scope), or the request named another account_id without the accounts:write scope. | Use a key that has the named scope, or omit account_id to act on your own account. | After an action |
invalid_api_keyInvalid API key problem |
401 | The bearer token has the KulPay key prefix but is malformed or its checksum does not verify (mistyped, truncated or invented); it was refused before any lookup. | Copy the full key again from where it was issued. | Fix the request |
invalid_clientOAuth client not recognised OAuth (RFC 6749) |
401 | POST /oauth/token: no client authentication, a wrong client secret, a client that is revoked or whose account is suspended, or a private-key JWT assertion that does not verify (wrong audience, expiry more than five minutes away, no jti, or a jti already used). Answered with WWW-Authenticate, in the OAuth 2.0 format {"error", "error_description"} (RFC 6749 §5.2). | Check the client id and secret, or the assertion: signed with the registered key, aud set to the token endpoint, a fresh jti, exp within five minutes. | Fix the request |
invalid_scopeOAuth scope not allowed OAuth (RFC 6749) |
400 | POST /oauth/token asked for a scope the client was not granted. | Ask only for scopes the client holds; an operator grants more. | Fix the request |
invalid_signatureInvalid signature problem |
401 | A GitHub secret-scanning report sent to POST /secret-scanning/github has a signature that does not verify against GitHub's public key. | Nothing for integrators; the report is ignored. GitHub should resend a correctly signed report. | Fix the request |
ip_not_allowedIP not allowed problem |
403 | The request came from an IP address outside the API key's IP allowlist, or outside the management-surface allowlist (mgmt.ip_allowlist), or the client IP could not be parsed. | Call from an allowed address, or have the allowlist updated (note localhost may be ::1). | After an action |
key_expiredKey expired problem |
401 | The API key is valid but past its expires_at. | Issue or rotate to a new key and use it. | After an action |
signature_invalidWebhook signature does not verify {"error": code} |
401 | POST /v1/admin/webhooks/verify: the KulPay-Signature header is malformed (no t= or v1=), or no configured secret produces its v1 over this body. The body is {"error": "signature_invalid", "message", "t"}. | Verify over the raw body exactly as received (before any JSON parsing), with the endpoint's current secret; during a secret roll, try both. | Fix the request |
signature_timestamp_outside_toleranceWebhook signature too old {"error": code} |
401 | POST /v1/admin/webhooks/verify: the signature's t= is further from the gateway's clock than the tolerance allows. Either the clock is wrong or the delivery is being replayed. | Check the receiving server's clock (NTP); never accept an old delivery as new. The t field in the body is the timestamp that was signed. | Fix the request |
temporarily_unavailableToken service unavailable OAuth (RFC 6749) |
503 | POST /oauth/token on a gateway running without its database, which cannot issue tokens. | Retry later; an operator must restore the gateway's store. | Retry later |
unauthorizedUnauthorized problem |
401 | The request has no Authorization: Bearer header, or the bearer token is not a known API key or token. A WWW-Authenticate header is set when the header is missing. | Send a valid API key as Authorization: Bearer <key>. | Fix the request |
unsupported_grant_typeOAuth grant type not supported OAuth (RFC 6749) |
400 | POST /oauth/token with a grant_type other than client_credentials. | Use grant_type=client_credentials. | Fix the request |
wrong_environmentWrong environment problem |
401 | A test key was used against live data or a live key against test data: the key's prefix does not match its environment, a ?mode= query asks for the other mode, or the payment being changed was created in the other mode. | Use the key of the environment (test or live) that owns the data, and drop a conflicting ?mode=. | Fix the request |
Malformed requests
The body, the parameters, the content type or the size.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
invalid_amountInvalid amount problem |
400 | POST /v1/payments was sent with an amount that is zero or negative. | Send a positive amount. | Fix the request |
invalid_callback_urlInvalid callback URL problem |
400 | POST /v1/payments was sent with a callback_url that does not start with http:// or https://. | Send an absolute HTTP(S) URL. | Fix the request |
invalid_currencyInvalid currency problem |
400 | POST /v1/payments was sent with a currency that is not 3 upper-case letters. | Send an ISO 4217 code such as MZN. | Fix the request |
invalid_requestInvalid request problem |
400 | The request is malformed or refused on its merits: unparseable body, a missing or out-of-range field or query parameter, or a rule the service checks (bills, billers, partners, events, statements, settlements, audit, keys and more). The message names the problem. | Fix the request as the message or detail says. | Fix the request |
invalid_return_urlInvalid return URL problem |
400 | POST /v1/payments was sent with a return_url that neither starts with http nor with /. | Send an absolute URL or a path starting with /. | Fix the request |
metadata_too_largeToo many metadata keys problem |
400 | POST /v1/payments was sent with more than 50 metadata keys. | Send at most 50 metadata keys. | Fix the request |
metadata_value_too_largeMetadata key or value too long problem |
400 | POST /v1/payments was sent with a metadata key longer than 100 characters or a value longer than 500. | Shorten the key or value. | Fix the request |
not_foundNot found problem |
404 | The resource named in the path or body does not exist, or exists but is not visible to the caller's account or mode (tenancy answers 404, not 403). Used across most surfaces: payments, transactions, payouts, bills, billers, PSP applications, approvals, journals, callbacks, settlements, reports, keys, audit tenants and more. | Check the id and that you are using a key of the account and environment (test/live) that owns the resource. | Fix the request |
Idempotency
The Idempotency-Key header that makes a retry safe.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
idempotency_key_requiredIdempotency key required problem |
400 | A request that moves money (for example POST /charges, /refunds, /payouts, /v1/payouts, /v1/bill-payments, /v1/psp/transactions, /v1/payments/{id}/refund, /v1/payments/{id}/cancel, /v1/cash-deposits) was sent without an Idempotency-Key header. | Send a unique Idempotency-Key header and reuse it only when retrying the same request. | Fix the request |
idempotency_key_reusedIdempotency key reused problem |
409 | The Idempotency-Key was already used in this account and mode for a different request (another method/path or a different body). | Use a new Idempotency-Key for a new or changed request; reuse a key only to retry the identical request. | Fix the request |
idempotency_unavailableIdempotency check unavailable problem |
503 | The idempotency store could not be reached, so the request was not run. | Retry after Retry-After (1 s) with the same Idempotency-Key. | Retry as is |
invalid_idempotency_keyInvalid idempotency key problem |
400 | The Idempotency-Key header is longer than 255 bytes. | Use a key of at most 255 bytes, such as a UUID. | Fix the request |
request_in_progressRequest in progress problem |
409 | A request with the same Idempotency-Key is still being processed and did not finish within the wait window. Retry-After: 1 is set. | Wait a moment and retry with the same Idempotency-Key and body; you will get the stored answer once the first request finishes. | Retry later |
Payments and refunds
Charges, hosted checkout, captures, refunds, risk and routing.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
lookup_usedLookup already used problem |
409 | POST /v1/cash-deposits/intents was called with a lookup that already started a deposit of another amount or currency: a lookup authorises one deposit. | Look the reference up again (POST /v1/name-lookups) and start the new deposit from the new lookup_id; the same amount again answers the intent already started. | Fix the request |
account_suspendedAccount suspended problem |
403 | The account the request acts on is suspended, so it cannot transact. Returned by the account, key, OAuth, connector and service-resource endpoints that share the generic service error mapper. | Contact the platform operator to reactivate the account. | After an action |
amount_exceeds_remainingRefund exceeds remaining amount problem |
400, 422 | POST /v1/payments/{id}/refund asked for more than the payment's amount minus what has already been refunded. | Refund at most the remaining amount, or omit amount to refund all of it. | Fix the request |
amount_too_highAmount above the method's maximum problem |
400 | POST /charges with an amount over the maximum the method accepts in one transaction; the detail names the maximum. | Split the payment, or offer a method with a higher limit (a card or a bank transfer). | Fix the request |
amount_too_lowAmount below the method's minimum problem |
400 | POST /charges with an amount under the minimum the method accepts; the detail names the minimum. | Raise the amount to the minimum, or offer another method. | Fix the request |
deposit_refusedDeposit refused problem |
422 | A cash deposit intent was refused on its merits when created or confirmed, for example the reference lookup has expired or the amount is outside the reference's limits. The message gives the reason. | Fix what the message names (look the reference up again, change the amount), then retry with a new Idempotency-Key. | Fix the request |
duplicate_originator_referenceOriginator reference already used problem |
409 | POST /v1/psp/transactions used an originator_reference that this originator has already submitted. | Fetch the existing transaction with GET /v1/psp/transactions?originator_reference=, or use a new reference. | Fix the request |
invalid_cardInvalid card number problem |
400 | A card number that is not 13 to 19 digits or fails the Luhn check (test mode, documented test cards only). | Use one of the documented test card numbers; in live mode send the acquirer's card token. | Fix the request |
invalid_card_tokenInvalid card token problem |
400 | The card token is not a valid reference from the tokenizer. | Send the token exactly as the acquirer's hosted fields returned it; tokens are single use where the acquirer says so. | Fix the request |
invalid_otpInvalid OTP problem |
422 | POST /charges/{id}/complete-action on a payment in an OTP flow was sent a code that does not match. | Ask the customer to re-enter the code and submit it again. | Fix the request |
invalid_phoneInvalid phone number problem |
400 | POST /charges with a phone number that does not match the pattern configured for the method (each mobile-money network has its own prefixes). | Ask the payer to check the number, or offer the method their number belongs to. | Fix the request |
invalid_stateInvalid state problem |
409 | The object is not in a state that allows the action: cancelling, confirming, rejecting or refunding a payment that is no longer pending (or not succeeded for a refund), changing manual-payment evidence after it left pending, cancelling a payout not pending/in transit, approving or rejecting a payout that is not waiting for approval, replaying an already replayed dead letter, a projection replay that did not match, or releasing a payout batch that is not closed (already released or sent) or whose release already waits for approval. | Fetch the object, check its current status, and do not repeat the action. | Fix the request |
limit_exceededLimit exceeded problem |
422 | The amount breaks a limit: the API key's currency, per-transaction or daily ceiling, or a routing rule's minimum/maximum amount (charges, payouts, PSP transactions; the problem names the rule and a rejection_id), or a bill product's daily limit per reference or per channel (bill quotes and payments), or the bank rail's daily limit per beneficiary NIB (bank payouts, rule beneficiary_daily) or per debit account (batch releases, rule debit_daily). | Lower the amount or use another currency/key as the detail says, or wait for the daily limit to reset. | Fix the request |
live_paymentTest action on a live payment problem |
403 | A hosted-checkout test action (confirm, decline, cancel, set metadata) was called on a live-mode payment. | Use test actions only on test-mode payments. | Fix the request |
missing_otpMissing OTP problem |
400 | POST /charges/{id}/complete-action was called for a charge using the OTP flow, but the body had no otp. | Send the code the customer received in the otp field. | Fix the request |
missing_phonePhone number required problem |
400 | POST /charges with a mobile-money source and no phone number. | Send source.mobile_wallet.phone_number in international format, e.g. +258841234567. | Fix the request |
no_connector_bindingPayment method not enabled for this account problem |
409 | The account has no connector (acquiring credentials) bound for the method, reported through the account/management service error mapping. | Ask the operator to bind a connector for this method to your account. | After an action |
no_routeNo route problem |
503 | No PSP is available for this transaction right now (all eligible candidates are unavailable), so it was refused before any provider call. Returned by charges, payouts, PSP transactions, name lookups and reversals; the problem carries rule availability and a rejection_id. | Retry after Retry-After (30 s) with the same Idempotency-Key. | Retry later |
not_a_partnerNot a partner bank or agent problem |
403 | A cash-deposit intent call (/v1/cash-deposits/intents...) was made with a key whose account is not a partner bank or agent (no PSP record for the account). | Use the key of an onboarded partner bank or agent account. | After an action |
not_receiptablePayment has no receipt yet problem |
409 | A receipt was requested (GET /v1/payments/{id}/receipt or /checkout/{id}/receipt) for a payment whose status does not have a receipt; a receipt is issued only once money has moved. | Poll GET /payments/{id} and request the receipt once the payment has succeeded. | After an action |
not_refundableNot refundable problem |
422 | A refund submitted through POST /v1/psp/transactions was refused because the original transaction cannot be refunded, or the amount is more than what is left to refund. | Check the original transaction's status and remaining refundable amount; lower the amount or do not refund. | Fix the request |
not_supportedNot supported problem |
422 | The request was refused before any provider call because the transaction type, method, currency or country is not enabled in the capability matrix of any eligible PSP, or the chosen provider cannot do the operation (name lookup, payout, reversal), or no connector is bound for the method, or the method is simulated and the request is live (rule method_not_live). Returned by payment creation, PSP transactions, payouts, name lookups and reversals. | Use a type, method, currency and country that is enabled for your account (the rule and detail fields name the failing check), or ask KulPay to enable it. | Fix the request |
pilot_limitPilot limit problem |
422 | The PSP is in pilot and the live transaction breaks a pilot rule, named in the rule member: settings_missing (pilot settings not set up), merchant_not_allowed (metadata.merchant_id missing or not on the allow-list; payments only), currency (not the pilot currency), per_transaction, daily or monthly (the limit would be exceeded). Daily and monthly limits count payments, payouts, bill payments and deposit intents together. Returned by POST /charges, POST /v1/psp/transactions, POST /v1/payouts, POST /v1/bill-payments and POST /v1/cash-deposits/intents, before any provider or supplier call. | Stay within the pilot setting named in rule and the detail, or ask the operator to change the pilot settings or complete go-live. | After an action |
raw_card_data_not_acceptedCard numbers are not accepted problem |
400 | A raw card number was sent where a token is required: any card number in live mode, an undocumented one in test mode, or a test card on a live payment. KulPay never takes card numbers, so the merchant stays out of PCI DSS scope. | Collect the card in the acquirer's hosted fields and send the token it returns. | Fix the request |
risk_declinedDeclined by risk rules problem |
422 | The payment, PSP transaction or payout was declined by the risk stage before any provider call; the rules that fired are kept on the record, not returned. The body carries rule "risk" and a rejection_id. | Do not retry the same transaction; if you believe it is legitimate, contact support with the rejection_id. | Fix the request |
unsupported_currencyCurrency not supported by the method problem |
400 | POST /charges in a currency the chosen method is not configured to take. | Charge in a currency the method supports; GET /methods lists them. | Fix the request |
Payouts
Sending money to a wallet or a bank account.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
beneficiary_name_requiredBeneficiary name required problem |
400 | POST /v1/payouts with method bank: beneficiary.name, the account holder's name the bank transfer carries, is missing. Nothing was debited. | Send beneficiary.name with the account holder's name. | Fix the request |
invalid_accountInvalid account problem |
400 | POST /v1/payouts with method bank: beneficiary.account is not a valid NIB (21 digits whose ISO 7064 MOD 97-10 check digits verify). Nothing was debited. | Correct the beneficiary's NIB (spaces, dots and dashes are ignored) and send the payout again. | Fix the request |
insufficient_balanceInsufficient balance problem |
422 | A balance KulPay holds for you does not cover the operation: your available balance for a payout, or a channel's float for a bill payment. Not to be confused with insufficient_funds, a payment failure_code for the payer's own wallet. | Fund the balance (or lower the amount), then send the request again. | After an action |
invalid_methodInvalid payout method problem |
400 | POST /payouts used a destination.type that is not supported. Valid types are mobile_wallet, bank_transfer, manual, crypto and cheque. | Use one of the listed destination types. | Fix the request |
upstream_errorProvider error problem |
502 | POST /v1/name-lookups reached the provider but it could not answer the beneficiary lookup. | Retry the lookup later. | Retry later |
Selling bills (channels)
Billers, lookups, quotes and bill payments.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
amount_out_of_rangeAmount not accepted problem |
422 | The amount on a bill quote or payment is not one the product or reference accepts: it differs from a fixed price or offer price, is not one of the allowed denominations, is outside the product's min/max or the reference's exact/min/max, nothing is due on the reference, or, for an invoice, it is below the open amount when partial payments are not allowed or above it when overpayment is not allowed. The message states the accepted amount or range. | Use the amount the message names, or look the reference up first to get the amount due. | Fix the request |
biller_unavailableBiller unavailable problem |
503, 422 | The biller or its supplier cannot take this payment. 503 with Retry-After: 60 when the supplier has a passing outage (for example a lookup timed out or the supplier is down). 422 when the biller is not accepting payments, the supplier of a quote is no longer configured, or (operator reconciliation) the supplier's transaction listing failed. | On 503 retry after the Retry-After delay; on 422 choose another biller or product, or contact the operator. | Retry later |
consent_requiredPayer consent required problem |
422 | A payer subscription (POST /v1/payers/{id}/subscriptions) was created without consent.accepted = true and the version of the terms the payer accepted (at most 60 characters). | Collect the payer's consent and send consent.accepted with the terms version. | After an action |
fee_bearer_conflictFee and discount conflict problem |
422 | A bill product was configured with contradictory fees: an agent-model product with a supplier discount, a payer-borne fee together with a biller discount, or a principal-model, biller-borne product with fees besides the discount. | Set either the fee or the discount as the message says, never both. | Fix the request |
invoice_closedInvoice closed problem |
409 | A quote was requested for an invoice that is no longer open or partially paid (POST /v1/bill-payments/quotes), or a biller tried to attach a document to a cancelled or expired invoice (PUT /v1/biller/invoices/{number}/document). | Look the invoice up again; it cannot be paid or changed in its current status. | Fix the request |
quote_expiredQuote expired problem |
422 | POST /v1/bill-payments was called with a quote whose validity has passed; the quote is marked expired. | Request a new quote (POST /v1/bill-payments/quotes) and pay that one. | Fix the request |
quote_usedQuote already used problem |
409 | POST /v1/bill-payments was called with a quote that is no longer in quoted state: it was already paid or is being collected/paid, or a concurrent request took it first. | Fetch the existing bill payment for this quote; to pay again, request a new quote. | Fix the request |
reference_not_foundReference not found problem |
422 | The biller has no customer/account for the reference: on a quote for a product that requires a lookup, or when a biller's payer is subscribed with an account_reference the biller does not know. | Check the reference (meter, account or contract number) with the payer and try again. | Fix the request |
reference_rejectedReference rejected by supplier problem |
422 | The bill supplier refused the lookup or quote for this reference (the message carries the supplier's reason, or "the supplier refused the request"). | Read the supplier's reason, correct the reference or amount, and try again. | Fix the request |
resend_limitResend limit reached problem |
422 | POST /v1/bill-payments/{id}/resend was refused because the payment's tokens/receipt were already sent again 3 times. | Do not resend; give the payer the receipt link or contact support. | Fix the request |
sms_unavailableSMS not available problem |
503 | POST /v1/bill-payments/{id}/resend asked for via "sms" on a gateway that has no SMS channel configured. | Resend by email instead, or ask the operator to configure SMS. | Fix the request |
Billers' own API
Invoices, enrolled payers and remittances.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
invoice_lockedInvoice locked problem |
409 | PATCH /v1/biller/invoices/{number} tried to change an invoice that is no longer open or already has a payment. | Leave the invoice as is; issue a new invoice if the amount owed changed. | Fix the request |
owned_by_originOwned by its origin problem |
409 | The request changes a commercial field (name, category, logo, credit account) of a biller onboarded in KulOffice, or retires it: KulOffice owns those, and KulPay takes them from its events (IO-3.4). | Make the change in KulOffice; it reaches KulPay with its event. | Fix the request |
not_a_billerNot a biller problem |
403 | The API key's organisation is not registered as a biller on KulPay, so it cannot use the biller API (invoices, payers, subscriptions, remittances). | Use a key that belongs to a biller organisation, or ask KulPay to register the organisation as a biller. | After an action |
Partners
PSP and biller onboarding, KYB, the portal, certification and go-live.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
option_lockedOption locked problem |
409 | POST /v1/name-lookups/{lookup_id}/select chose another option of an ambiguous reference after a deposit already started from the lookup (IO-1.6). | Keep the option the deposit started with; for another destination, look the reference up again and start a new deposit. | Fix the request |
already_submittedApplication already submitted problem |
409 | A PSP onboarding application was submitted, or changed in a way that requires it to be a draft, after it had already been submitted. | Read the application's status; it is already in review. | Fix the request |
application_incompleteApplication incomplete problem |
422 | A PSP onboarding application was submitted with required fields missing. The error object carries an extra `missing` array listing them. | Fill in the fields listed in error.missing, then submit again. | Fix the request |
certification_runningCertification already running problem |
409 | A certification run was started for a PSP while another run for it is still in progress. | Wait for the current run to finish, then start a new one. | Retry later |
document_tamperedKYB document altered problem |
409 | A stored KYB document was downloaded and its content no longer matches the checksum recorded at upload. | Treat the document as compromised: ask the partner to upload it again and report the incident. | After an action |
invalid_logoInvalid logo problem |
400 | A PSP logo upload (PUT /v1/admin/psps/{id}/logo) or logo review (POST /v1/admin/psps/{id}/logo/review) was refused on its merits; the message gives the reason. | Fix the file or review request as the message says. | Fix the request |
invalid_transitionInvalid transition problem |
409 | A PSP lifecycle change (e.g. POST /v1/admin/psps/{id}/transitions, and other PSP operations that move its state) is not allowed from the PSP's current state; the body adds from, to and cause. | Complete what cause names (for example KYB or certification) or choose a transition allowed from the current state. | After an action |
config_kind_retiredConfiguration kind retired problem |
410 | A new version, a rollback or the approval of a pending version was requested for a retired PSP configuration kind (routing, retired by F1-5: the router never read it). Its versions stay readable as history. The body names kind and successor, and the Link header (rel=successor-version) points to where the setting lives now: the global routing rules, POST /v1/routing/versions. | Configure the setting at successor (for routing, prepare a version of the global routing rules naming the PSP); reject a pending version of the retired kind. | After an action |
live_keys_not_allowedLive keys not allowed yet problem |
409 | A live API key was requested for a PSP that has not reached pilot; until then it may only hold test keys. | Use a test key until the PSP reaches pilot. | After an action |
not_submittedApplication not submitted problem |
409 | A KYB document was uploaded (POST /v1/psps/applications/{id}/documents) to a PSP application that has not been submitted yet. | Submit the application first (POST /v1/psps/applications/{id}/submit), then upload the documents. | After an action |
sandbox_unavailableSandbox unavailable problem |
409 | A PSP sandbox operation was refused: the sandbox opens only when the PSP enters technical_setup, or the sandbox key was already revealed once. | Wait until the PSP is in technical_setup, or issue a new test key instead of revealing the old one. | After an action |
Webhooks
Endpoints, deliveries and replays.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
account_requiredKey not bound to an account problem |
403 | GET /v1/events was called with a key that is not bound to any account, so there is no event log to read. | Call with a key issued to an account. | After an action |
endpoint_disabledWebhook endpoint disabled problem |
409 | A replay or a test event was requested for a webhook endpoint that is disabled. | Enable the endpoint (POST /v1/webhook-endpoints/{id}/enable), then replay or test again. | After an action |
missing_signatureMissing signature problem |
401 | POST /v1/admin/webhooks/verify (the signature-checking helper) was called without a KulPay-Signature (or X-KulPay-Signature) header. | Send the KulPay-Signature header your signer produced. | Fix the request |
no_secret_configuredNo webhook secret configured problem |
503 | POST /v1/admin/webhooks/verify was called on a gateway that has no webhook signing secret configured (neither webhook.secrets nor the legacy webhook.secret), so it cannot check a signature. | Operator: configure a webhook signing secret on the gateway, then call the endpoint again. | After an action |
no_urlNo webhook URL problem |
400 | POST /v1/admin/webhooks/test was called without a ?url= query parameter and the gateway has no webhook.url configured, so there is nowhere to send the test event. | Pass ?url= with the receiver URL, or set webhook.url in the gateway configuration. | Fix the request |
Files
Uploads and downloads.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
file_too_largeFile too large problem |
413, 422 | An uploaded file exceeds its size limit: a /v1/files upload, a payment attachment, a pushed or uploaded statement, an invoice file, an invoice document or a supplier report (5 MB for the bills files). | Upload a smaller file, or split it. | Fix the request |
missing_fileMissing file problem |
400 | A multipart upload had no file part: POST /files, or POST /v1/payments/{id}/attachments. | Send the file in a multipart part named file. | Fix the request |
too_many_attachmentsToo many attachments problem |
409 | POST /v1/payments/{id}/attachments was refused because the payment already has the maximum of 8 attachments. | Delete an attachment you no longer need before uploading another. | After an action |
unsupported_content_typeUnsupported file type problem |
415 | POST /v1/payments/{id}/attachments: the uploaded file's Content-Type is not in the list allowed for the payment's method. | Upload a file of an allowed type (the message names the type that was refused). | Fix the request |
Reconciliation and settlement
Statements, matching, settlements, the ledger and reports.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
callback_rejectedStatement push rejected problem |
401 | A PSP pushed a statement to POST /callbacks/{psp_id}/{binding_id}/statements/{profile_id} and the request failed the binding's verification (signature, token or source IP). | Sign or authenticate the push as the binding requires, from an allowed address. | Fix the request |
not_readyPeriod not ready to close problem |
409 | POST /v1/admin/reconciliation/periods/{period}/close was refused: the month has not ended, it is already closed, a close is already waiting for sign-off, the period is not YYYY-MM, or the close checklist does not pass. The body carries the checklist next to the error when it was computed. | Read the message and checklist, resolve or carry forward the open items (or wait for the month to end / the pending sign-off), then request the close again. | After an action |
not_reversibleJournal not reversible problem |
409 | POST /v1/admin/ledger/journals/{id}/reverse was refused: the journal is itself a reversal, it was already reversed, or no reason was given. | Reverse only an original journal that has not been reversed, and always send a reason; to undo a reversal, post the movement again. | Fix the request |
not_submittableRegulatory run not submittable problem |
409 | POST /v1/admin/regulatory/runs/{id}/submit was refused because the run is blocked (the message gives the blocking detail) or was already submitted. | Fix the cause in the blocking detail and generate a new run, or do nothing if it was already submitted. | After an action |
sample_rejectedStatement sample rejected problem |
422 | POST /v1/psps/{id}/statement-profiles/preview could not parse the sample statement with the given profile; the message says why. | Fix the profile (columns, formats) or the sample file and preview again. | Fix the request |
statement_rejectedStatement rejected problem |
422 | A pushed statement file (POST /v1/statements) was received and stored as rejected: checksum missing or not matching, unverified transport signature, parse failure, or an account not covered by the profile. The message gives the reason. | Fix the file or checksum per the reason and push it again. | Fix the request |
Operator API
Approvals, connectors, routing, keys and audit.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
account_not_foundAccount not found problem |
404 | A connector request (list, configure or delete bindings) named an account_id other than the caller's own, and no account with that id exists on this deployment. Only checked when the gateway runs with the payments store. | Check the account_id, or omit it to act on your own account. | Fix the request |
already_decidedApproval already decided problem |
409 | POST /v1/admin/approvals/{id}/decide was called on an approval request that has already been approved or rejected. | Read the approval to see its outcome; no further decision is needed. | Fix the request |
ambiguous_keysExisting keys are not bound to an account problem |
409 | POST /v1/admin/accounts was refused because one or more keys in the deployment's static `keys:` registry have no account_id. Creating a second account would let those keys read the first account's payments. The detail lists the key ids. | Set account_id on every key in the `keys:` registry, restart, then create the account again. | After an action |
audit_unavailableAudit trail not available problem |
503 | The audit chains, anchors, verifier or exports were requested on a deployment that runs without the payments store, the audit verifier or the audit exporter. | Enable the payments store and audit trail on this deployment; retrying alone will not help. | After an action |
audit_unsignedNo audit signing key problem |
404 | GET /v1/admin/audit/public-key was called on a deployment that has no audit signing key, so anchors and bundles are unsigned. | Configure an audit signing key on the deployment if signed anchors are required. | After an action |
config_requiredCredentials required problem |
400 | A connector write (POST /v1/admin/connectors) did not carry a `config` object. Stored credentials are never returned, so every write must send the full config; an empty one would erase the live credentials. | Send the full connector `config` with every write. | Fix the request |
connectors_disabledConnector management unavailable problem |
501 | The connector management API was called on a deployment started without a connector factory, so bindings cannot be managed at all. | Run a deployment with dynamic connectors enabled; retrying will not help. | After an action |
invalid_connectorInvalid connector configuration problem |
400 | POST /v1/admin/connectors named an unknown driver, or the driver's configuration is missing or malformed; the binding was not saved. | Check the driver name and its required configuration and send it again. | Fix the request |
invalid_expires_atInvalid expires_at problem |
400 | POST /v1/admin/access-keys was sent with an expires_at that is not an RFC 3339 timestamp. | Send expires_at as RFC 3339, e.g. 2026-12-31T23:59:59Z. | Fix the request |
invalid_keyInvalid key problem |
400 | POST /v1/admin/access-keys described a key that cannot be built (for example an unknown mode, scope or IP allowlist entry); the message gives the reason. | Fix the field named in the message. | Fix the request |
invalid_modeInvalid mode problem |
400 | A connector admin endpoint was given a mode other than test or live. | Send mode=test or mode=live. | Fix the request |
invalid_phone_patternInvalid phone pattern problem |
400 | POST /v1/sources was sent with a phone_pattern that is not a valid regular expression. | Send a valid (Go RE2) regular expression. | Fix the request |
jobs_unavailableJob scheduler not running problem |
503 | GET /v1/admin/jobs reached a node on which the job scheduler is not running. | Retry later, or query a node that runs the scheduler. | Retry later |
key_conflictKey conflict problem |
409 | POST /v1/admin/access-keys could not add the key to the registry, typically because a key with the same id already exists; the message gives the reason. | Choose another id, or omit id to have one generated. | Fix the request |
missing_nameMissing name problem |
400 | POST /v1/sources was sent without name. | Send the source name. | Fix the request |
not_replayableCallback not replayable problem |
409 | POST /v1/admin/callbacks/{id}/replay was called for a provider callback that is not dead-lettered or failed. | Replay only callbacks in dead_lettered or failed status; others are already processed or pending. | Fix the request |
range_too_largeDate range too large problem |
400 | An audit-trail export was requested for a range longer than 92 days. | Split the export into ranges of at most 92 days. | Fix the request |
same_approverSame person cannot approve problem |
403 | Four-eyes rule: the person who created a payout or an approval request tried to approve it (payout approval, POST /v1/admin/approvals/{id}/decide). | Have a different person approve the request. | After an action |
stream_capacityToo many live streams problem |
503 | GET /admin/stats/stream (the dashboard's live stats stream) refused a new subscriber because the server already holds its maximum number of streams. | Poll instead, or open the stream again after Retry-After (30 s). | Retry later |
Platform
Availability, rate limits, residency and internal errors.
| Code | Status | Meaning | What to do | Retry |
|---|---|---|---|---|
internal_errorInternal error problem |
500 | Something failed inside the gateway. Every 500 has this code (the area it happened in is in the logs, under the X-Request-ID); the detail never carries the cause. | Retry with the same Idempotency-Key: a money-moving call never acts twice. If it persists, contact support with the X-Request-ID; if a payment's state is unclear, read it rather than charging again. | Retry as is |
onboarding_unavailableOnboarding unavailable problem |
503 | POST /v1/psps/applications was called on a gateway node that runs without the database-backed payments store. | Operator: run the gateway with the payments store; callers should use a node that has it. | After an action |
rate_limitedRate limited problem |
429 | Too many requests: the per-IP/surface limit, the API key's own per-minute limit, or too many failed authentication attempts from the same IP was exceeded. | Wait for the number of seconds in Retry-After, then retry; slow down or ask for a higher key limit. | Retry later |
residency_unavailableResidency not configured problem |
503 | GET /v1/admin/residency was called on a node where region pinning is not configured or that has no payments store. | Operator: configure residency on the node, or call a node that has it. | After an action |
service_unavailableService unavailable problem |
503 | The endpoint needs the database-backed service (or the event store) and this gateway node runs without it. Also used by the GitHub secret-scanning receiver. | Operator: run the gateway with the database-backed service; callers should use a node that has it. | After an action |
stages_unavailableStages unavailable problem |
503 | GET /v1/transactions/{id}/stages was called on a node without the payments store. | Operator: run the gateway with the payments store; callers should use a node that has it. | After an action |
timeline_unavailableTimeline unavailable problem |
503 | A transaction or account timeline was requested on a node without the payments store. | Operator: run the gateway with the payments store; callers should use a node that has it. | After an action |
wrong_regionWrong region problem |
421 | The tenant's (or PSP applicant's country's) data is held in another region, so this region does not serve the request. Headers KulPay-Region and KulPay-Region-Url, and body fields region and base_url, name the right region. | Send the request to the base URL of the region named in the response. | Fix the request |
Field codes
Inside a validation problem's errors[], each item names the field and one of these codes.
| Code | Meaning | What to do |
|---|---|---|
invalidInvalid value | Per-field code in a validation problem's errors[]: the field has a malformed value, e.g. money.currency not 3-8 characters, money.amount not a decimal, min_amount/max_amount not positive, retry_policy delays not Go durations, retry_on not 4xx/5xx/100-599, or a payment link's amount or expires_at malformed. | Fix the field named in the item as its message says. |
out_of_rangeValue out of range | Inside a validation problem's errors[]: a numeric field is outside its allowed range: money.amount not positive or outside min_amount/max_amount, max_amount below min_amount, retry_policy values (max_attempts, backoff_multiplier and others) out of bounds, or a payment link's amount not positive or usage_limit negative. | Change the field named in errors[].field to a value within the range given in the message. |
requiredField required | Inside a validation problem's errors[]: a required field is missing, e.g. money.currency, money.amount (unless open_amount is true) or source.type on a charge. | Send the field named in errors[].field. |
restrictedMethod restricted | Inside the gateway.method_restricted validation problem: the charge's source.type is not in its own allowed_sources or is in its blocked_sources. | Drop the conflicting restriction or choose a source type the restrictions admit. |
incompatibleIncompatible value | Per-field code in the errors[] of gateway.open_amount_requires_checkout: a charge asked for open_amount=true with a method that has no checkout page, so the customer cannot enter the amount. | Use a method that presents a checkout page, or send a fixed amount. |
Deprecated codes
These codes were replaced by one code per condition and are no longer sent: errors carry only the new code (there is no legacy_code any more). The old error: {code, message} member still rides along, with the new code, until its sunset: once the date is announced every error carries the Deprecation and Sunset headers (RFC 9745, RFC 8594), and from that date the member is gone. Read code, detail and the problem's own members.
| Old code | Now |
|---|---|
approvals_failed | internal_error |
audit_failed | internal_error |
balance_failed | internal_error |
bills_failed | internal_error |
callbacks_failed | internal_error |
cash_deposits_failed | internal_error |
close_failed | internal_error |
code_mappings_failed | internal_error |
conflict | invalid_state |
credentials_failed | internal_error |
data_transfer_failed | internal_error |
encode_failed | internal_error |
exceptions_failed | internal_error |
export_failed | internal_error |
fee_claims_failed | internal_error |
fee_schedules_failed | internal_error |
fetch_failed | upstream_error |
funds_flow_failed | internal_error |
gateway.account_suspended | account_suspended |
gateway.amount_exceeds_balance | amount_exceeds_remaining |
gateway.amount_exceeds_remaining | amount_exceeds_remaining |
gateway.amount_too_high | amount_too_high |
gateway.amount_too_low | amount_too_low |
gateway.callback_rejected | callback_rejected |
gateway.close_refused | invalid_state |
gateway.empty_body | invalid_request |
gateway.enqueue_error | internal_error |
gateway.idempotency_key_required | idempotency_key_required |
gateway.internal_error | internal_error |
gateway.invalid_amount | invalid_amount |
gateway.invalid_body | invalid_request |
gateway.invalid_card | invalid_card |
gateway.invalid_card_token | invalid_card_token |
gateway.invalid_method | invalid_method |
gateway.invalid_money | invalid_amount |
gateway.invalid_otp | invalid_otp |
gateway.invalid_phone | invalid_phone |
gateway.invalid_request | invalid_request |
gateway.invalid_state | invalid_state |
gateway.invalid_upload | invalid_request |
gateway.link_create_failed | invalid_request |
gateway.live_payment | live_payment |
gateway.missing_file | missing_file |
gateway.missing_method | invalid_request |
gateway.missing_otp | missing_otp |
gateway.missing_payment_id | invalid_request |
gateway.missing_payout_id | invalid_request |
gateway.missing_phone | missing_phone |
gateway.missing_provider | invalid_request |
gateway.missing_refund_id | invalid_request |
gateway.no_connector_binding | no_connector_binding |
gateway.not_found | not_found |
gateway.not_supported | not_supported |
gateway.pilot_limit | pilot_limit |
gateway.provider_error | upstream_error |
gateway.rate_limited | rate_limited |
gateway.raw_card_data_not_accepted | raw_card_data_not_accepted |
gateway.read_error | internal_error |
gateway.reconcile_failed | invalid_request |
gateway.resolve_failed | invalid_request |
gateway.service_unavailable | service_unavailable |
gateway.statement_import_failed | invalid_request |
gateway.stream_capacity | stream_capacity |
gateway.unsupported_currency | unsupported_currency |
health_failed | internal_error |
insufficient_funds | insufficient_balance |
internal | internal_error |
invalid_body | invalid_request |
invalid_upload | invalid_request |
jobs_failed | internal_error |
keys_unavailable | internal_error |
ledger_failed | internal_error |
logo_failed | internal_error |
onboarding_failed | internal_error |
payouts_failed | internal_error |
psp_failed | internal_error |
psps_failed | internal_error |
rand_failed | internal_error |
read_error | invalid_request |
read_failed | internal_error |
regulatory_failed | internal_error |
replay_failed | internal_error |
residency_failed | internal_error |
resource_missing | not_found |
reversals_failed | internal_error |
reviews_failed | internal_error |
risk_failed | internal_error |
routing_failed | internal_error |
safeguarding_failed | internal_error |
server_error | internal_error |
settlement_reports_failed | internal_error |
settlements_failed | internal_error |
stages_failed | internal_error |
statements_failed | internal_error |
stream_unsupported | internal_error |
timeline_failed | internal_error |
webhooks_failed | internal_error |
Outcome codes
An accepted payment can still fail at the provider: the payer cancels, the wallet is short, the card is declined. The request got a 2xx; the payment moves to failed later, and its failure_code (with failure_message) says why, in the resource and in the *.failed webhook. KulPay maps every provider's own code to one vocabulary, so you write one switch for all rails; the provider's code stays in provider_code.
Each payment failure has a family: soft_decline may succeed later or with the payer's help (top up and try again); hard_decline will not succeed as asked (offer another method); error is technical, on the provider's side or the network (retrying is reasonable). The payer messages are short texts an app can show as they are.
Payment, refund and payout failures
The failure_code of a payment, a refund or a payout that the provider declined or that could not complete. One vocabulary for every rail: each provider's own code is kept in provider_code.
| Code | Family | Meaning | What to do | Tell the payer |
|---|---|---|---|---|
card_declinedCard declined card |
hard_decline | The card issuer refused the payment without giving a more specific reason. | Do not retry the same card unchanged; it is a hard decline. Show the payer a message and let them pay with another card or method. | O seu cartão foi recusado. Tente outro cartão ou outro método de pagamento. Your card was declined. Please try another card or payment method. |
insufficient_fundsInsufficient funds card · mobile_money |
soft_decline | The card or wallet did not have enough balance to cover the amount. | Retrying immediately will fail; it can succeed once the payer has added money. Show the payer a message and let them top up or use another method. | Saldo insuficiente. Carregue a carteira ou a conta e tente de novo. Insufficient balance. Please top up your wallet or account and try again. |
expired_cardCard expired card |
hard_decline | The card is past its expiry date. | Retrying with the same card will not succeed. Ask the payer to use a different card or method. | O seu cartão expirou. Use outro cartão ou outro método de pagamento. Your card has expired. Please use another card or payment method. |
incorrect_cvcIncorrect security code or PIN card |
hard_decline | The card security code (CVC) or the PIN the payer entered was wrong. | Not retried automatically. A new attempt can succeed if the payer re-enters the correct details; show a message and let them try again. | Código de segurança ou PIN incorrecto. Verifique os dados e tente de novo. The security code or PIN is incorrect. Please check and try again. |
incorrect_numberIncorrect card number card |
hard_decline | The card number is not valid. | Not retried automatically. A new attempt can succeed if the payer corrects the card number; show a message and let them re-enter it. | O número do cartão está incorrecto. Verifique e tente de novo. The card number is incorrect. Please check it and try again. |
processing_errorProcessing error card · mobile_money |
error retriable |
The provider hit an internal error while processing the payment. | Transient: a new attempt can succeed. Retry later with a new payment, or let the payer try again. | Ocorreu um erro ao processar o pagamento. Tente de novo dentro de alguns minutos. Something went wrong processing your payment. Please try again in a few minutes. |
authentication_requiredAuthentication required card |
soft_decline | The issuer requires the payer to authenticate (for example 3-D Secure), or authentication was not completed. | Not retried automatically. A new attempt can succeed if the payer completes authentication; let them try again. | O seu banco pede confirmação adicional. Tente de novo e conclua a verificação. Your bank needs extra verification. Please try again and complete the check. |
do_not_honorDo not honour card |
hard_decline | The issuer declined the payment with the generic "do not honour" response. | Hard decline: retrying the same card will not succeed. Ask the payer to contact their bank or use another method. | O seu banco recusou o pagamento. Contacte o banco ou use outro método. Your bank declined the payment. Please contact your bank or use another method. |
card_not_supportedCard not supported card |
hard_decline | The card does not support this kind of purchase. | Retrying the same card will not succeed. Let the payer use another card or method. | Este cartão não é aceite para este pagamento. Use outro cartão ou método. This card can't be used for this payment. Please use another card or method. |
currency_not_supportedCurrency not supported card |
hard_decline | The card cannot be charged in the payment's currency. | Retrying the same card will not succeed. Let the payer use another card or method. | Este cartão não aceita pagamentos nesta moeda. Use outro cartão ou método. This card can't pay in this currency. Please use another card or method. |
fraudulentSuspected fraud card |
hard_decline | The issuer or provider blocked the payment as suspected fraud, or a dispute was opened with reason fraudulent. | Do not retry. Show the payer a neutral message without mentioning fraud, and review the order before offering another method. | Não foi possível concluir o pagamento. Use outro método ou contacte o seu banco. We couldn't complete the payment. Please use another method or contact your bank. |
lost_cardCard reported lost card |
hard_decline | The card has been reported lost. | Do not retry. Show a neutral message and let the payer use another method. | O seu cartão foi recusado. Use outro cartão ou método de pagamento. Your card was declined. Please use another card or payment method. |
stolen_cardCard reported stolen card |
hard_decline | The card has been reported stolen. | Do not retry. Show a neutral message and let the payer use another method. | O seu cartão foi recusado. Use outro cartão ou método de pagamento. Your card was declined. Please use another card or payment method. |
pickup_cardPick up card card |
hard_decline | The issuer asked for the card to be retained, usually because it has been cancelled. | Do not retry. Show a neutral message and let the payer use another method. | O seu cartão foi recusado. Contacte o banco ou use outro método. Your card was declined. Please contact your bank or use another method. |
restricted_cardRestricted card card |
hard_decline | The card is restricted and cannot be used for this payment. | Retrying the same card will not succeed. Ask the payer to contact their bank or use another method. | Este cartão tem restrições. Contacte o banco ou use outro método. This card is restricted. Please contact your bank or use another method. |
security_violationSecurity or compliance block card |
hard_decline | The provider blocked the payment for a security or compliance reason, such as a compliance hold or missing customer verification. | Do not retry. Show a neutral message; the payer may need to contact their provider. | Não foi possível concluir o pagamento. Contacte o seu banco ou operador. We couldn't complete the payment. Please contact your bank or provider. |
transaction_not_allowedTransaction not allowed any |
hard_decline | The payer's card or wallet is not permitted to make this payment (inactive account, profile problem, wrong market), or KulPay's risk review declined it. | Retrying unchanged will not succeed. Let the payer use another method; for a risk decline, review before accepting a new attempt. | Este pagamento não é permitido para a sua conta. Use outro método de pagamento. This payment isn't allowed for your account. Please use another payment method. |
try_again_laterTry again later card |
error retriable |
The issuer could not process the payment right now and asked for it to be sent again. | Transient: a new attempt can succeed. Retry after a short wait or let the payer try again. | Não foi possível processar o pagamento agora. Tente de novo dentro de alguns minutos. We couldn't process the payment right now. Please try again in a few minutes. |
withdrawal_count_limit_exceededToo many transactions card |
soft_decline | The card has reached the number of transactions allowed in the period. | Retrying now will fail; it can succeed after the limit resets. Let the payer use another method or try later. | Atingiu o limite de transacções do cartão. Tente mais tarde ou use outro método. Your card has reached its transaction limit. Please try later or use another method. |
generic_declinePayment declined card · mobile_money |
hard_decline | The provider declined the payment without a specific reason. | Hard decline: retrying unchanged will not succeed. Let the payer use another method or contact their provider. | O pagamento foi recusado. Use outro método ou contacte o seu banco ou operador. The payment was declined. Please use another method or contact your bank or provider. |
user_cancelledCancelled by the payer mobile_money |
hard_decline | The payer cancelled or rejected the payment request on their phone. | Not retried automatically. The payer can start a new payment if they want to; let them try again or choose another method. | Cancelou o pagamento no telemóvel. Pode tentar de novo quando quiser. You cancelled the payment on your phone. You can try again whenever you like. |
timeoutTimed out card · mobile_money |
error retriable |
The payer did not confirm in time (for example did not enter the PIN on the phone), or the provider answered too late. | Transient: a new attempt can succeed. Let the payer try again and confirm promptly. | O tempo para confirmar o pagamento esgotou. Tente de novo e confirme no telemóvel. The time to confirm the payment ran out. Please try again and confirm on your phone. |
provider_unavailableProvider unavailable card · mobile_money |
error retriable |
The mobile money operator, issuer or switch was temporarily unavailable or overloaded. | Transient: a new attempt can succeed. Retry later or let the payer use another method. | O serviço está temporariamente indisponível. Tente de novo dentro de alguns minutos. The service is temporarily unavailable. Please try again in a few minutes. |
duplicate_transactionDuplicate transaction card · mobile_money |
hard_decline | The provider saw this transaction as a repeat of one it already received. | Do not retry. Check whether the earlier payment succeeded before asking the payer to pay again. | Este pagamento já foi recebido. Verifique antes de pagar de novo. This payment was already received. Please check before paying again. |
network_errorNetwork error any |
error retriable |
The connection to the provider failed. | Transient: a new attempt can succeed. Retry later or let the payer try again. | Houve uma falha de comunicação. Tente de novo dentro de alguns minutos. There was a connection problem. Please try again in a few minutes. |
limit_exceededLimit exceeded card |
soft_decline | The amount is over a limit on the payer's card or account. | Retrying the same amount now will fail; it can succeed with a smaller amount or after the limit resets. Let the payer use another method. | O valor ultrapassa o limite da sua conta. Tente um valor menor ou outro método. The amount is over your account limit. Please try a smaller amount or another method. |
account_not_foundAccount not found mobile_money |
hard_decline | The phone number or receiver is not valid at the provider. | Retrying unchanged will not succeed. Ask the payer to check the number and try again, or use another method. | Número inválido. Verifique o número e tente de novo. Invalid number. Please check the number and try again. |
no_accountNo wallet for this number mobile_money |
hard_decline | The phone number has no wallet with this operator. | Retrying the same number will not succeed. Let the payer use a number with a wallet or another method. | Este número não tem conta neste serviço. Use outro número ou método. This number has no account with this service. Please use another number or method. |
bank_rejectedRejected by the bank bank |
hard_decline | The bank, or the operator reviewing the bank transfer, rejected the payment. | Retrying unchanged will not succeed. Ask the payer to check the transfer details or use another method. | O banco rejeitou o pagamento. Verifique os dados ou use outro método. The bank rejected the payment. Please check the details or use another method. |
amount_mismatchAmount does not match any |
hard_decline | The amount paid or sent to the provider did not match the amount expected. | Retrying unchanged will not succeed. Check the amount and create a new payment for the correct value. | O valor pago não corresponde ao valor pedido. Verifique e tente de novo. The amount paid doesn't match the amount requested. Please check and try again. |
expiredExpired any |
hard_decline | The payment or its reference expired before it was completed; KulPay also sets it when a payment held for risk review is not reviewed in time. | This payment cannot be completed. Create a new payment if the payer still wants to pay. | O pagamento expirou. Inicie um novo pagamento. The payment has expired. Please start a new payment. |
connector_errorCould not reach the provider any |
error retriable |
KulPay could not send the request to the provider, or the provider rejected it as malformed or unauthorised. The money did not move. | Usually transient: a new attempt can succeed. Retry later; if it persists, contact KulPay support. | Não foi possível processar o pagamento. Tente de novo dentro de alguns minutos. We couldn't process the payment. Please try again in a few minutes. |
cancelledCancelled any |
hard_decline | The payment was cancelled before it completed. | This payment will not complete. Create a new payment if the payer still wants to pay. | O pagamento foi cancelado. Pode iniciar um novo pagamento. The payment was cancelled. You can start a new payment. |
unknownUnknown reason any |
unknown | The payment failed, or was disputed, but the provider's reason has no mapping to a more specific code. | Retry policy is unknown. Read failure_message, let the payer try again or use another method, and contact KulPay support if it repeats. | Não foi possível concluir o pagamento. Tente de novo ou use outro método. We couldn't complete the payment. Please try again or use another method. |
Refund and payout failures set by KulPay
Codes KulPay itself sets on a refund or a payout.
| Code | On | Meaning | What to do | Tell the payer |
|---|---|---|---|---|
rejectedRejected by an approver any |
refund, payout | A refund or payout that needed a second person's approval was rejected, so it was never sent. failure_message is "Rejected by an approver." | Do not retry automatically; create a new refund or payout only after resolving why it was rejected. The reserved money is released. | O reembolso ou pagamento não foi aprovado. Contacte o comerciante. The refund or transfer was not approved. Please contact the merchant. |
reversal_failedReversal failed any |
refund | KulPay asked the provider to reverse the original transaction and the provider did not reverse it. | The refund failed and the reserved amount is released. Retry the refund later or contact KulPay support. | Não foi possível devolver o dinheiro agora. O comerciante vai tentar de novo. We couldn't return the money right now. The merchant will try again. |
Bill payment failures
The failure_code of a bill payment, with failure_message carrying the biller's or supplier's reason.
| Code | On | Meaning | What to do | Tell the payer |
|---|---|---|---|---|
rejectedRejected by the biller any |
bill_payment | The biller (supplier) refused the payment, for example because the reference or amount is not accepted. failure_message carries the biller's reason. | Retrying unchanged will not succeed. Show the reason, let the payer check the reference or amount, and start a new bill payment. A payer who was already charged is refunded automatically. | A empresa recusou o pagamento. Verifique a referência e o valor e tente de novo. The biller refused the payment. Please check the reference and amount and try again. |
biller_unavailableBiller unavailable any |
bill_payment | The biller could not deliver right now (unavailable, out of float, or not authorised) and nothing was delivered. | Transient: a new bill payment later can succeed. Nothing was charged; a payer charged by collection is refunded automatically. | O serviço da empresa está indisponível. Nada foi cobrado. Tente mais tarde. The biller's service is unavailable. Nothing was charged. Please try later. |
collection_failedPayer was not charged any |
bill_payment | The payment collecting the money from the payer could not be created, or it failed, was cancelled or expired. | Nothing was charged and nothing was sent to the biller. Let the payer start again, possibly with another method; the collection payment's own failure_code says why. | Não foi possível cobrar o pagamento. Nada foi debitado. Tente de novo ou use outro método. We couldn't collect the payment. Nothing was charged. Please try again or use another method. |
collection_shortCollected amount too low any |
bill_payment | The payer paid, but after fees the collected amount did not cover the bill. | The collection is refunded automatically. Start a new bill payment; check the quoted total and fees. | O valor recebido não cobre a factura. O dinheiro será devolvido. The amount received doesn't cover the bill. Your money will be returned. |
not_processedNever received by the biller any |
bill_payment | After the waiting period the biller still had no record of the payment, so nothing was charged. | A new bill payment can succeed. A payer charged by collection is refunded automatically. | A empresa não recebeu o pagamento. Nada foi cobrado. Tente de novo. The biller never received the payment. Nothing was charged. Please try again. |
resolved_failedFailed after manual review any |
bill_payment | The bill payment's outcome was uncertain and KulPay operators confirmed, with a second approval, that the biller did not deliver. | The reserved money is released. Start a new bill payment if the payer still wants to pay; contact KulPay about any money collected from the payer. | Confirmámos que o pagamento não foi entregue à empresa. Tente de novo. We confirmed the payment was not delivered to the biller. Please try again. |
Cash-deposit exceptions
The code of a cash_in.exception event.
| Code | On | Meaning | What to do | Tell the payer |
|---|---|---|---|---|
unconfirmed_depositDeposit not confirmed bank |
cash_deposit | No bank statement line matched the reported cash deposit by the end of the arrival window; the deposit moves to status unconfirmed and nothing is credited. | Check the entry id and account reported; the case is raised as an exception for review. | Ainda não confirmámos o seu depósito. Guarde o talão e contacte o balcão. We haven't confirmed your deposit yet. Keep your receipt and contact the branch. |
deposit_amount_mismatchDeposit amount differs bank |
cash_deposit | The bank statement line's amount or currency differs from the reported deposit; the deposit moves to status mismatch and nothing is credited. | Compare the reported amount with statement_amount_minor; the case is raised as an exception for review. | O valor do depósito não corresponde. Guarde o talão e contacte o balcão. The deposit amount doesn't match. Keep your receipt and contact the branch. |
unidentified_depositDepositor not identified bank |
cash_deposit | The deposit matched a statement line but its depositor reference names no KulPay account or tag; the money is held in suspense (status suspended). | Supply the correct account or tag, or have the deposit returned from suspense. | Não identificámos a conta do depósito. Contacte o balcão com o seu talão. We couldn't identify the account for your deposit. Contact the branch with your receipt. |
credit_rejectedCredit rejected by the resolver bank |
cash_deposit | The resolver (KulOffice) refused to credit the wallet the deposit was for (wallet_closed, limit_exceeded, customer_unknown), or an operator decided so in review; the credit to the resolver's account is taken back and the money is held in suspense (status suspended). | Assign the deposit to another destination by a new lookup of the reference, or return it; a second operator approves. | Não conseguimos creditar a carteira indicada. Guarde o talão; vamos contactá-lo ou devolver o valor. We couldn't credit the wallet you gave. Keep your receipt; we will contact you or return the money. |
bill_failedBill not paid bank |
cash_deposit | The deposit was for an invoice or a biller's product and the bill could not be paid (refused, failed at the supplier, or not quoted within 24 hours) (IO-2.5); the credit is taken back and the money is held in suspense (status suspended). | Assign the deposit to another destination by a new lookup of the reference, or return it; a second operator approves. | Não conseguimos pagar a factura indicada. Guarde o talão; vamos contactá-lo ou devolver o valor. We couldn't pay the bill you gave. Keep your receipt; we will contact you or return the money. |
credit_account_invalidBiller credit account withdrawn bank |
cash_deposit | The deposit was for a biller whose credit account, named at lookup, is no longer one of its registered active accounts (IO-4.1): nothing was paid, and the money is held in suspense (status suspended). | Assign the deposit by a new lookup of the reference (the biller's current account), or return it; a second operator approves. | Não conseguimos pagar a factura indicada. Guarde o talão; vamos contactá-lo ou devolver o valor. We couldn't pay the bill you gave. Keep your receipt; we will contact you or return the money. |
Webhook endpoint disabled
The disabled_reason of a webhook endpoint that stopped receiving deliveries.
| Code | On | Meaning | What to do | Tell the payer |
|---|---|---|---|---|
pausedPaused by its owner any |
webhook_endpoint | The endpoint's owner paused it; the endpoint shows status paused and receives no deliveries. | Resume the endpoint when ready to receive events again. | — |
goneEndpoint answered 410 Gone any |
webhook_endpoint | The endpoint answered a delivery with HTTP 410, so KulPay disabled it. | Fix or replace the endpoint URL, then re-enable it and replay missed deliveries. | — |
dead_lettersDeliveries failing for three days any |
webhook_endpoint | The endpoint had dead-lettered deliveries on each of the last three days, so KulPay disabled it. | Fix the endpoint, re-enable it, and replay the dead-lettered deliveries. | — |
KulPay