Errors and status handling

Use this page to decide what to retry, what to trust, and what counts as a terminal answer.

Typical error shape

Most Hilt route failures come back as one of these shapes:
If you are normalising client errors, read:
  1. detail.code, then code, then error
  2. detail.message, then detail, then message
  3. the request id header, usually X-Hilt-Request-Id or X-Request-Id
The official SDKs expose these values as HiltApiError.code, statusCode or status_code, requestId or request_id, retryable, and docsUrl or docs_url.

Public error-code catalog

Use the error code as the durable branch in your integration. Human-readable messages can become more specific over time.

payment_failed

The payment did not complete successfully. Example:
Developer action: treat the payment as terminal, show the buyer a fresh payment path, and keep using Hilt payment or entitlement state instead of wallet UI state.

subscription_expired

The paid-through period has ended. Example:
Developer action: stop serving the protected resource and create a new payment session when the customer wants access again. For durable access, check entitlement before serving. For metered work, atomically consume a unit for each billable request.

invalid_authorization

The API key, bearer token, or permission scope is missing or invalid for the route. Example:
Developer action: check that the request uses the right auth surface, that server-to-server calls use X-Hilt-Key, and that the key has the required scope.

webhook_signature_failed

The webhook payload could not be verified against the endpoint signing secret. Example:
Developer action: verify the raw request body, parse X-Hilt-Signature as t=<unix_timestamp>,v1=<hex_hmac_sha256>, reject stale timestamps, and use the signing secret returned when the webhook endpoint was created or rotated.

rate_limited

The client is sending too many requests. Example:
Developer action: back off, honor Retry-After when present, and avoid retry loops that create new payment sessions before reading current Hilt state.

setup_not_ready

The product, rail, webhook, billing, or live configuration needs attention before the requested live action can proceed. Example:
Developer action: call GET /v1/access/setup/readiness, complete the returned customer-facing actions, and retry the live request only after readiness is clear.

entitlement_missing

Hilt does not have an active entitlement for the customer and product. Example:
Developer action: return HTTP 402 Payment Required from the protected resource, create a Hilt payment session, and re-check entitlement after payment.

subscription_cancelled

The native subscription authorization has been cancelled. Example:
Developer action: stop future collection attempts and continue to use entitlement checks for paid-through access until Hilt reports access is no longer active.

subscription_requires_reapproval

The subscription needs buyer reapproval before future automatic renewals can continue. Example:
Developer action: ask the buyer to complete the current Hilt reapproval path and do not assume future periods will renew until Hilt entitlement and webhook state show renewal is active.

request_timeout

The SDK did not receive a response before its local timeout. Example:
Developer action: retry reads with backoff. For writes, reuse the same Idempotency-Key and read current Hilt state before creating a replacement object.

Idempotency errors

Hilt Pay API write routes require an Idempotency-Key header so retries do not create duplicate live objects.

idempotency_key_required

The write request did not include a usable idempotency key. Example:
Developer action: generate a stable key for the intended operation, keep it at least 8 characters, and reuse it for retries of the same request body.

idempotency_key_too_long

The idempotency key is longer than Hilt accepts. Example:
Developer action: shorten the key to 255 characters or fewer. Prefer compact keys such as session-cust-123-pro-api-001.

idempotency_key_invalid

The idempotency key contains whitespace, control characters, or non-visible ASCII. Example:
Developer action: use visible ASCII characters only. Avoid spaces, tabs, newlines, and copied values with hidden characters.

invalid_idempotency_key

Some SDK and integration layers use this broader client-side code when an idempotency key fails local validation before a request is sent. Example:
Developer action: normalize your idempotency-key generator to match Hilt’s public rules: at least 8 characters, at most 255 characters, visible ASCII, and no whitespace.

idempotency_in_progress

Another request with the same idempotency key and request body is still processing. Example:
Developer action: retry the same request with the same key after a short backoff, or read the relevant object if your app can identify it.

idempotency_conflict

The same idempotency key was already used with a different request body. Example:
Developer action: do not retry with that key. Use the original request body for retries, or generate a new key only when you intentionally want a separate write.

idempotency_race

