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
code?RevenueErrorCode
Normalized failure code — the field to branch on.
RevenueErrorCodeprovider?'polar' | 'lemon-squeezy' | 'stripe' | 'paddle' | 'dodo-payments' | 'testing'
Which provider produced the error.
'polar' | 'lemon-squeezy' | 'stripe' | 'paddle' | 'dodo-payments' | 'testing'status?number
The HTTP status, when the failure came from a response.
numberretryable?boolean
Whether retrying the operation may succeed.
booleanretryAfter?number
Seconds to wait, parsed from Retry-After (numeric seconds or an HTTP date).
numbercause?unknown
The parsed provider response body, or the underlying fetch error.
unknownRevenueError 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:
- the code is
rate_limited, retryAfteris defined,retryAfter <= maxRetryAfterSeconds(default10).
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;
}