Errors
Every failure is JSON with the same shape. There is no other error format.
{ "error": { "type": "invalid_request_error", "code": "mint_not_allowed", "message": "That token is not accepted on this platform.", "param": "mint" }}Branch on code, never on message. Codes are contract; messages are written for humans and
will be reworded. param names the offending field when one field is at fault.
type is a coarse grouping and does not map cleanly onto the status: session_required is a
403 with type invalid_request_error, not authentication_error. Branch on code and read
type as a hint only.
Retry or not
Section titled “Retry or not”The single most useful distinction, because getting it wrong either loses money or duplicates it.
| Status | Retry? |
|---|---|
| 400, 403, 404, 409 | No. The request is wrong or the state has moved on. Retrying sends the identical request and gets the identical answer. |
| 401 | Only after fixing credentials. A retry with the same key fails identically. |
| 429 | Yes, after retry-after seconds. The response carries retry-after and ratelimit-* headers. |
| 500 | Yes, with backoff, and with an Idempotency-Key on anything that creates. |
| 413 | No. Send less. |
Authentication
Section titled “Authentication”| Code | Status | What it means |
|---|---|---|
unauthenticated |
401 | No API key and no session. Send authorization: Bearer sk_... or sign in. |
invalid_credentials |
401 | The email or password is wrong. Deliberately does not say which. |
insufficient_scope |
403 | A read-only key attempted a write. Scope is enforced by HTTP method: read keys may only GET. |
session_required |
403 | This route refuses API keys and needs a signed-in human. It guards /v1/payout_wallet, so a stolen key cannot redirect where your money lands. |
owner_required |
403 | A platform-administration route. Not available to merchants. |
wallet_proof_required |
401 | You asked whether a wallet is entitled without proving you are entitled to ask. Send the purchase’s client_secret. (A payer wallet-session route also satisfies this; it is used by the hosted wallet page and is not documented for integrators yet.) |
wallet_not_signed_in |
401 | A payer-facing route needs a wallet session. Sign a challenge first. |
invalid_wallet_signature |
401 | The signature does not verify against the address, or the challenge expired or was already used. Request a fresh one; each is single-use. |
invalid_client_secret |
401 | The client_secret does not match this mandate. They are scoped to one purchase and one payer. |
Your request
Section titled “Your request”| Code | Status | What it means |
|---|---|---|
parameter_missing |
400 | A required field is absent. param names it. |
parameter_invalid |
400 | Present but unusable: a malformed address, a negative amount, an amount that is not a string of atomic units. |
invalid_json |
400 | The body did not parse. |
request_too_large |
413 | Over 256 KiB. Counted as the bytes arrive, so the connection is closed rather than buffered. |
resource_missing |
404 | No such product, mandate, charge or link, or it is not yours. Tenancy failures look identical to absence on purpose. |
unknown_route |
404 | No such endpoint. |
email_taken |
409 | An account already exists with that email. |
acknowledgement_required |
400 | Signup needs acknowledged: true, recording that you understand Jorvi never holds funds and cannot reverse a settled payment. |
Selling
Section titled “Selling”| Code | Status | What it means |
|---|---|---|
recurring_unavailable |
400 | This deployment settles one-time purchases only. Set sellMode: "once". It refuses rather than accepting a subscription it cannot renew. |
not_for_sale_once |
400 | You tried to buy outright something offered only by subscription. |
not_for_sale_subscription |
400 | You tried to subscribe to something sold only outright. |
no_payout_wallet |
400 | Set and prove a payout wallet before taking live payments. There is nowhere for the money to go. |
mint_not_allowed |
400 | That token is not on the platform registry. GET /v1/tokens lists what is accepted, and that route needs authorization: Bearer $KEY. |
decimals_unsupported |
400 | The token’s decimals are outside what the pricing code handles safely. |
floor_token |
400 | The platform’s default token cannot be disabled or removed. |
Paying
Section titled “Paying”| Code | Status | What it means |
|---|---|---|
transaction_not_issued |
400 | The signed transaction is not the one we handed you. Only the exact transaction Jorvi issued can pay an order. |
broadcast_unavailable |
400 | No chain is configured behind this deployment, so nothing can be broadcast. |
purchases_unavailable |
400 | One-time purchases need a chain. Same cause. |
receipts_unavailable |
400 | Reading a receipt needs a chain to read it from. |
onchain_proof_unavailable |
400 | On-chain wallet proof is not enabled on this deployment. Prove the wallet by signing the challenge message instead, which needs no chain and no fee. |
mandate_not_pending |
409 | Already activated, or revoked. A mandate is activated once. |
already_revoked |
409 | It was already cancelled. |
verification_failed |
402 | Could not confirm the wallet holds enough to start. |
refund_exceeds_charge |
400 | A refund can never exceed what was received. |
refund_failed |
400 | The chain refused the refund. message carries what it said. |
wallet_verification_failed |
400 | The wallet ownership proof did not check out. |
Ours, and rate limits
Section titled “Ours, and rate limits”| Code | Status | What it means |
|---|---|---|
rate_limit_exceeded |
429 | Too many requests. Wait retry-after seconds. ratelimit-limit and ratelimit-remaining are on every response, not only refusals. |
request_in_flight |
409 | The same Idempotency-Key is being processed right now. The key is claimed before the work, so a double-submit cannot run twice. Wait and retry. |
internal_error |
500 | Ours. Safe to retry with backoff; use an Idempotency-Key on creates. |
test_mode_only |
400 | A /v1/test/* helper on a deployment that has none. Production builds do not construct them, so they cannot be switched on by configuration. |
Idempotency
Section titled “Idempotency”Send Idempotency-Key on anything that creates. Keys are scoped to your account, so two merchants
both using order-1 never collide.
Replaying a key returns the original response. If the first request is still in flight you get
request_in_flight (409) rather than a second execution: the key is claimed before the work
starts, not after it finishes.