Skip to main content

Error Handling

The Open Pay API uses conventional HTTP status codes and a consistent error response format. This guide covers every error scenario and how to handle them.

Error Response Format

All errors follow a standard JSON structure:
{
  "error": {
    "code": "INVALID_AMOUNT",
    "message": "Amount must be a positive number greater than 0.01"
  }
}
FieldTypeDescription
error.codestringMachine-readable error code (uppercase snake_case)
error.messagestringHuman-readable explanation of the error

HTTP Status Codes

StatusMeaningWhen It Happens
400Bad RequestMalformed JSON, missing required fields, invalid parameter values
401UnauthorizedMissing or invalid API key / bearer token
403ForbiddenValid credentials but insufficient permissions (e.g., accessing another merchant’s data)
404Not FoundResource does not exist (e.g., invalid payment_id)
409ConflictDuplicate operation (e.g., payment already completed, idempotency key reused with different parameters)
422Unprocessable EntitySemantically invalid request (e.g., unsupported token, amount below minimum)
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnexpected server error. Retry with backoff.

Common Error Codes

CodeStatusDescriptionFix
INVALID_API_KEY401API key is malformed or revokedCheck your API key in the Merchant Portal
EXPIRED_TOKEN401JWT bearer token has expiredRefresh your token via POST /v1/auth/refresh-token
MISSING_AUTH_HEADER401No Authorization header providedAdd Authorization: Bearer <token> header
INSUFFICIENT_PERMISSIONS403API key lacks required scopeGenerate a new API key with the correct permissions
TWO_FA_REQUIRED403Operation requires 2FA verificationComplete 2FA challenge first

Rate Limiting

The API enforces rate limits to ensure fair usage. When you exceed the limit, you receive a 429 response. Rate limit headers are included in every response:
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Default limits:
TierRate Limit
Standard100 requests / minute
Pro500 requests / minute
EnterpriseCustom
Example 429 response:
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Retry after 1711454400."
  }
}

Handling Rate Limits

async function apiRequest(url: string, options: RequestInit, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status !== 429) {
      return response;
    }

    // Use the reset header to calculate wait time
    const resetTimestamp = parseInt(
      response.headers.get('X-RateLimit-Reset') || '0',
      10
    );
    const waitMs = Math.max(resetTimestamp * 1000 - Date.now(), 1000);

    console.warn(`Rate limited. Waiting ${waitMs}ms before retry ${attempt + 1}`);
    await new Promise(r => setTimeout(r, waitMs));
  }

  throw new Error('Rate limit exceeded after maximum retries');
}

Retry Strategies

1

Identify Retryable Errors

Only retry on these status codes:
  • 429 - Rate limit exceeded (wait for X-RateLimit-Reset)
  • 500 - Internal server error (transient)
  • 502/503/504 - Gateway errors (transient)
Do not retry 400, 401, 403, 404, 409, or 422 errors. These require fixing the request.
2

Use Exponential Backoff

Increase the delay between retries exponentially with jitter:
function getBackoffDelay(attempt: number): number {
  const baseDelay = 1000; // 1 second
  const maxDelay = 30000; // 30 seconds
  const exponential = baseDelay * Math.pow(2, attempt);
  const jitter = Math.random() * 1000;
  return Math.min(exponential + jitter, maxDelay);
}
3

Set a Maximum Retry Count

Limit retries to 3-5 attempts. If the request still fails, log the error and alert your monitoring system.
4

Use Idempotency Keys

For POST requests (especially payment creation), include an Idempotency-Key header so retries don’t create duplicate resources:
curl -X POST https://olp-api.nipuntheekshana.com/v1/payments \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: order-1042-payment-v1" \
  -H "Content-Type: application/json" \
  -d '{ "amount": "25.00", "currency": "USD" }'
Idempotency keys are scoped to your merchant account and expire after 24 hours. Using the same key with the same parameters returns the original response without creating a new resource.

Complete Error Handling Example

import { OpenPay, OpenPayError } from '@openpay/sdk';

const client = new OpenPay({ apiKey: 'sk_live_...' });

async function createPaymentSafely(params: CreatePaymentParams) {
  try {
    const payment = await client.payments.create(params, {
      idempotencyKey: `order-${params.metadata.orderId}-v1`,
    });
    return payment;
  } catch (err) {
    if (err instanceof OpenPayError) {
      switch (err.code) {
        case 'INVALID_AMOUNT':
          console.error('Fix the amount:', err.message);
          break;
        case 'RATE_LIMIT_EXCEEDED':
          console.warn('Rate limited, retrying...');
          await new Promise(r => setTimeout(r, 5000));
          return createPaymentSafely(params); // Retry
        case 'IDEMPOTENCY_MISMATCH':
          console.error('Duplicate key with different params');
          break;
        default:
          console.error(`API error [${err.status}]: ${err.code} - ${err.message}`);
      }
    }
    throw err;
  }
}
Never expose raw API error messages to end users. Map error codes to user-friendly messages in your application.