Error handling

The problem-document format, the full slug catalogue, and which errors are worth retrying.

Every error is an RFC 7807 problem document with content type application/problem+json. Errors are not wrapped in the data/meta envelope.

{
  "type": "urn:qup:error:checkout-reservation-expired",
  "title": "reservation has expired",
  "status": 404,
  "detail": "the reservation has expired, please place a new one",
  "instance": "urn:qup:correlation:0f4c8e1a-9b2d-4c7e-8a1f-2d3b4c5e6f70"
}
FieldAlways?Use it for
statusYesThe HTTP status, repeated in the body.
titleYesHuman-readable summary. Do not branch on it — it is prose and gets reworded.
typeNourn:qup:error:<slug>. The stable, machine-readable identity of the failure. Branch on this.
detailNoA safe explanation of the specific failure, suitable for showing a developer.
instanceYesurn:qup:correlation:<uuid>. Log it. Quote it when you report a problem.

When type is absent

type and detail accompany a named failure — one the platform can describe safely. Two
kinds of response arrive without them:

  1. Unexpected server faults. A 500 carrying only a title means the cause was not something we
    can put in a response body. Quote the instance correlation id and we can trace it.
  2. Orders and tickets. The fulfillment routes answer with
    status, title and instance. Branch on the HTTP status there, and on type elsewhere.

Raw infrastructure errors are never echoed back, so a missing detail means the underlying cause
was not safe to share rather than that the response is incomplete. The correlation id is the way
in.

Status codes and what they mean here

StatusMeaning in this APIRetry?
400Malformed request — bad body, missing required field, unparseable date, missing Idempotency-KeyNo. Fix the request.
401Missing or invalid credential — yours, or one required by a connected external systemNo. Re-authenticate, then retry once.
403Valid token, insufficient scope, or credentials bound to another campaignNo. Contact us.
404Resource absent — also returned when a resource exists but is not yoursNo.
409State conflict — idempotency collision, code already redeemed, tickets not ready yetYes, with backoff.
410Voucher permanently exhaustedNo.
422Well-formed but cannot be processed — unavailable product, expired reservation, date too closeNo. Change the request.
429Throttled (voucher validation is rate-limited)Yes. Respect Retry-After.
500Unexpected server faultYes.
502A downstream system — PSP, ticket provider, loyalty partner — failed or rejectedYes, cautiously. See below.
504Upstream timeout, e.g. catalog searchYes.

404 hides ownership on purpose

Where a resource belongs to a specific customer — a fulfillment order, a checkout bound to a signed-in user — asking for someone else's returns 404, not 403. The two cases are indistinguishable by design, so order ids cannot be enumerated across users. Do not read a 404 as proof the id is wrong.

The exception is guest checkouts: a checkout created without a session has no owner, so the checkout_id itself is the capability. Anyone holding it can read it. Treat a guest checkout_id as a secret.

502 needs thought, not blind retries

A 502 means we reached a third party and it failed. Whether a retry is safe depends on the endpoint: on a read it always is; on confirm checkout the safe retry is the same request with the same Idempotency-Key, which either replays the original result or re-attempts cleanly. Never re-issue a confirm with a fresh key after a 502 — that is how you double-charge someone.

Retry policy

Retry only 429, 500, 502, 503 and 504. Use truncated exponential backoff with full jitter:

delay = min(30s, 1s * 2^attempt) * random(0, 1)
AttemptMax delay before jitter
12 s
24 s
38 s
416 s
530 s (capped)

Stop after 5 attempts and surface the failure. When a Retry-After header is present, honour it and ignore the formula for that interval — it is a real signal, not a guess. For anything carrying an Idempotency-Key, retry with the same key; see Idempotency.

Slug catalogue

Every type a retailer-facing endpoint can return, grouped by area. The slug is the part after urn:qup:error:.

Checkout

