Sessions
The GateSession is the unit of work for a single 0Gate buy, sell, or swap — created server-side with the amount and currency bound in, mounted in the browser with a client_secret, and driven to a terminal state by signed webhooks.
A GateSession is the unit of work for one 0Gate checkout: a single buy, sell, or swap. You create it on your server with the amount and currency bound in, hand a browser-safe client_secret to the page that mounts the widget, and 0Gate drives the session to a terminal outcome and tells you about it over signed webhooks. Everything in a 0Gate integration — the iframe, the embed handshake, the settlement signal — hangs off this one object. If you understand the session, you understand the integration.
Think of it the way a Stripe Checkout Session works: a server-created, server-bound record that a browser-safe token unlocks for exactly one user, exactly once.
What a session represents
One session is one attempt to move money for one user. It carries:
- The economic terms —
amountandcurrency, bound at creation on the server and not changeable from the browser. - The flow —
on_ramp(buy),off_ramp(sell),swap, or unlocked (the user picks a tab). - The destination — optional
target_token/target_network, plusreturn_url/cancel_url. - Its own identity and lifecycle — a server-only
id, a browser-safeclient_secret, astatus, and anexpires_at.
A session is not a quote, a user, or a payment method. It is the container that ties one priced intent to one widget instance and one eventual webhook. The KYC, payment-method selection, fiat rails, and on-chain settlement all happen inside that container, run by 0Bit — you never assemble those screens yourself.
0Bit owns compliance and settlement
A session runs against 0Bit's non-custodial compliance and settlement stack. KYC (by 0Bit's verification partner), AML and sanctions screening, fiat pay-in/pay-out, chargebacks, and the on-chain leg are all executed by 0Bit on every tier. You bring the user and the page chrome; the regulated machinery lives behind the session.
The GateSession object
Every read of a session returns the same shape. These are the fields the API guarantees (object is the discriminator gate_session; id, partner_id, mode, amount, currency, return_url, status, and expires_at are always present).
| Field | Type | Notes |
|---|---|---|
id | string | Server-assigned Mongo ObjectId hex, e.g. 67a1f3b9e4b0c10001234567. |
object | "gate_session" | Discriminator for client-side type narrowing. |
partner_id | string | The partner that owns this session. |
mode | test | live | Inherited from the sk_* key that created the session. |
flow | on_ramp | off_ramp | swap | null | Kit-block flow lock. null = open widget (user picks the tab). |
amount | string | Decimal string, bound at create. |
currency | string | ISO 4217 three-letter code. |
target_token | string | null | Optional partner constraint (e.g. USDC). |
target_network | string | null | Optional partner constraint (e.g. ETHEREUM, POLYGON). |
return_url | string (uri) | Where the iframe sends the user on success. |
cancel_url | string | null (uri) | Optional cancel destination. |
wallet_address | string | null | Pre-filled destination wallet (enterprise input). |
user_reference | string | null | Opaque partner-side id, echoed in webhooks. |
kyc_pre_verified | boolean | true only for a trusted partner that supplied a kyc_package. Default false. |
status | enum | open | completed | expired | cancelled. |
expires_at | string (date-time) | ISO-8601, default 24h after creation. |
created_at | string | null (date-time) | Creation timestamp. |
metadata | object | Opaque partner notes, returned verbatim. Not used for business logic. |
The raw client_secret is not part of this object — it appears only on the create response (see below). What 0Bit persists server-side is a client_secret_prefix plus a SHA-256 hash, never the plaintext.
Creating a session
You create a session with a secret key (sk_*) from your backend, against POST /v1/gate_sessions. The rate limit is 10 requests / minute / key, and an Idempotency-Key UUID is expected on every create.
curl -X POST https://gate-api.0bit.app/v1/gate_sessions \
-H "Authorization: Bearer $GATE_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"amount": "100.00",
"currency": "EUR",
"return_url": "https://app.example.com/return"
}'The response is the session, plus a one-time client_secret:
{
"id": "67a1f3b9e4b0c10001234567",
"object": "gate_session",
"client_secret": "gsec_67a1f3b9e4b0c10001234567_AbCdEfGhIjKlMnOpQrStUvWxYz012345",
"amount": "100.00",
"currency": "EUR",
"status": "open",
"expires_at": "2026-05-26T13:45:00Z"
}Request parameters
Only three fields are required: amount, currency, and return_url. Everything else narrows or pre-fills the flow.
| Param | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Decimal string, pattern ^[0-9]+(\.[0-9]{1,8})?$ — up to 8 fractional digits, must be > 0. Bound server-side; buy/sell calls against the session must match exactly. |
currency | string | Yes | ISO 4217 three-letter code, pattern ^[A-Za-z]{3}$ (e.g. EUR, USD). |
return_url | string (uri) | Yes | Where the iframe sends the user after a successful flow. HTTPS only (loopback allowed in dev); its origin must be in partner.allowed_domains. |
cancel_url | string (uri) | No | Where the user is sent if they abandon the flow. |
target_token | string | No | Lock the buy/swap target token. Pattern ^[A-Za-z0-9]{2,12}$ (e.g. USDC). |
target_network | string | No | Lock the network. Pattern ^[A-Za-z0-9_-]{2,30}$ (e.g. ETHEREUM, POLYGON). |
flow | on_ramp | off_ramp | swap | No | Kit-block flow lock. When set, the widget forces the user into the chosen flow and hides the buy/sell/swap tab strip. Omit for the open widget where the user picks. |
wallet_address | string | No | Enterprise input. Pre-filled destination wallet, max 128 chars. Validated against the resolved network at flow time, not at create. Accepted from any partner. |
user_reference | string | No | Enterprise input. Opaque partner-side user/order id, max 128 chars. Echoed in webhook payloads so you can correlate events back to your record. Accepted from any partner. |
kyc_package | object | No | Enterprise, contract-gated. Partner-supplied pre-verified KYC. Only accepted when partner.kyc_trusted: true; otherwise returns 403 kyc_package_not_trusted. Shape is opaque; provider (string) is required for audit. On acceptance the session is created with kyc_pre_verified: true and a redacted gate_session.kyc_package_accepted webhook fires. The raw body is never echoed in any API response. |
metadata | object | No | Opaque per-session notes, returned verbatim. Not used for business logic — a good place for your own order reference. |
Flow lock vs the open widget
Set flow when you've already captured the user's intent on your own surface — a "Buy crypto" button maps to on_ramp, "Sell" to off_ramp. The widget then drops the user straight into the right step instead of showing the tab strip. Leave it unset to ship the legacy open widget where the user chooses. The flow you bind here is echoed back through bootstrap so the iframe can lock its UI to match.
SDK typed surface is a subset
The hand-written Node/Python SDK create params currently expose amount, currency, return_url, cancel_url, target_token, target_network, and metadata. The newer enterprise fields (flow, wallet_address, user_reference, kyc_package) are in the API contract but not yet in the typed SDK params — pass them through the raw request body until the SDK types catch up.
Created server-side — amount and currency can't be tampered with
The reason this matters is trust. Because the amount and currency are fixed on the server when the session is created, a hostile or buggy browser cannot change what the user is charged. The widget can only execute the terms the session was created with — if the iframe ever tries to transact a different amount, 0Gate rejects it. That rejection is tamper protection working as designed, not a bug in your integration.
This binding is enforced end to end: when the browser redeems the client_secret at bootstrap, the embed token comes back stamped with the session's amount, currency, target_token, target_network, and flow, and every subsequent iframe call is checked against those locked values. The browser receives the figures to render them, never to set them.
This is also why the create call needs a secret key and the page does not: minting the priced intent is a privileged, server-only operation. The browser only ever receives the client_secret, which can do nothing except open this session.
Never create sessions from the browser
sk_* keys can move money and must stay on your server. Browsers only ever see the client_secret. The publishable key (pk_*) the page uses authorizes a single thing — POST /v1/embed/bootstrap — and cannot create, cancel, or read sessions.
Create errors
| Status | When |
|---|---|
400 | Validation error — missing amount/currency/return_url, malformed value, or a pattern violation. message may be an array, one entry per failed field. |
401 | Missing or malformed credential. |
403 | Credential rejected — return_url origin not in allowed_domains, or kyc_package sent by a partner that is not kyc_trusted (code: kyc_package_not_trusted). |
429 | Over the 10/min create limit. |
The error envelope is the unified cross-product shape:
{
"type": "...",
"code": "...",
"message": "...",
"request_id": "...",
"doc_url": null,
"statusCode": 000
}An X-Request-Id response header accompanies every response (matching request_id). Branch on the machine-readable type / code — never on the human-readable message, which may be a single string or an array of strings (validation) and can change without notice.
The client secret
Every session has two identifiers, and confusing them is the most common early mistake.
Session id | client_secret | |
|---|---|---|
| Shape | 67a1f3b9e4b0c10001234567 | gsec_<id>_<random> |
| Lives where | Your server, your database | The browser — safe to send to the page |
| Returned | On every retrieve | Once, only in the create response |
| Used for | GET / cancel with sk_* | Mounting the widget (bootstrap) |
| If leaked | Useless without a secret key | Can only open this one session in the widget |
What each one is for
The id is your server-side handle. You store it, look up the session with it, cancel with it, and correlate webhooks against it. It is meaningless to a browser on its own — reading or cancelling a session requires the secret key.
The client_secret (gsec_<sessionId>_<random>, where the random part is 32 characters) is the browser-safe capability that binds the widget to exactly this session. You hand it to the page, the SDK exchanges your pk_* plus the client_secret at POST /v1/embed/bootstrap for a short-lived embed token, and the iframe runs the user through the flow. It is shown once, in the create response — capture it then. It carries no secret-key power: the worst a leaked client_secret can do is let someone open the same checkout the user was already going to see.
Shown once — capture it on create
0Bit never stores the raw client_secret. After create, the backend keeps only a prefix and a SHA-256 hash for lookup, so there is no API call that can return the plaintext again. A GET on the session returns the plain GateSession with no client_secret field. If you lose it before mounting the widget, you cannot recover it — create a fresh session instead.
Don't store the client_secret long-term
The client_secret is a short-lived, one-session capability — pass it straight to the page that mounts the widget. Persist the session id (and your own order reference in metadata or user_reference); re-fetch state with GET /v1/gate_sessions/{id} rather than holding the secret.
The lifecycle
A session is created open and ends in exactly one terminal state. The browser callback is UX-only; the webhook is the source of truth.
Status enum
The status field is exactly one of four values. Three are terminal.
| Status | Meaning | How it's reached | Driving event |
|---|---|---|---|
open | Live and mountable. The user can run the flow. | Set at creation. | gate_session.created |
completed | At least one intent linked to the session succeeded. Terminal. | An on-ramp/off-ramp/swap settles. | gate_session.completed (adds tx_refid) |
cancelled | You cancelled it via the API. Terminal. | POST /v1/gate_sessions/{id}/cancel. | gate_session.cancelled |
expired | Past expires_at without completing. Terminal. | Lazily, on the first read after expires_at. | gate_session.expired |
There is no separate failed status
The session status enum is open / completed / expired / cancelled only — there is no failed status on the object. The backend does emit a gate_session.failed webhook for failed attempts (declined payment, KYC, settlement error), but that event is not in the published OpenAPI status set, so don't key state-machine logic on a failed status — handle the .failed event and otherwise treat a non-completing session as cancelled/expired. See the API reference for the full event catalogue.
Terminal states are final. A session never moves out of completed, cancelled, or expired — to try again, create a new session.
The browser callback is not settlement
The widget's onSuccess means "the user finished the flow," not "money has moved." Treat it as a cue to show a friendly processing state. Settlement is confirmed only by the gate_session.completed webhook, verified against your signing secret. Make the handler idempotent.
Webhooks drive the state
State transitions are reported by signed POSTs to your webhook_url. The events documented in the spec are gate_session.created, .completed, .cancelled, .expired, and gate_session.kyc_package_accepted; the backend additionally emits gate_session.failed, kyc.required, and the operational partner.quota.warning / partner.quota.exhausted.
Event envelope
Every event is { id, type, created_at, data }:
id— a UUID, stable across retries. Use it as your dedupe key.type— the event name (e.g.gate_session.completed).created_at— Unix epoch seconds at emission (an integer, not an ISO string).data— the fullGateSessionforcreated/cancelled/expired. Forcompletedthe session is augmented withtx_refid(the refid of the underlying transaction — join it to your order record or deep-link to the receipt). Forkyc_package_acceptedthe payload is the redactedKycPackageAcceptance(session_id,partner_id,mode,provider,accepted_at,user_reference) — never raw KYC PII.
Verifying signatures
Verify every delivery before trusting it:
- Read the
Gate-Signatureheader (valuet=<unix>,v1=<hex>). It is HMAC-SHA256 over<timestamp>.<rawBody>keyed with yourwhsec_*secret, with a 300-second tolerance. (Never look for anx-0bit-signatureheader — it does not exist; the backend constant is locked toGate-Signature.) - Companion headers ride alongside it:
X-0bit-Timestamp(same value as thet=),X-0bit-Event-Id(the dedupe UUID), andX-0bit-Event-Type. The User-Agent is0bit-webhooks/1.0. - The SDKs verify for you:
client.webhooks.constructEvent(rawBody, header, secret)in Node,client.webhooks.construct_event(payload, sig_header, secret)in Python — both do a constant-time compare and throw/raise on mismatch.
Delivery & retries
Deliveries are enqueued per event and POSTed by a worker. A failed delivery retries on the schedule 1m → 5m → 30m → 2h, dead-lettering after 5 attempts. Because retries reuse the same event id, your handler must be idempotent — ack any 2xx (the body is ignored), dedupe on id, and reconcile via GET if you suspect a missed delivery.
See Authentication for the full signing and verification reference, and Webhooks for the event catalogue.
Retrieve and cancel
Two server-side operations let you inspect and end a session. Both take the session id and a secret key.
GET /v1/gate_sessions/{id} returns the current session (status, amount, currency, expiry) — but never the client_secret, which exists only in the create response. Rate limit 60 / minute.
curl https://gate-api.0bit.app/v1/gate_sessions/67a1f3b9e4b0c10001234567 \
-H "Authorization: Bearer $GATE_KEY"Retrieve is for reconciliation and support — confirming a webhook, recovering from a dropped delivery, or answering "what happened to this checkout?". It is not a substitute for webhooks: poll it to reconcile, not as your primary settlement signal. A read after expires_at is also what lazily transitions a stale open session to expired.
Errors: 401 (bad credential), 404 (no such session, or your key doesn't scope to it), 429 (over 60/min).
POST /v1/gate_sessions/{id}/cancel ends an open session you no longer want — e.g. the user backed out, or your order was voided upstream. Rate limit 30 / minute.
curl -X POST https://gate-api.0bit.app/v1/gate_sessions/67a1f3b9e4b0c10001234567/cancel \
-H "Authorization: Bearer $GATE_KEY"It returns the session, now cancelled, and fires the gate_session.cancelled webhook.
Errors: 401 (bad credential), 404 (no such session for this key), 409 (already terminal), 429 (over 30/min).
Cancel only applies to an open session. Cancelling one that is already terminal returns 409 — that is the correct "already in a final state" signal, not an error to retry.
One session id is stable across modes and retries
A session id is unique to its environment. Sandbox and live never collide, and an Idempotency-Key on create means a retried create returns the same session rather than a duplicate. Pin the id (and your order reference in metadata / user_reference) and treat it as the durable handle for the whole checkout.
See also
Quickstart
The full end-to-end flow — create a session and mount the widget in about ten minutes.
Authentication
Publishable vs secret keys, the embed handshake, and verifying the Gate-Signature header.
Choose an integration
Full widget, kit blocks, hosted redirect, or server-only — and how each uses a session.
API reference
The gate_sessions endpoints, the GateSession object, and the webhook event catalogue.