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:
{
  "detail": "Human readable message"
}
If you are normalising client errors, read:
  1. detail
  2. then message
  3. then error

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.