SlugStatusMeaning
checkout-not-found404No checkout with that id
checkout-reservation-expired404The 30-minute reservation window elapsed
checkout-reservation-invalid422Reservation no longer valid — start a new checkout
checkout-basket-empty400No product lines submitted
checkout-basket-line-invalid400A line requests an invalid quantity
checkout-basket-spans-providers400Basket mixes products from different fulfillment providers
checkout-basket-spans-merchants400Basket mixes products from different merchants
checkout-date-in-past400Requested visit date is before today
checkout-shipping-address-required400Basket has a physical product; address fields are incomplete
checkout-timeslot-required400A product needs a time of day, not just a date
checkout-locale-invalid400Accept-Language missing or unusable
checkout-product-not-priceable422A product is unavailable for the requested date
checkout-product-not-fulfillable422A product cannot currently be fulfilled
checkout-billing-detail-rejected422Billing/shipping details rejected — unknown country, invalid e-mail
checkout-fulfillment-request-rejected422The ticket provider refused the booking
checkout-order-failed502The payment system could not confirm the order
checkout-order-empty-redirect502The payment system returned no payment URL
checkout-reservation-placement-failed502Reservation could not be placed downstream
checkout-fulfillment-reservation-failed502Fulfillment could not be started
checkout-discount-resolution-failed502Discounts could not be resolved
checkout-basket-pricing-failed500Our pricing failed — not your request
checkout-discount-allocation-failed500Discount could not be applied to the basket
checkout-merchant-lookup-failed500Merchant details could not be resolved
checkout-debtor-code-unresolved500The settlement account for the basket could not be resolved
checkout-product-classification-failed500Basket classification failed
campaign-not-found404The campaign is not configured — tell us

Sessions and authentication

SlugStatusMeaning
authorization-code-not-found404Code never existed or expired
authorization-code-already-redeemed409Code already spent
authorization-challenge-failed401 / 502Magic-link challenge could not be created or exchanged
refresh-token-invalid401Refresh token expired, revoked or malformed — re-authenticate
identity-invalid401Partner identity proof is invalid
identity-token-expired401Partner identity proof expired
identity-not-allowed401Identity issuer is not on the allow-list
identity-verify-failed403Identity could not be verified
identity-provider-unavailable502Partner identity provider is down
session-mint-failed500Session could not be created
user-magic-link-send-failed502Magic-link e-mail could not be sent
user-no-callback-url400No usable callback_url could be resolved

Catalog

SlugStatusMeaning
merchant-not-found404No such merchant in this campaign
product-not-found404No such product
merchant-search-timed-out504Search backend timed out — retry
merchant-search-failed502Search backend failed
merchant-internal-error, product-internal-error500Server-side fault — retry, then quote the correlation id

Vouchers

SlugStatusMeaning
voucher-not-found404Unknown code in this campaign
voucher-revoked409Code was revoked
voucher-exhausted410No redemptions remaining
voucher-not-yet-valid422Before valid_from
voucher-expired422After valid_until
voucher-type-not-configured422Campaign has no award configured for this voucher type
voucher-redemption-effect-auth-required401The campaign's connected system needs a credential that was not supplied
voucher-redemption-effect-auth-rejected401Partner rejected the credential — refresh it
voucher-redemption-effect-failed502Partner could not record the redemption

Loyalty wallet and memberships

SlugStatusMeaning
wallet-balance-not-found404No balance for this user in this campaign
wallet-sync-auth-required401The connected system needs a credential that was not supplied
wallet-sync-provider-failed502Loyalty partner returned no balance
wallet-sync-campaign-unreachable500Campaign metadata unavailable
membership-lookup-failed500Memberships could not be read

Agreements

SlugStatusMeaning
agreement-auth-required401The connected system needs a credential that was not supplied
agreement-auth-rejected401Partner rejected the credential
agreement-invalid-request422Partner rejected the agreement ids
agreement-provider-unavailable502Partner CRM unreachable

Payments and analytics

SlugStatusMeaning
payment-not-found404No such payment
analytics-empty-batch400events was empty
analytics-ingest-failed502Batch could not be enqueued

What to log

For every failed call, record: the endpoint, the HTTP status, type, and instance. That is enough for us to resolve almost anything without a round trip. Never log the request body of a checkout confirm — it contains customer name, e-mail and address.


Did this page help you?