Skip to content
revenue-sdk
Esc
navigateopen⌘Jpreview
On this page

Error codes

The full RevenueErrorCode union with its HTTP-status mapping, typical causes, retry semantics, and the client's single bounded rate-limit retry.

A complete reference of the RevenueErrorCode union — with the HTTP statuses each code is derived from and its retry semantics. For the conceptual overview see Errors.

The RevenueError model

PropType
code?RevenueErrorCode

Normalized failure code — the field to branch on.

TypeRevenueErrorCode
provider?'polar' | 'lemon-squeezy' | 'stripe' | 'paddle' | 'dodo-payments' | 'testing'

Which provider produced the error.

Type'polar' | 'lemon-squeezy' | 'stripe' | 'paddle' | 'dodo-payments' | 'testing'
status?number

The HTTP status, when the failure came from a response.

Typenumber
retryable?boolean

Whether retrying the operation may succeed.

Typeboolean
retryAfter?number

Seconds to wait, parsed from Retry-After (numeric seconds or an HTTP date).

Typenumber
cause?unknown

The parsed provider response body, or the underlying fetch error.

Typeunknown

RevenueError extends Error, so error instanceof RevenueError and error.message work as usual. name is always 'RevenueError'.

The code union

RevenueErrorCode is a closed union of ten values:

code Typical cause HTTP status retryable
validation Bad parameters, from the provider or a client-side check 400, 422 no
unauthorized Missing, invalid, or revoked credentials 401 no
payment_required The provider refused for billing reasons (e.g. a declined card) 402 no
forbidden Authenticated, but not allowed 403 no
not_found The resource does not exist, or is gone 404, 410 no
conflict Conflicts with current state, or a precondition failed 409, 412 no
rate_limited The provider’s rate limit was hit 429 yes
provider_error An unclassified provider response any other status only when 5xx
unsupported The active provider can’t do what was requested no
network_error The request never completed (DNS, TLS, connection, timeout) yes

unsupported and network_error have no HTTP status: the first is raised before a request is made, the second when the request never produced a response.

Errors raised before any request

These never touch the network:

Code Raised by
validation An empty id, customerId, product, or items[].product; an empty items array; a quantity that isn’t a positive integer
validation A malformed pagination cursor, a cursor from another provider, or a cursor URL on a different origin
unsupported Capability gating — successUrl, returnUrl, customerId on a subscription list, reason/comment, uncancel, endTrial, revoke, and a prorationBehavior outside capabilities.prorationBehaviors

Adapters raise unsupported at call time for the finer-grained limits a capability boolean can’t express — see the capability matrix.

validation is also raised by parseWebhookEvent when the payload isn’t valid JSON. Note that verifyWebhook never throws — it returns false.

retryable and retryAfter

retryable defaults to true for rate_limited and network_error, and for any error whose HTTP status is 5xx. retryAfter is parsed from the provider’s Retry-After header, accepting either numeric seconds or an HTTP date (converted to seconds from now, never negative).

Provider behavior on rate limits:

Provider Retry-After on 429 Notes
Polar yes The client’s bounded retry engages.
Lemon Squeezy usually
Stripe no Signals retryability via Stripe-Should-Retry; use your own back-off.
Paddle usually
Dodo Payments usually

The client’s bounded retry

One retry, and only when all three hold:

  1. the code is rate_limited,
  2. retryAfter is defined,
  3. retryAfter <= maxRetryAfterSeconds (default 10).
const client = createClient({
  provider,
  retry: { maxRetryAfterSeconds: 10 }, // 0 disables it
});

It applies to every call, including each page fetched inside listAll.

Secret redaction

RevenueError redacts the configured API key or access token from its message before construction, replacing it with [redacted].

Handling errors

import { RevenueError } from 'revenue-sdk';

try {
  await client.subscriptions.changePlan({ id, product, prorationBehavior: 'none' });
} catch (error) {
  if (error instanceof RevenueError) {
    switch (error.code) {
      case 'unsupported':
        // this provider lacks the capability — check client.capabilities
        break;
      case 'payment_required':
        // the customer's payment failed — surface it, don't retry
        break;
      case 'not_found':
        // the subscription or price no longer exists
        break;
      case 'rate_limited':
        // back off using error.retryAfter
        break;
      default:
        if (error.retryable) {
          // transient — retry with back-off
        }
    }
  }
  throw error;
}

Last updated on August 6, 2026

Was this page helpful?