Handle errors gracefully to provide the best customer experience. All errors include clear codes and messages.
Errors follow a consistent structure:
{
"error" : {
"code" : "VALIDATION_ERROR" ,
"message" : "Invalid request parameters" ,
"details" : [
{
"field" : "amount" ,
"message" : "Amount must be positive"
}
]
}
}
HTTP Status Codes
Status Code Meaning Common Causes 400Bad Request Invalid parameters, validation errors 401Unauthorized Invalid or missing API key 404Not Found Resource doesn’t exist 409Conflict Duplicate resource, conflicting state 422Unprocessable Entity Payment declined, business logic error 429Too Many Requests Rate limit exceeded 500Internal Server Error Server error (rare) 503Service Unavailable Temporary service disruption
Error Codes
Client Errors (400-499)
Code Status Meaning Action VALIDATION_ERROR400 Invalid request parameters Check and fix parameters UNAUTHORIZED401 Invalid API key Verify credentials NOT_FOUND404 Resource not found Check resource ID DUPLICATE409 Resource already exists Use existing resource DECLINED422 Payment declined by bank Try different card INSUFFICIENT_FUNDS422 Customer has low balance Contact customer EXPIRED_CARD422 Card has expired Request new card details INVALID_CARD422 Invalid card number Re-enter card information AUTHENTICATION_REQUIRED422 3DS authentication needed Show 3DS challenge AUTHENTICATION_FAILED422 3DS verification failed Allow customer to retry
Server Errors (500-599)
Code Status Meaning Action INTERNAL_ERROR500 System error Retry request with backoff SERVICE_UNAVAILABLE503 Temporary issue Retry with exponential backoff
Handle Declined Payments
Show customer-friendly messages when payments are declined:
Declined Payment Response
{
"error" : {
"code" : "DECLINED" ,
"message" : "Payment declined by issuing bank" ,
"declineReason" : "insufficient_funds"
}
}
Decline Reasons
Decline Reason Customer-Friendly Message insufficient_funds”Payment couldn’t be processed. Please try a different card.” invalid_card”Unable to process this card. Please check your card details.” expired_card”This card has expired. Please use a different card.” card_declined”Payment couldn’t be completed. Please try another payment method.” processing_error”We’re having trouble processing this payment. Please try again.”
Best Practices
Don’t expose specific decline reasons to customers. Use friendly, generic messages that don’t embarrass them. ✅ Good: “Payment couldn’t be processed. Please try a different card.”
❌ Bad: “Insufficient funds in your account.”
When a payment is declined, suggest trying:
Another payment method
A different card
SPEI bank transfer (for large amounts)
Log the complete error response for your records, but show simplified messages to customers.
Let customers retry payments. Some declines are temporary (network issues, temporary holds).
Example: Handle Declined Payment
async function processPayment ( paymentData ) {
try {
const response = await cheqpay . payments . create ( paymentData );
return { success: true , payment: response };
} catch ( error ) {
// Log full error for debugging
logger . error ( 'Payment failed' , { error , paymentData });
// Show customer-friendly message
if ( error . code === 'DECLINED' ) {
return {
success: false ,
message: 'Payment couldn \' t be processed. Please try a different card.' ,
allowRetry: true
};
}
// Handle other errors...
}
}
Retry Failed Requests
For temporary errors, implement retry logic with exponential backoff:
async function createPaymentWithRetry ( data , maxRetries = 3 ) {
for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
try {
return await createPayment ( data );
} catch ( error ) {
// Only retry server errors
const isRetryable =
error . status === 500 ||
error . status === 503 ||
error . code === 'INTERNAL_ERROR' ||
error . code === 'SERVICE_UNAVAILABLE' ;
if ( ! isRetryable || attempt === maxRetries - 1 ) {
throw error ;
}
// Exponential backoff: 1s, 2s, 4s
const delay = Math . pow ( 2 , attempt ) * 1000 ;
await sleep ( delay );
}
}
}
function sleep ( ms ) {
return new Promise ( resolve => setTimeout ( resolve , ms ));
}
Safe Retries with Idempotency
Thanks to idempotency, you can safely retry requests without creating duplicate charges:
const payment = await createPaymentWithRetry ({
externalId: 'order-12345' , // Same ID = safe retry
amount: 10000 ,
currency: 'MXN' ,
// ...
});
Always include an externalId to prevent duplicate charges when retrying failed requests.
Validation Errors
Handle validation errors before submitting:
{
"error" : {
"code" : "VALIDATION_ERROR" ,
"message" : "Invalid request parameters" ,
"details" : [
{
"field" : "amount" ,
"message" : "Amount must be positive"
},
{
"field" : "customer.email" ,
"message" : "Email is invalid"
}
]
}
}
Client-Side Validation
Validate data before sending to API:
function validatePaymentData ( data ) {
const errors = [];
// Validate amount
if ( ! data . amount || data . amount <= 0 ) {
errors . push ({ field: 'amount' , message: 'Amount must be greater than 0' });
}
// Validate email
if ( ! data . customer ?. email || ! isValidEmail ( data . customer . email )) {
errors . push ({ field: 'email' , message: 'Valid email required' });
}
// Validate card number
if ( data . paymentMethod . type === 'card' ) {
const cardNumber = data . paymentMethod . options . card . number ;
if ( ! isValidCardNumber ( cardNumber )) {
errors . push ({ field: 'cardNumber' , message: 'Invalid card number' });
}
}
return errors ;
}
Handle Authentication Errors
Invalid API Key
{
"error" : {
"code" : "UNAUTHORIZED" ,
"message" : "Invalid API key"
}
}
What to check:
API key is correct
Using the right environment (sandbox vs production)
Authorization header format: Bearer YOUR_API_KEY
Example
async function makeRequest ( endpoint , data ) {
try {
const response = await fetch ( endpoint , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ process . env . CHEQPAY_API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ( data )
});
if ( ! response . ok ) {
throw await response . json ();
}
return await response . json ();
} catch ( error ) {
if ( error . code === 'UNAUTHORIZED' ) {
logger . error ( 'Invalid API key - check environment configuration' );
// Alert ops team
sendAlert ( 'API key issue detected' );
}
throw error ;
}
}
Handle 3DS Errors
Authentication Required
{
"paymentOrder" : {
"status" : "PAYER_AUTHENTICATION_CHALLENGE_REQUIRED"
},
"payerAuthentication" : {
"stepUpUrl" : "..." ,
"jwt" : "..."
}
}
This isn’t an error - it means 3DS is required. Display the authentication challenge.
Authentication Failed
{
"error" : {
"code" : "AUTHENTICATION_FAILED" ,
"message" : "Customer failed to complete authentication"
}
}
What to do:
Show friendly message: “We couldn’t verify your identity. Please try again.”
Allow customer to retry
Offer alternative payment method
3D Secure Guide Learn how to implement 3DS authentication
Rate Limiting
If you exceed rate limits:
{
"error" : {
"code" : "RATE_LIMIT_EXCEEDED" ,
"message" : "Too many requests" ,
"retryAfter" : 60
}
}
Handle Rate Limits
async function makeRequestWithRateLimit ( endpoint , data ) {
try {
return await makeRequest ( endpoint , data );
} catch ( error ) {
if ( error . code === 'RATE_LIMIT_EXCEEDED' ) {
const retryAfter = error . retryAfter || 60 ;
logger . warn ( `Rate limited. Retrying after ${ retryAfter } s` );
await sleep ( retryAfter * 1000 );
return await makeRequest ( endpoint , data );
}
throw error ;
}
}
Contact [email protected] if you consistently hit rate limits. We can increase your limits.
Network Errors
Handle network connectivity issues:
async function makeRequestWithNetworkRetry ( endpoint , data , maxRetries = 3 ) {
for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
try {
return await makeRequest ( endpoint , data );
} catch ( error ) {
// Check if it's a network error
if ( error . code === 'ECONNREFUSED' ||
error . code === 'ETIMEDOUT' ||
error . code === 'ENOTFOUND' ) {
if ( attempt < maxRetries - 1 ) {
const delay = Math . pow ( 2 , attempt ) * 1000 ;
logger . warn ( `Network error, retrying in ${ delay } ms` );
await sleep ( delay );
continue ;
}
}
throw error ;
}
}
}
Error Logging
Log errors for debugging and monitoring:
function logError ( error , context ) {
logger . error ( 'Payment error' , {
errorCode: error . code ,
errorMessage: error . message ,
statusCode: error . status ,
timestamp: new Date (). toISOString (),
context: {
externalId: context . externalId ,
amount: context . amount ,
customerId: context . customerId
},
// Don't log sensitive data
// cardNumber: NEVER LOG THIS
});
// Send to error tracking service
if ( process . env . NODE_ENV === 'production' ) {
Sentry . captureException ( error , { extra: context });
}
}
Never log sensitive data like full card numbers, CVCs, or API keys.
Error Monitoring
Set up alerts for critical errors:
const errorThresholds = {
DECLINED: 0.15 , // Alert if >15% decline rate
INTERNAL_ERROR: 0.01 , // Alert if >1% server errors
UNAUTHORIZED: 0.001 // Alert immediately
};
function trackError ( error ) {
metrics . incrementError ( error . code );
const errorRate = metrics . getErrorRate ( error . code );
const threshold = errorThresholds [ error . code ] || 0.05 ;
if ( errorRate > threshold ) {
sendAlert ({
title: `High ${ error . code } rate detected` ,
message: ` ${ error . code } rate: ${ ( errorRate * 100 ). toFixed ( 2 ) } %` ,
severity: 'high'
});
}
}
Best Practices Summary
Use Customer-Friendly Messages
Show simple, non-technical error messages to customers. Log detailed errors for debugging.
Retry temporary failures with exponential backoff. Use idempotency to prevent duplicates.
Validate data before sending to API to provide instant feedback and reduce errors.
Log all errors with context for debugging. Never log sensitive data like card numbers.
Track error rates and set up alerts for unusual patterns or critical errors.
Offer alternative payment methods when primary method fails.
Next Steps