Skip to main content

Error Format

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 CodeMeaningCommon Causes
400Bad RequestInvalid parameters, validation errors
401UnauthorizedInvalid or missing API key
404Not FoundResource doesn’t exist
409ConflictDuplicate resource, conflicting state
422Unprocessable EntityPayment declined, business logic error
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer error (rare)
503Service UnavailableTemporary service disruption

Error Codes

Client Errors (400-499)

CodeStatusMeaningAction
VALIDATION_ERROR400Invalid request parametersCheck and fix parameters
UNAUTHORIZED401Invalid API keyVerify credentials
NOT_FOUND404Resource not foundCheck resource ID
DUPLICATE409Resource already existsUse existing resource
DECLINED422Payment declined by bankTry different card
INSUFFICIENT_FUNDS422Customer has low balanceContact customer
EXPIRED_CARD422Card has expiredRequest new card details
INVALID_CARD422Invalid card numberRe-enter card information
AUTHENTICATION_REQUIRED4223DS authentication neededShow 3DS challenge
AUTHENTICATION_FAILED4223DS verification failedAllow customer to retry

Server Errors (500-599)

CodeStatusMeaningAction
INTERNAL_ERROR500System errorRetry request with backoff
SERVICE_UNAVAILABLE503Temporary issueRetry 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 ReasonCustomer-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

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