Errors
Error reference
LuniPay follows a Stripe-style error shape. Every 4xx and 5xx response has a typed error body — code, message, and the specific parameter that caused it, if any.
The error envelope
Every non-2xx response has a consistent shape. Build your error handler against error.code — it is stable and machine-readable.
{
"error": {
"type": "invalid_request_error",
"code": "parameter_missing",
"message": "Missing required parameter: success_url",
"param": "success_url"
}
}Fields:
type— high-level category. See the table below.code— stable machine-readable identifier. Always branch on this, never on the message.message— human-readable explanation safe to log. Not safe to show directly to end users.param— optional. When present, it names the request parameter that triggered the error.
Error types
| Type | Status | Meaning |
|---|---|---|
| invalid_request_error | 400, 404, 409, 422 | The request was understood but rejected — missing fields, invalid values, resource not found, state conflict. |
| authentication_error | 401 | The API key is missing, malformed, revoked, or for the wrong livemode. |
| permission_error | 403 | The key is valid but not authorized for this resource — e.g., a publishable key trying to create a customer. |
| rate_limit_error | 429 | You are sending requests too quickly. Back off exponentially. |
| api_error | 500, 502, 503 | Something went wrong on our side. Safe to retry — preferably with an idempotency key. |
Common error codes
| Code | When |
|---|---|
| parameter_missing | A required field was not supplied. |
| parameter_invalid | A field has the wrong type, wrong format, or fails a constraint. |
| resource_missing | A referenced id does not exist in the current livemode. |
| resource_already_exists | Attempted to create a row that collides with a unique constraint (e.g., duplicate customer email). |
| resource_in_use | Cannot delete because dependent rows still reference the object. |
| idempotency_error | An idempotency key was reused with a different request body. |
| amount_too_small | The amount is below the minimum chargeable value for the currency. |
| invalid_api_key | Authorization header is missing, malformed, revoked, or for the wrong livemode. |
| rate_limited | You are exceeding the request-per-second quota for your plan. |
Handling errors
A solid strategy for every client: branch on the HTTP status first, then on error.code for the 4xx family. Retry 5xx with exponential backoff and an idempotency key. Surface nothing to your end users — customer-facing messages belong to your UI, not the API.
import LuniPay, {
InvalidRequestError,
RateLimitError,
ApiError,
} from 'lunipay';
import { randomUUID } from 'node:crypto';
const lunipay = new LuniPay(process.env.LUNIPAY_SECRET_KEY!);
async function createSession(body) {
try {
return await lunipay.checkout.sessions.create(body, {
idempotencyKey: randomUUID(),
});
} catch (err) {
if (err instanceof InvalidRequestError) {
if (err.code === 'amount_too_small') {
throw new UserVisibleError('Please enter at least $0.50.');
}
throw new UserVisibleError(err.message);
}
if (err instanceof RateLimitError) {
throw new RetryableError('Try again in a few seconds.');
}
if (err instanceof ApiError) {
// 5xx — safe to retry with the same idempotency key.
throw new RetryableError('LuniPay is having trouble — retrying shortly.');
}
throw err;
}
}Log the whole envelope
Your logs should include
error.type, error.code, error.param, and the request id from the Request-Id response header. It makes support investigations almost painless.