Error Handling
Understand the error response format and common error codes to handle failures gracefully.
Error Response Format
When an error occurs, the API returns a JSON response with the following structure:
{"success": false,"error": {"code": "ERROR_CODE","message": "Human-readable description"}}
Error Codes
| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED | 401 | Invalid or missing API keys, or the group these keys belong to has been deactivated |
INVALID_REQUEST | 400 | Missing or invalid parameters |
BILLING_REQUIRED | 403 | Complete billing setup in dashboard |
BILLING_SUSPENDED | 403 | Account suspended for non-payment |
TRANSACTION_NOT_FOUND | 404 | No payment with this ID exists in your group |
NOT_FOUND | 404 | The requested endpoint does not exist. Check the URL and HTTP method. |
ALREADY_COMPLETED | 400 | Payment can no longer be cancelled. Only pending or processing payments can be cancelled; any other status (for example succeeded, failed, expired or already cancelled) returns this error, and the message includes the current status. |
NO_ELIGIBLE_ACCOUNT_FOR_AMOUNT | 422 | Create Payment only. No account in your group that currently has capacity is configured to accept a payment of this amount. This usually points to a routing configuration issue, so retrying the same request right away is unlikely to help, although a later retry can succeed once an account that accepts this amount has capacity again. |
NO_ELIGIBLE_ACCOUNT_FOR_MODE | 422 | Create Payment only. No account in your group matches the group's live/test routing setting. This is a routing configuration issue, so retrying the same request will not help. |
NO_AVAILABLE_SAFESITE | 503 | No SafeSites available for processing |
STRIPE_ERROR | 502 | Error communicating with payment processor |
RATE_LIMIT_EXCEEDED | 429 | Too many requests. Wait for the number of seconds in the Retry-After header before retrying. See Rate Limits below. |
INTERNAL_ERROR | 400 / 413 / 415 | The request body could not be read: invalid or empty JSON (400), a body larger than 1 MB, that is 1,048,576 bytes (413), or a missing or unsupported Content-Type (415). The message describes the problem. Fix the request rather than retrying it. |
INTERNAL_ERROR | 500 | Something went wrong |
INTERNAL_ERROR is not always a server error, so check the HTTP status as well as the code. Send request bodies as JSON with Content-Type: application/json, and only send that header on requests that have a body. For example, a DELETE request with Content-Type: application/json and no body is rejected with HTTP 400.
Handling Errors
Always check the success field in the response and handle errors appropriately:
async function createPayment(amount, currency, customerEmail) {const response = await fetch('https://pay.ultrapay.cc/api/v1/payments/create', {method: 'POST',headers: {'X-Public-Key': process.env.ULTRAPAY_PUBLIC_KEY,'X-Secret-Key': process.env.ULTRAPAY_SECRET_KEY,'Content-Type': 'application/json',},body: JSON.stringify({amount,currency,customerEmail,successUrl: 'https://yoursite.com/success',cancelUrl: 'https://yoursite.com/cancel',}),});const data = await response.json();if (!data.success) {switch (data.error.code) {case 'UNAUTHORIZED':throw new Error('Invalid API keys. Check your configuration.');case 'INVALID_REQUEST':throw new Error(`Invalid request: ${data.error.message}`);case 'NO_AVAILABLE_SAFESITE':throw new Error('Payment processing temporarily unavailable.');case 'RATE_LIMIT_EXCEEDED': {// Wait the number of seconds in Retry-After, then retry.// Rate-limited requests are rejected before processing, so no payment was created.const retryAfterSeconds = Number(response.headers.get('Retry-After')) || 1;await new Promise(resolve => setTimeout(resolve, retryAfterSeconds * 1000));return createPayment(amount, currency, customerEmail);}default:throw new Error(data.error.message);}}return data.data;}
Rate Limits
| Limit | Value |
|---|---|
| Requests per minute | 100 |
| Counted per | Public API key (the X-Public-Key header, when it starts with upp_), otherwise per client IP address |
Requests over the limit are rejected with HTTP 429 and error code RATE_LIMIT_EXCEEDED before they are processed. Wait for the number of seconds in the Retry-After header before retrying. API responses include these headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Requests allowed per window |
X-RateLimit-Remaining | Requests left in the current window (0 when rate-limited) |
X-RateLimit-Reset | Seconds until the current window resets |
Retry-After | Sent on 429 responses only. Seconds to wait before retrying. |