Skip to Content
ReferenceError codes

Error codes

HTTP error codes returned by the token signing handler and the widget session API.


Token signing handler errors

createUnchurnHandler returns these HTTP responses. Source: packages/widget/src/server.ts.

StatusConditionBody
200Token minted successfullyUnchurnTokenResponse JSON
401resolveUser returned null (unauthenticated or no subscription)empty
405Request method is not POSTempty, Allow: POST header
500resolveUser threw an exceptionempty
500resolveUser returned a non-object or invalid shapeempty
500Token mint failed (e.g. subscriptionId contains illegal characters)empty

All error bodies are empty — no information is leaked to the browser. Errors are logged server-side.

resolveUser shape validation:

If resolveUser returns a value where:

  • subscriptionId is not a non-empty string → 500
  • mode is not 'test', 'live', or undefined → 500
  • subscriptionId contains characters outside [A-Za-z0-9_-] → 500 (detected at mint time by assertValidIdentifier)

Widget session API errors

The Unchurn backend returns structured errors for widget requests.

Error response shape

export const apiErrorResponseSchema = z.object({ error: z.object({ code: apiErrorCodeSchema, message: z.string(), docs: z.string().url().optional(), example_file: z.string().optional(), offer_kind: offerKindSchema.optional(), }), });
FieldTypeDescription
error.codeApiErrorCodeMachine-readable code (see table below)
error.messagestringHuman-readable description
error.docsstring (URL)Optional documentation link
error.example_filestringOptional suggested file path for the fix
error.offer_kind'plan_switch' | 'trial_extension' | 'pause' | 'discount'Which retention offer the rejection refers to (only on policy-rejection errors)

Error codes

CodeHTTP statusWhen it firesWhat to do
bad_request400Malformed request bodyFix the widget integration
unauthorized401Auth rejection not covered by a more specific codeCheck message; confirm UNCHURN_SECRET matches the dashboard
auth_token_required401require_signed_widget=true but no authToken was suppliedSupply a signed widget token
auth_token_invalid401Token present but signature, payload, or mode is wrongRegenerate token server-side
auth_token_expired401Token present but exp is in the past (TTL 600 s)Regenerate; useUnchurn retries automatically
signing_not_configured401Token required or supplied but merchant has no widget_signing_secretSet a signing secret in the dashboard
forbidden403Policy rejection not covered by a more specific code
direct_cancel_disabled403Cancel Now clicked but direct_cancel_access was false at session creation
not_found404Session or resource not found
conflict409Conflict not covered by a more specific code
stripe_not_connected409Stripe Connect not yet completed for this merchantComplete the Stripe Connect OAuth flow
session_already_completed409/complete called twice for the same terminal outcome; Stripe side-effect already committedWidget dismisses quietly — idempotent
refresh_required409Stale flow-config or session snapshotWidget refreshes the session and retries
pause_not_eligible_interval409Pause offer unavailable — subscription is not billed monthly
unsupported_plan_type422Subscription price shape is outside the support matrix (metered, tiered, one-time)See supported subscriptions
rate_limited429Too many requestsBack off and retry
stripe_error502Stripe API returned an errorCheck Stripe status; retry
manual_cancellation_unavailable503Transient infra or Stripe failure on the manual-cancellation requestRetry
operation_in_progress503Concurrent mutation already claimed the sessionWidget retries shortly
internal500Unexpected server errorContact support

SDK error state

useUnchurn exposes an error: Error | null field. createUnchurn().open() and window.unchurn.open({ tokenEndpoint }) reject their returned promises for the same token/runtime failures.

  • Token endpoint returned a non-2xx status: Error('Token endpoint returned HTTP <status>')
  • Token endpoint returned a malformed response body: a Zod validation error describing which field was wrong

In React, these surface as the hook’s error field so the merchant can show a degraded UI instead of crashing. In non-React code, catch the returned promise.