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

TypeStatusMeaning
invalid_request_error400, 404, 409, 422The request was understood but rejected — missing fields, invalid values, resource not found, state conflict.
authentication_error401The API key is missing, malformed, revoked, or for the wrong livemode.
permission_error403The key is valid but not authorized for this resource — e.g., a publishable key trying to create a customer.
rate_limit_error429You are sending requests too quickly. Back off exponentially.
api_error500, 502, 503Something went wrong on our side. Safe to retry — preferably with an idempotency key.

Common error codes

CodeWhen
parameter_missingA required field was not supplied.
parameter_invalidA field has the wrong type, wrong format, or fails a constraint.
resource_missingA referenced id does not exist in the current livemode.
resource_already_existsAttempted to create a row that collides with a unique constraint (e.g., duplicate customer email).
resource_in_useCannot delete because dependent rows still reference the object.
idempotency_errorAn idempotency key was reused with a different request body.
amount_too_smallThe amount is below the minimum chargeable value for the currency.
invalid_api_keyAuthorization header is missing, malformed, revoked, or for the wrong livemode.
rate_limitedYou 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.