Error Handling and Operational Notes
Hilt is intentionally not hiding every backend failure behind a single generic envelope. That means your client should be prepared to handle standard FastAPI error responses and a few route-specific behaviors.
Error shape
Most route failures come back as:
{
"detail": "Human readable message"
}
Examples:
{
"detail": "Authentication required"
}
{
"detail": "Payment session has expired"
}
Some older surfaces may still emit lightweight error or message fields. If you are building a backend client, normalize both patterns:
- prefer
detailfirst - fall back to
message - then fall back to
error
Common status codes
400 Bad Request
Used when the route itself is well-formed but the action is invalid.
Examples:
- unsupported
product_type - malformed OAuth state
- invalid Solana RPC payload
401 Unauthorized
Used when the caller is missing valid auth or presents an invalid secret.
Examples:
- bad JWT
- revoked API key
- invalid Telegram webhook secret
403 Forbidden
Used when the caller is authenticated but not allowed.
Examples:
- suspended account
- non-staff caller attempting an admin-only action
- connector handoff secret mismatch
404 Not Found
Used when the resource does not exist or is not visible to the caller.
Examples:
- product id not found
- receipt id not found
- ticket id not found
409 Conflict
Used when the current state makes the request invalid.
Examples:
- payment session already linked to a different transaction
- ticket closed
- email already registered
410 Gone
Used for expired buyer-side sessions.
Examples:
- checkout payment session expired
- identity session expired
422 Unprocessable Entity
Used for validation errors and state checks.
Examples:
- invalid Solana wallet address
- identity verification required before confirmation
- unsupported key permissions
- bad billing payload
429 Too Many Requests
Used for:
- API key plan limits
- rate limiting on
/v1/*
Rate-limited responses may include:
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-After
Practical rule:
- treat the response headers as authoritative
- do not hard-code your own idea of when a limit resets
- back off and retry after
Retry-Afterwhen it is present
503 Service Unavailable
Used when a required runtime dependency is not configured or is temporarily unavailable.
Examples:
- Stripe billing not configured
- Hilt fee wallet missing
- SOL pricing oracle unavailable or stale
- connector handoff not configured
Buyer checkout state machine
The hosted checkout is easiest to integrate if you treat payment state as a lifecycle, not a single button press.
Typical progression:
PENDING_SIGNATUREPENDING_CONFIRMATIONCONFIRMED
Possible failure states:
FAILED- expired session
- delivery failure while payment is still confirmed
Important rule:
- do not ask the buyer to sign again just because confirmation is still pending
- keep the original
payment_id - keep the original
tx_signature - use
GET /v1/payments/{payment_id}to track state
This is especially important for mobile wallet flows where app-switch timing can make the UI feel behind the chain for a short period.
Identity and delivery failure handling
For Telegram or Discord membership flows:
- confirm the identity handshake before payment confirmation
- if delivery fails after payment settles, the membership may still be valid
- use:
POST /v1/memberships/{membership_id}/retry-delivery
That lets you recover delivery without replaying the payment itself.
API key operational guidance
Treat keys as server-side credentials:
- generate them in the dashboard or merchant app
- store them in a secrets manager
- never expose them in browser code
- rotate on personnel or deployment changes
- inspect
usage_summaryandrecent_activityto understand what each key is doing
Current plan-level key quotas are documented in Security Best Practices.
Practical retry guidance
Safe retry candidates:
GETrequestsGET /v1/payments/{payment_id}POST /v1/memberships/{membership_id}/retry-deliveryPOST /v1/billing/refresh/stripe
Be more careful with:
POST /v1/productsPOST /v1/keysPOST /v1/support/ticketsPOST /v1/pay/confirm
For those, keep your own idempotency or local request tracking if you are building a custom backend workflow around Hilt.
Practical async guidance
The most common developer mistake is treating settlement, membership activation, delivery, and receipt visibility as one atomic browser event.
They are related, but they are not the same moment.
Safer pattern:
- keep
payment_id - poll
GET /v1/payments/{payment_id} - once confirmed, read membership state
- if needed, read receipt state
- if delivery alone failed, retry delivery instead of replaying payment
For the provider side of that flow, read Webhooks.