Jeton Merchant API
Overview
The Jeton Merchant API lets a merchant accept payments, move money out of its accounts, exchange currencies, read balances and ledger activity, and follow every outcome through events and webhooks.
Conventions that apply to every endpoint:
- Resources, not verbs.
POST /payoutscreates,GET /payouts/{id}reads,GET /payoutslists. Anything else is an action sub-resource, such asPOST /payouts/requirements. - HTTP status set is exactly seven:
200,202,400,401,404,429,5xx. A movement that does not go through once accepted is not an HTTP error — it is a resource whosestatussays so: a payoutfailed, a payment the customer never paysexpired. - Success bodies are bare resources (no envelope). Every resource carries
object,id,createdAtand, where applicable,status. - Errors are RFC 9457
application/problem+jsonwith a stablesnake_casecode, atraceIdand optional field-levelerrors[]. Every code is described in Errors. - Money is a string decimal object
{"value":"149.90","currency":"EUR"}. Never a float, never minor units. - Enums are
lowercase_snake_case. Timestamps are RFC 3339 UTC with milliseconds (2026-09-18T10:15:30.123Z). - Enums you receive are open. Any enum this API returns — a resource's
statusortype, an event'stype— can gain a value without a new version. Treat a value you do not know as "other": handle it generically, never fail on it, and configure a client generated from this document to accept unknown values. Enums you send stay closed: an unknown value is400 validation_failed. - Lists are cursor paginated:
?limit=&cursor=→{data, nextCursor, hasMore}.
Authentication (HMAC request signing)
Every request carries four headers:
| Header | Value |
|---|---|
Jeton-Api-Key |
The public key id (nbm_live_…), created in the merchant portal under API Keys. |
Jeton-Timestamp |
Unix seconds. Must be within ±300 s of server time. |
Jeton-Nonce |
Client-generated random string, 16–128 printable non-space ASCII characters (CSPRNG; a UUID is fine). Single use per key within the freshness window. |
Jeton-Signature |
hex(HMAC-SHA256(apiSecret, canonicalRequest)) |
The canonical request is:
METHOD # upper-case, e.g. POST
/v2/payouts # the path from /v2 on (/v2, then the path as this document lists it), lower-case, no trailing slash
limit=20&status=pending # query: each key=value RFC 3986-encoded (space is %20), sorted by key then value, joined with &; empty string when none
1758190530 # Jeton-Timestamp
5f1c… # Jeton-Nonce
e3b0c442… # lower-case hex SHA-256 of the raw body; sha256("") for bodiless requests
joined with \n. Path parameters are part of the path (/v2/payouts/txn_…); repeated query keys
produce one key=value pair each. The API secret never travels on the wire. Every authentication failure returns
the same neutral 401 problem (code: unauthorized) — unknown key, bad signature, stale timestamp,
replayed nonce and IP-allowlist rejection are indistinguishable by design.
Idempotency
Send an Idempotency-Key header (1–255 chars, UUID recommended) on every POST that moves money
(/payments, /payouts, /transfers, /fx/exchanges). It is required there and optional elsewhere.
- Same key + same request → the original response is replayed byte-for-byte with the original
status code and content type, plus
Idempotent-Replayed: true. Business rejections (4xx) are replayed too — the decision was final. - Same key + different request →
400 idempotency_key_reused. "Same request" = same method, same path and byte-identical body; changing anything means a new key. - Same key while the first request is still executing →
400 request_in_progress+Retry-After: 1. - Missing header on a money-moving
POST→400 idempotency_key_required; malformed →400 idempotency_key_invalid. - A
5xxoutcome is not retained: retrying with the same key runs the request again. For a payout or a transfer that never means a second movement — the platform keeps every request it took and answers a retry with what the first attempt made, or makes it once; a request left unanswered and not retried for 15 minutes is settled for you (answered with the movement if one was made, otherwise400 request_not_executed). If the replay store itself is unreachable the request is refused with500 internal_errorrather than accepted without a replay guarantee.
Keys are scoped to the API key and retained for 24 hours by the replay; a payout or transfer request is
kept for good, so the same key never makes a second one, however late the retry. Every movement ALSO
carries your own merchantReference — the same field, under the same name, on payments, payouts,
transfers and exchanges. Reusing it under a new key is 400 duplicate_merchant_reference: on payments
and exchanges for good, on payouts and transfers while a movement holds it — a request that was refused
frees its reference, so a payout refused for insufficient funds can be sent again under the same
reference once the account is topped up. The two work together: the key makes a retry safe, the
reference makes a second payment of the same thing impossible. A replay returns the original body —
for a 202 that is the initial pending snapshot even if the movement has since completed.
Synchronous vs asynchronous
Creating a resource is always synchronous — the money movement behind it may not be. Every operation
declares x-execution: synchronous | asynchronous, and the status code of a given operation never
varies (no "200 if it happened to finish, 202 otherwise"):
| Operation | Code | Terminal outcome |
|---|---|---|
POST /payments |
200 — the deliverable is the checkout link |
GET /payments/{id}, GET /events; payment.* webhooks |
POST /payouts |
202 always, any destination |
body status, GET /payouts/{id}, GET /events; payout.* webhooks |
POST /fx/quotes |
200 — the deliverable is the price |
— |
POST /fx/exchanges |
202 always |
body status; a fiat exchange is normally completed already |
POST /transfers |
202 always |
body status; normally completed already, pending until it settles |
every GET |
200 |
— |
Reading a 202: treat the movement as in progress unless the body's status is already terminal
(completed or failed). Payouts on the internal ledger (jeton_account destinations) usually complete
inside the request and come back completed — that is an optimisation, not a contract; never branch on
destination type, always on status. A 202 carries Location (the resource
to poll) and, only while non-terminal, Retry-After.
Poll GET sparingly — it is rate limited. Webhooks plus GET /events reconciliation is the intended
integration pattern.
Retries
A 4xx response is safe to retry after fixing the request. On a timeout or 5xx retry with the
same Idempotency-Key — the replay guarantees you never create a duplicate.
Rate limiting
Limits are applied per API key over a fixed one-minute window (the key's configured requests per
second × 60; the current figure is in GET /me → rateLimit.requestsPerMinute). Every authenticated
/v2 response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the
window resets). An exceeded limit returns 429 rate_limit_exceeded with Retry-After; nothing is
executed and no Idempotency-Key is consumed. Spread bursts across the minute rather than front-loading them.
Webhooks
GET /events lists every status a payment, payout, transfer or exchange reached, with the resource as its GET
read it then. Your webhook receives the outcomes among them — one notification per result:
payment.completed|expired|cancelled|refunded, payout.completed|failed|cancelled,
transfer.completed|failed and fx_exchange.completed|failed|cancelled. The intermediate statuses (pending,
processing) are in GET /events only. A payment has no failed outcome: a
failed attempt leaves it pending while the customer may retry, and a payment never paid ends expired.
Your v2 webhook is set in the merchant portal's webhook settings, not through this API: one address for every
event, and its own signing secret (shown once; rotate it there). It is separate from a legacy (/v1, jetonbank)
notification address: while you move from a legacy integration to v2 you keep both, each at its own address — the
legacy one goes on receiving the legacy notifications of operations your legacy keys start, and the v2 webhook
receives these events for every operation, whichever channel started it (this API, the portal, a batch file or a
legacy key). Switched off, it receives nothing until switched back on; events raised meanwhile are delivered then
(within the event log's 30 days).
Deliveries are signed as the Standard Webhooks specification defines (webhook-id, webhook-timestamp,
webhook-signature), so any of its libraries verifies them, and retried 12 times over about three and a half
days; see the webhooks section at the bottom of this document. A request never names its own notification
address.