Jeton Merchant API

Download OpenAPI

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 /payouts creates, GET /payouts/{id} reads, GET /payouts lists. Anything else is an action sub-resource, such as POST /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 whose status says so: a payout failed, a payment the customer never pays expired.
  • Success bodies are bare resources (no envelope). Every resource carries object, id, createdAt and, where applicable, status.
  • Errors are RFC 9457 application/problem+json with a stable snake_case code, a traceId and optional field-level errors[]. 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 status or type, an event's type — 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 is 400 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 5xx outcome 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, otherwise 400 request_not_executed). If the replay store itself is unreachable the request is refused with 500 internal_error rather 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.