Browse documentation

HTTP errors use this shape:

{
  "error": {
    "code": "invalid_request",
    "message": "The request is invalid.",
    "requestId": "req_123"
  }
}

The Node SDK throws SubKitApiError with code, status, requestId, and a redacted message. Expo purchase states such as pending or cancelled are expected outcomes and are not automatically transport errors.

Retry policy

The shared contract marks only these codes retryable by default:

  • network
  • rate_limited
  • service_unavailable
  • server_error

Use bounded exponential backoff, preserve the same idempotency key for the same server mutation, and respect Retry-After when supplied. Do not retry validation, auth, capability, ownership, beneficiary, or idempotency conflicts unchanged.

Codes

Code Typical status Retry unchanged? Reaction
cancelled 400 No Treat as normal user intent.
not_ready 409/503 Later Keep access locked; wait for initialization/sync.
store_unavailable 503 Yes Show temporary Store outage.
product_unavailable 409 No Refresh offerings; never substitute a static Store ID.
already_owned 409 No Refresh/restore CustomerInfo.
network 503 Yes Preserve bounded offline authority and retry.
validation_failed 400/422 No Fix request/provider evidence.
ownership_conflict 409 No Keep access locked and offer support/restore guidance.
login_required 401 After login Identify the app user, then retry.
beneficiary_conflict 409 No Resolve the selected beneficiary explicitly.
device_selection_required 409 After selection Show device activation choices.
device_replacement_cooldown 409 Later Display the next eligible time.
device_change_limit_reached 409 No Explain policy; require operator/support action.
device_replaced 409 After refresh Refresh device and CustomerInfo state.
rate_limited 429 Yes Back off and honor Retry-After.
unauthorized 401 No Replace missing/invalid key; do not expose it in logs.
forbidden 403 No Use a correctly scoped key/capability.
not_found 404 No Check app-scoped identifier; do not infer cross-app existence.
invalid_request 400 No Correct shape, headers, or required reason.
idempotency_conflict 409 No Do not reuse a key for different evidence.
webhook_verification_failed 401/400 No unchanged Fix provider signature/topic configuration.
service_unavailable 503 Yes Retry with backoff; keep fail-closed behavior.
server_error 500 Yes Retry safely and retain the request ID for support.
unknown 500 No by default Fail closed; capture the request ID.

Domain decisions versus technical failure

inactive and device_blocked are Effective Access decisions, not transport errors. device_blocked means the requested commercial entitlement is active but this installation needs recovery. Render the typed recovery path; do not ask the user to buy again.

offline_unavailable and error are separate lifecycle states: no bounded access decision is currently available. Keep access fail-closed and offer the appropriate retry or diagnostics UI.

Server entitlement checks similarly return allowed: false with a domain reason. Do not turn a domain denial into an unbounded retry loop or crash.

Secret hygiene

Never log bearer keys, receipts, purchase tokens, raw Store payloads, invitation tokens, or webhook bodies. SDK-generated errors redact sensitive evidence; keep application telemetry equally strict.

Type to search…

↑↓ navigate↵ selectEsc close