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:
{
  "detail": "Human readable message"
}
{
  "detail": {
    "code": "idempotency_key_required",
    "message": "Write requests require an Idempotency-Key header of at least 8 characters."
  }
}
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:
{
  "detail": {
    "code": "payment_failed",
    "message": "The payment failed before Hilt could activate access."
  }
}
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:
{
  "detail": {
    "code": "subscription_expired",
    "message": "The subscription paid-through period has expired."
  }
}
Developer action: stop serving the protected resource, create a new payment session when the customer wants access again, and keep checking entitlement before serving paid work.

invalid_authorization

The API key, bearer token, or permission scope is missing or invalid for the route. Example:
{
  "detail": {
    "code": "invalid_authorization",
    "message": "The request is not authorized for this Hilt route."
  }
}
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:
{
  "detail": {
    "code": "webhook_signature_failed",
    "message": "Invalid Hilt webhook signature."
  }
}
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:
{
  "detail": {
    "code": "rate_limited",
    "message": "Too many requests. Retry more slowly."
  }
}
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:
{
  "detail": {
    "code": "setup_not_ready",
    "message": "Complete setup readiness actions before creating live payment sessions."
  }
}
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:
{
  "detail": {
    "code": "entitlement_missing",
    "message": "No active entitlement was found for this customer."
  }
}
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:
{
  "detail": {
    "code": "subscription_cancelled",
    "message": "The subscription authorization is cancelled."
  }
}
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:
{
  "detail": {
    "code": "subscription_requires_reapproval",
    "message": "The subscription requires buyer reapproval before the next renewal."
  }
}
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:
{
  "code": "request_timeout",
  "message": "Hilt request timed out after 20000ms."
}
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:
{
  "detail": {
    "code": "idempotency_key_required",
    "message": "Write requests require an Idempotency-Key header of at least 8 characters."
  }
}
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:
{
  "detail": {
    "code": "idempotency_key_too_long",
    "message": "Idempotency-Key must be 255 characters or fewer."
  }
}
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:
{
  "detail": {
    "code": "idempotency_key_invalid",
    "message": "Idempotency-Key must be visible ASCII without whitespace."
  }
}
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:
{
  "code": "invalid_idempotency_key",
  "message": "The idempotency key is not valid for this request."
}
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:
{
  "detail": {
    "code": "idempotency_in_progress",
    "message": "A request with this Idempotency-Key is still being processed."
  }
}
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:
{
  "detail": {
    "code": "idempotency_conflict",
    "message": "This Idempotency-Key was already used with a different request body."
  }
}
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:
{
  "detail": {
    "code": "idempotency_race",
    "message": "Retry request."
  }
}
Developer action: retry the same request with the same key after a short backoff.

Common HTTP meanings

StatusMeaningTypical action
400The request shape is valid enough to parse, but the action itself is not allowedFix the request
401Session or API key is not validRe-authenticate or replace the key
403Caller is authenticated but not allowed to do that actionCheck workspace ownership or access level
404Object does not exist or does not belong to that workspaceRe-check the identifier and workspace context
408The chain has not confirmed quickly enough for the current confirm attemptKeep the same payment_id and retry later
409The object state conflicts with the requested actionRead current state before retrying
410The active session is no longer usableStart a fresh buyer session
422Payload shape is valid JSON, but not valid for this operationFix the request body or query parameters
429You are sending too many requestsBack off and retry more slowly
503A required runtime dependency is unavailableRetry later and surface a temporary error

Payment states that matter most

StatusMeaningWhat your app should do
PENDING_SIGNATUREBuyer session exists but the transaction is not signed yetWait for buyer action
PENDING_CONFIRMATIONThe transaction is signed or broadcast but not yet final in HiltPoll until a terminal state appears
CONFIRMEDPayment is final in HiltContinue into memberships, receipts, and post-payment logic
FAILEDPayment did not complete successfullyStop and show a recovery path

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:
SituationGood behavior
Fresh checkout just startedretry reads quickly
Payment has moved to PENDING_CONFIRMATIONslow down
Payment is older than expectedkeep the same payment_id, log the case, and continue with wider intervals
Payment is terminalstop polling
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.