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:

json
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description"
}
}

Error Codes

CodeHTTPDescription
UNAUTHORIZED401Invalid or missing API keys, or the group these keys belong to has been deactivated
INVALID_REQUEST400Missing or invalid parameters
BILLING_REQUIRED403Complete billing setup in dashboard
BILLING_SUSPENDED403Account suspended for non-payment
TRANSACTION_NOT_FOUND404No payment with this ID exists in your group
NOT_FOUND404The requested endpoint does not exist. Check the URL and HTTP method.
ALREADY_COMPLETED400Payment 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_AMOUNT422Create 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_MODE422Create 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_SAFESITE503No SafeSites available for processing
STRIPE_ERROR502Error communicating with payment processor
RATE_LIMIT_EXCEEDED429Too many requests. Wait for the number of seconds in the Retry-After header before retrying. See Rate Limits below.
INTERNAL_ERROR400 / 413 / 415The 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_ERROR500Something 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:

javascript
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

LimitValue
Requests per minute100
Counted perPublic 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:

HeaderDescription
X-RateLimit-LimitRequests allowed per window
X-RateLimit-RemainingRequests left in the current window (0 when rate-limited)
X-RateLimit-ResetSeconds until the current window resets
Retry-AfterSent on 429 responses only. Seconds to wait before retrying.
↑ ↓ navigate↵ select