Errors
Understanding and handling errors from the Uprails API.
Error Response Format
When an error occurs, the API returns a JSON response with an error object containing the error type, code, and a human-readable message.
{
"error": {
"type": "invalid_request",
"code": "IR_04",
"message": "Invalid value for field: amount"
}
}HTTP Status Codes
| Status | Description |
|---|---|
200 | Request succeeded |
201 | Resource created successfully |
400 | Bad request — invalid parameters or missing required fields |
401 | Unauthorized — invalid or missing API key |
403 | Forbidden — insufficient permissions |
404 | Not found — resource doesn't exist |
409 | Conflict — resource already exists or action conflicts |
422 | Unprocessable entity — request understood but cannot be processed |
429 | Too many requests — rate limit exceeded |
500 | Internal server error — something went wrong on our end |
502 | Bad gateway — upstream service error |
503 | Service unavailable — temporary outage |
Error Types
authentication_error
Issues with API authentication. Check that your API key is correct and has the required permissions.
invalid_request
The request was malformed or contained invalid parameters. Check the request body and parameters.
processing_error
An error occurred while processing the payment. This may include card declines or issues with the payment method.
rate_limit_error
Too many requests were made in a short period. Wait and retry with exponential backoff.
api_error
An unexpected error occurred on our servers. These are rare and usually temporary. Retry with exponential backoff.
Common Error Codes
| Code | Type | Description |
|---|---|---|
AU_01 | authentication_error | Invalid API key provided |
AU_02 | authentication_error | API key has been revoked |
IR_01 | invalid_request | Missing required field |
IR_04 | invalid_request | Invalid field value |
IR_05 | invalid_request | Resource not found |
PE_01 | processing_error | Card declined |
PE_02 | processing_error | Insufficient funds |
PE_03 | processing_error | Expired card |
CE_01 | connector_error | Processor unavailable |
RL_01 | rate_limit_error | Rate limit exceeded |
Payment Failure Codes
error.code and unified_code are different things. error.code (IR_04, HE_00)
means the API rejected your request — you get a 4xx/5xx and no payment is attempted.
unified_code (UE_1001) means a payment was attempted and declined —
you get a 200 with status: "failed".
Every card network and acquirer describes the same decline differently. A payment refused for
insufficient funds may come back as INSUFFICIENT_FUNDS, 51, or NotSufficientFunds
depending on which acquirer handled it.
unified_code normalises these into one vocabulary, so you can write your decline handling
once and it keeps working when we add an acquirer. It appears on the payment object, in
/payments/list, and in webhooks.
Response shape
{
"payment_id": "pay_kQdeYLEIhIk62GZomSsE",
"status": "failed",
"unified_code": "UE_1003",
"unified_message": "The card number is invalid",
"error_code": "DECLINED",
"error_message": "Resubmit with alternative payment details",
"error_details": {
"unified_details": {
"category": "UE_1003",
"message": "The card number is invalid"
},
"issuer_details": {
"code": "14",
"message": "Invalid card number",
"network_details": { "name": "Visa" }
},
"connector_details": {
"code": "DECLINED",
"message": "Resubmit with alternative payment details",
"reason": "Failed: Invalid card number"
}
}
}unified_code and unified_message are what you should branch on and, where appropriate,
show to your customer. error_code, error_message and error_details.connector_details
carry the acquirer's own wording — useful when raising a support ticket with us, but they
vary by acquirer and are not a stable integration surface.
Do not match on error_message. It is the acquirer's text and changes without notice —
"Resubmit with alternative payment details" is an instruction to us, not something to
show a cardholder. Branch on unified_code.
Code families
The first digit tells you what kind of failure it was, which is usually enough to decide what to do. Match on the family when you want broad handling, on the exact code when you want to tailor the message.
| Family | Meaning | Retry? |
|---|---|---|
UE_1xxx | Card or issuer declined | No — the customer needs a different card (except UE_1018, see below) |
UE_2xxx | Not eligible, or stopped by risk rules | No — except UE_2002, see below |
UE_3xxx | Temporary availability problem | Yes, with backoff |
UE_4xxx | Something wrong in the request | No — fix the request first |
UE_5xxx | Failed during 3-D Secure authentication | Sometimes — see below |
UE_9000 | Unrecognised failure | Treat as terminal |
UE_5xxx is about authentication, not the card. These payments failed during 3-D Secure
and never reached the issuer for authorisation, so the card itself may be perfectly good. A
customer who abandoned the challenge (UE_5003) can usually just try again.
UE_2002 is retryable. The issuer has asked for authentication under PSD2 rather than
declining the card. Retrying the same payment with 3-D Secure will often succeed — treat it
as a prompt to authenticate, not as a refusal.
UE_1018 is not a decline. The acquirer reported a duplicate, which means the original
attempt may have succeeded. Check the status of the earlier payment before retrying or
refunding — treating it as a plain failure risks charging the customer twice.
UE_1xxx — Card and issuer declines
The issuer refused the payment. The card details or the account are the problem.
| Code | Message |
|---|---|
UE_1001 | Insufficient funds |
UE_1002 | The card has expired |
UE_1003 | The card number is invalid |
UE_1004 | The security code is incorrect |
UE_1005 | The PIN is incorrect |
UE_1006 | The card was reported lost or stolen |
UE_1007 | The card was declined by the issuer |
UE_1008 | This card is not supported |
UE_1009 | The card is restricted |
UE_1010 | This transaction is not permitted for this card |
UE_1011 | The transaction exceeds the card's limit |
UE_1012 | The transaction was declined for security reasons |
UE_1013 | Declined, please contact your card issuer |
UE_1014 | No record of this card was found |
UE_1015 | The card is not active |
UE_1016 | This account cannot be used for this payment |
UE_1017 | The cardholder has cancelled this payment |
UE_1018 | This payment may have already been processed |
UE_1099 | The payment was declined |
UE_2xxx — Eligibility and risk
The payment was stopped by eligibility or risk rules rather than by a decline on the card
itself. UE_2002 is the exception: there the issuer is asking for authentication.
| Code | Message |
|---|---|
UE_2001 | This payment method is currently unavailable |
UE_2002 | Additional authentication is required |
UE_2003 | The payment was blocked |
UE_2004 | This currency or country is not supported |
UE_3xxx — Availability
A temporary problem reaching the acquirer or issuer. Safe to retry later.
| Code | Message |
|---|---|
UE_3001 | The payment service is temporarily unavailable, please try again later |
UE_3002 | The payment could not be processed, please try again |
UE_3003 | The payment timed out, please try again |
UE_4xxx — Request data
Something in the request was missing, malformed or no longer valid.
| Code | Message |
|---|---|
UE_4001 | Required payment information is missing or invalid |
UE_4002 | The security code is not the expected length |
UE_4003 | The payment session has expired, please start again |
UE_4004 | The transaction is in an invalid state |
UE_5xxx — Authentication (3DS)
The payment failed during 3-D Secure, before the issuer was asked to authorise it.
| Code | Message |
|---|---|
UE_5001 | Card authentication failed |
UE_5002 | Authentication was declined by the issuer |
UE_5003 | Authentication was not completed |
UE_5004 | The cardholder could not be verified |
UE_5005 | The authentication service is unavailable |
UE_5006 | Too many authentication attempts |
UE_5007 | The device could not be recognised |
UE_5008 | The authentication response was invalid |
UE_5009 | No response from the authentication service |
UE_5010 | The card is not enrolled for authentication |
UE_5011 | Authentication timed out |
UE_5012 | This authentication method is not supported |
UE_5013 | The card has expired |
UE_5014 | The card number is invalid |
UE_5015 | The transaction is not valid for authentication |
UE_5016 | No record of this card was found |
UE_5017 | Authentication was declined for security reasons |
UE_5018 | The card was reported lost or stolen |
UE_5019 | Authentication is not permitted for this card |
UE_5020 | Required authentication data is missing or invalid |
UE_5021 | The authentication session has expired |
Legacy codes
Acquirers we have not yet migrated still return these coarser codes. Unlike the codes above,
a legacy code does not imply a single message — UE_1000 alone covers insufficient funds,
an expired card, an incorrect security code and several others.
| Code | Meaning | Example messages |
|---|---|---|
UE_1000 | Card or payment-method problem | "Insufficient funds", "The card has expired", "The card number is incorrect" |
UE_2000 | Not eligible, blocked, or 3DS required | "3D Secure authentication is required for this card", "This currency is not supported" |
UE_3000 | Temporary problem | "The payment could not be completed, please try again or use a different payment method" |
UE_4000 | Request data problem | "Required payment information is missing or invalid", "The amount is below the minimum allowed" |
If you receive a legacy code, branch on the code and show unified_message to your customer —
do not assume the message from the code. These are being replaced by the granular codes above
as each acquirer is migrated, so handling written against UE_1xxx–UE_5xxx will continue
to work as more move across.
Fallback
| Code | Message |
|---|---|
UE_9000 | Something went wrong |
Handling declines
const payment = await response.json();if (payment.status === 'failed') {const code = payment.unified_code;// Branch on the family for broad handlingswitch (code?.slice(0, 4)) { case 'UE_3': // Temporary — safe to retry with backoff return scheduleRetry(payment.payment_id); case 'UE_5': // Authentication failed; the card may still be fine return promptRetryAuthentication(payment.unified_message); case 'UE_4': // Our request was wrong — do not retry unchanged return logIntegrationError(payment); default: // UE_1xxx / UE_2xxx — ask for another payment method return showToCustomer(payment.unified_message);}}Handling Errors
Here's an example of how to handle errors in your code:
try {const response = await fetch('https://api.uprails.com/payments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'api-key': 'snd_YOUR_API_KEY' }, body: JSON.stringify({ amount: 1000, currency: 'USD', profile_id: 'YOUR_PROFILE_ID' })});const data = await response.json();if (!response.ok) { const { error } = data; switch (error.type) { case 'authentication_error': console.error('Authentication failed:', error.message); break; case 'invalid_request': console.error('Invalid request:', error.message); break; case 'processing_error': console.error('Payment failed:', error.message); break; case 'rate_limit_error': console.error('Rate limited, retrying...'); break; default: console.error('An error occurred:', error.message); } return;}console.log('Payment succeeded:', data.payment_id);} catch (networkError) {console.error('Network error:', networkError);}Retry Strategy
For transient errors (5xx status codes and rate limits), implement exponential backoff with jitter:
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
// Only retry on 5xx errors and rate limits
if (error.status < 500 && error.status !== 429) throw error;
// Exponential backoff with jitter
const delay = Math.min(1000 * Math.pow(2, i) + Math.random() * 1000, 30000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}Retrying payment creation: Reuse the same payment_id on every retry. If the retry returns HE_01 ("payment already exists"), the original request was received — call GET /payments/{payment_id} to read its true status instead of treating it as a failure.