Hilt could not yet read the idempotency record that another concurrent request just created. Example:
Developer action: retry the same request with the same key after a short backoff.

Common HTTP meanings

Payment states that matter most

Expiry handling

Expiry usually appears one of two ways:
  • 410 during the active buyer session
  • FAILED with an expiry-style failure reason when Hilt later finalises the payment record
Treat both as a clean restart, not as a prompt for the buyer to sign the same session again.

Payment behavior that is normal

Treat these as normal:
  • a payment taking time to settle
  • repeated polling before a terminal state appears
  • the buyer seeing wallet success before your backend sees final Hilt status
Treat these as issues:
  • asking the buyer to sign again while the first payment is still pending
  • building business logic from screenshots instead of Hilt state

Edge cases worth handling deliberately

Duplicate payment attempts

If your checkout flow already has a live payment_id, keep using it until that payment reaches a terminal state. Do not start a second payment just because:
  • the buyer refreshed the page
  • the wallet popup closed late
  • confirmation is still catching up

Duplicate confirm calls

If your own app calls POST /v1/pay/confirm more than once for the same transaction, read the current payment state before trying again. The right recovery pattern is:
  1. read GET /v1/payments/{payment_id}
  2. if the payment is already CONFIRMED, continue
  3. if the payment is still transitional, wait and poll
  4. if the payment is terminal and failed, start a clean new checkout

Expired sessions

If a checkout session has expired:
  • treat it as closed
  • create a fresh session
  • do not ask the buyer to sign the old one again

Identity handoff expiry

If a signed handoff link is no longer valid, create a fresh handoff token and send the buyer back through a new checkout URL. Do not patch around this by manually guessing the buyer identity on the backend.

Good polling pattern

Use a simple progression:
  1. poll quickly just after payment starts
  2. slow down once the payment is clearly in PENDING_CONFIRMATION
  3. stop polling once the payment is CONFIRMED or FAILED
The important rule is to keep the same payment_id. Do not create a second payment just because confirmation is still catching up.

Safe retry guidance

Safe retries usually mean:
  • retrying reads
  • retrying lookups
  • retrying your own app-side automation after reading current Hilt state
Unsafe retries usually mean:
  • starting a second payment before the first one is clearly failed or expired
  • mutating access state without checking the current payment, membership, or receipt trail

Timeout and backoff guidance

Use calmer retry behavior as a payment ages: This gives buyers a responsive experience early without turning your backend into a busy loop.

Good retry candidates

  • GET /v1/products
  • GET /v1/products/{product_id}
  • GET /v1/payments/{payment_id}
  • GET /v1/memberships
  • GET /v1/memberships/lookup
  • GET /v1/receipts
  • POST /v1/memberships/{membership_id}/retry-delivery

Routes that deserve more care

  • POST /v1/products
  • PATCH /v1/products/{product_id}
  • POST /v1/pay/confirm
  • POST /v1/support/tickets
For those, keep your own request correlation and read the current Hilt state before replaying the action.

Practical recovery sequence

When something looks wrong:
  1. read GET /v1/payments/{payment_id}
  2. inspect the related membership if access is involved
  3. inspect the related receipt if proof is involved
  4. open or append a support ticket if a human conversation is needed

A strong correlation habit

Even without a separate idempotency header, you can keep your integration safe by treating payment_id as the canonical correlation key for:
  • retries
  • queue jobs
  • buyer progress tracking
  • post-payment automation
That one habit prevents a large class of duplicate-processing mistakes. That sequence is much more reliable than reacting to the buyer UI alone.

A good developer rule

Use Hilt states for:
  • payment truth
  • access truth
  • receipt truth
That keeps your backend aligned with the same merchant surface the dashboard shows.

Common questions

How should my integration handle Hilt errors?

Use the HTTP status, Hilt response body, rate-limit headers, and current Hilt object state together. For ambiguous cases, read the current payment or membership before retrying a write.

Can payment confirmation be delayed?

Yes. Treat pending payment states as normal while the buyer signs or the transaction settles, then stop polling once Hilt returns a terminal state.

What should I retry safely?

Retry reads and transient failures first. Be more careful with writes such as product creation, payment confirmation, and support ticket creation because those can create lasting records.