Error Handling
The ZenPays REST API uses standard HTTP status codes and returns detailed error information in JSON format.
Error Response Format
All error responses follow this structure:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": {
// Additional error context (optional)
}
}
}
HTTP Status Codes
| Status | Description |
|---|---|
200 | Success |
201 | Resource created |
400 | Bad Request - Invalid parameters |
401 | Unauthorized - Invalid or missing authentication |
403 | Forbidden - Insufficient permissions |
404 | Not Found - Resource doesn't exist |
409 | Conflict - Resource state conflict |
422 | Unprocessable Entity - Validation error |
429 | Too Many Requests - Rate limit exceeded |
500 | Internal Server Error |
502 | Bad Gateway - TSP provider error |
503 | Service Unavailable |
Error Codes
Authentication Errors (401)
| Code | Description |
|---|---|
UNAUTHORIZED | Missing or invalid API key |
INVALID_SIGNATURE | HMAC signature verification failed |
TIMESTAMP_EXPIRED | Request timestamp outside allowed window |
API_KEY_REVOKED | API key has been revoked |
API_KEY_EXPIRED | API key has expired |
Authorization Errors (403)
| Code | Description |
|---|---|
FORBIDDEN | Access denied |
IP_NOT_WHITELISTED | Request from unauthorized IP |
INSUFFICIENT_PERMISSIONS | API key lacks required scope |
MERCHANT_SUSPENDED | Merchant account is suspended |
Validation Errors (400/422)
| Code | Description |
|---|---|
INVALID_REQUEST | Request format is invalid |
VALIDATION_ERROR | One or more fields failed validation |
INVALID_AMOUNT | Amount is invalid or out of range |
INVALID_CURRENCY | Currency code not supported |
INVALID_PAYMENT_METHOD | Payment method not available |
MISSING_REQUIRED_FIELD | Required field is missing |
Resource Errors (404/409)
| Code | Description |
|---|---|
RESOURCE_NOT_FOUND | Requested resource doesn't exist |
PAYMENT_INTENT_NOT_FOUND | Payment intent ID is invalid |
TRANSACTION_NOT_FOUND | Transaction ID is invalid |
CUSTOMER_NOT_FOUND | Customer ID is invalid |
REFUND_NOT_FOUND | Refund ID is invalid |
DUPLICATE_REQUEST | Request with same idempotency key exists |
STATE_CONFLICT | Resource is in conflicting state |
Payment Errors
| Code | Description |
|---|---|
PAYMENT_FAILED | Payment processing failed |
PAYMENT_DECLINED | Payment was declined by provider |
PAYMENT_EXPIRED | Payment intent has expired |
INSUFFICIENT_FUNDS | Customer has insufficient funds |
CARD_DECLINED | Card was declined |
FRAUD_DETECTED | Suspicious activity detected |
LIMIT_EXCEEDED | Transaction limit exceeded |
Channel Limit Errors (422)
These errors occur during payment confirmation when the amount violates min/max limits configured per payment method and currency.
| Code | Description |
|---|---|
CHANNEL_LIMIT_MIN_EXCEEDED | Amount is below the minimum for the selected payment method |
CHANNEL_LIMIT_MAX_EXCEEDED | Amount exceeds the maximum for the selected payment method |
{
"success": false,
"error": {
"code": "CHANNEL_LIMIT_MIN_EXCEEDED",
"message": "The payment amount of 100 INR is below the minimum limit of 500 INR for UPI. Please use a different payment method or increase the amount.",
"type": "validation_error",
"details": {
"amount": 100,
"currency": "INR",
"paymentMethod": "UPI",
"minimumAmount": 500,
"maximumAmount": 49967
}
}
}
Refund Errors
| Code | Description |
|---|---|
REFUND_FAILED | Refund processing failed |
REFUND_NOT_ALLOWED | Transaction cannot be refunded |
REFUND_AMOUNT_EXCEEDED | Refund amount exceeds original |
REFUND_WINDOW_CLOSED | Refund period has expired |
Rate Limit Errors (429)
| Code | Description |
|---|---|
RATE_LIMIT_EXCEEDED | Too many requests |
Response includes retry information:
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Retry after 60 seconds",
"details": {
"limit": 1000,
"remaining": 0,
"resetAt": "2024-01-15T10:31:00.000Z"
}
}
}
Provider Errors (502)
| Code | Description |
|---|---|
TSP_ERROR | Payment provider returned an error |
TSP_TIMEOUT | Payment provider request timed out |
TSP_UNAVAILABLE | Payment provider is unavailable |
Operational Errors
These codes describe higher-level "we couldn't even attempt this" failures —
distinct from numeric transaction-state codes (bank decline, insufficient
funds, etc.). They surface in error.code of the standard error envelope and
the checkout SDK's payment-failed socket event.
| Code | HTTP | Customer-facing message | What it means | Suggested action |
|---|---|---|---|---|
NO_TSP_AVAILABLE | 503 | "We can't process this payment right now. Please try again in a few minutes, or contact support if the issue persists." | No active payment routes are configured to fulfill the request. | Retry after a short delay; escalate to support if persistent. |
UNSUPPORTED_REGION | 400 | "This payment route isn't available in your region right now. Please try a different method or contact support." | TSPs exist but none support the customer's country. | Offer an alternate method or surface a region-specific support flow. |
UNSUPPORTED_CURRENCY_PAIR | 400 | "We couldn't set up this payment with our provider. Try another currency or contact support." | The provider rejected the source/destination currency combination. | Suggest a supported currency. |
PROVIDER_VALIDATION_ERROR | 400 | "We hit a problem setting up your payment. Please double-check your details and try again, or contact support." | Upstream provider returned a 4xx the platform couldn't normalize further. | Inspect details.providerResponse server-side; ask the customer to retry. |
PROVIDER_UNAVAILABLE | 502 | "Our payment partner is having trouble right now. Please try again in a few minutes." | Upstream provider returned 5xx or timed out. | Retry with backoff; consider failover routing. |
PAYMENT_STUCK | 200 | "This is taking longer than usual. We're still trying — feel free to wait, or come back via your dashboard." | The intent has been in processing past the heartbeat threshold (~90s) with no transitions. Advisory only — not a terminal failure. | Show a friendlier "still working" UI; keep the subscription alive. |
PAYMENT_RETRIES_EXHAUSTED | 400 | "We tried several times but couldn't complete this payment. Please try a different payment method or contact support." | Retry budget exhausted; intent moved to canceled. | Offer a different method; investigate the last TSP failure for the underlying cause. |
TSP_PAYMENT_FAILED | 400 | "Your payment couldn't be completed. Please try a different payment method or contact your bank." | TSP / webhook reported a terminal payment failure. | Surface to the customer; inspect details.providerResponse for upstream reason. |
CRYPTO_DEPOSIT_FAILED | 400 | "We couldn't confirm your crypto deposit. If you've already sent funds, contact support with the transaction hash." | Crypto deposit reported failure (chain reorg, address mismatch, amount divergence). | Capture the tx hash and route to support. |
These codes also appear as code on the WebSocket payment-failed event
(see Payment Webhooks).
Validation Error Details
Validation errors include field-specific details:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": {
"fields": [
{
"field": "amount",
"message": "Amount must be greater than 0",
"code": "min"
},
{
"field": "currency",
"message": "Currency must be a valid ISO 4217 code",
"code": "invalid"
}
]
}
}
}
Error Handling Examples
- JavaScript
- Python
async function createPayment(amount, currency) {
try {
const response = await fetch('https://api.zenpayz.com/api/v1/payment-intents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount, currency }),
});
const data = await response.json();
if (!response.ok) {
// Handle specific error codes
switch (data.error?.code) {
case 'RATE_LIMIT_EXCEEDED':
const retryAfter = data.error.details?.resetAt;
console.log(`Rate limited. Retry after: ${retryAfter}`);
break;
case 'VALIDATION_ERROR':
data.error.details?.fields?.forEach(field => {
console.log(`${field.field}: ${field.message}`);
});
break;
case 'UNAUTHORIZED':
case 'INVALID_SIGNATURE':
console.log('Authentication failed. Check your API key and signature.');
break;
default:
console.log(`Error: ${data.error?.message}`);
}
return null;
}
return data.data;
} catch (error) {
console.error('Network error:', error.message);
return null;
}
}
import requests
def create_payment(amount: int, currency: str):
try:
response = requests.post(
"https://api.zenpayz.com/api/v1/payment-intents",
headers={
"Authorization": f"Bearer {api_key}",
"X-Timestamp": timestamp,
"X-Signature": signature,
"Content-Type": "application/json",
},
json={"amount": amount, "currency": currency},
)
data = response.json()
if not response.ok:
error = data.get("error", {})
code = error.get("code")
if code == "RATE_LIMIT_EXCEEDED":
retry_after = error.get("details", {}).get("resetAt")
print(f"Rate limited. Retry after: {retry_after}")
elif code == "VALIDATION_ERROR":
for field in error.get("details", {}).get("fields", []):
print(f"{field['field']}: {field['message']}")
elif code in ("UNAUTHORIZED", "INVALID_SIGNATURE"):
print("Authentication failed. Check your API key and signature.")
else:
print(f"Error: {error.get('message')}")
return None
return data.get("data")
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
return None
Retry Strategy
For transient errors, implement exponential backoff:
- JavaScript
- Python
async function requestWithRetry(fn, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
const code = error.code;
// Only retry on transient errors
const retriableErrors = [
'RATE_LIMIT_EXCEEDED',
'TSP_TIMEOUT',
'TSP_UNAVAILABLE',
];
if (!retriableErrors.includes(code)) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s...
const delay = Math.pow(2, attempt) * 1000;
console.log(`Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
import time
def request_with_retry(fn, max_retries=3):
last_error = None
for attempt in range(max_retries):
try:
return fn()
except Exception as error:
last_error = error
code = getattr(error, "code", None)
# Only retry on transient errors
retriable_errors = [
"RATE_LIMIT_EXCEEDED",
"TSP_TIMEOUT",
"TSP_UNAVAILABLE",
]
if code not in retriable_errors:
raise error
# Exponential backoff: 1s, 2s, 4s...
delay = (2 ** attempt)
print(f"Retrying in {delay}s (attempt {attempt + 1}/{max_retries})")
time.sleep(delay)
raise last_error
Idempotency
To prevent duplicate operations, include an X-Idempotency-Key header:
curl -X POST https://api.zenpayz.com/api/v1/payment-intents \
-H "Authorization: Bearer zp_live_xxxxx" \
-H "X-Idempotency-Key: unique-request-id-123" \
-H "Content-Type: application/json" \
-d '{"amount": 1000, "currency": "USD"}'
If the same idempotency key is used within 24 hours, the API returns the original response instead of creating a duplicate.
Next Steps
- Authentication - Learn about request signing
- Error Handling Guide - SDK error handling
- Webhooks - Handle async notifications