Developers
Core Concepts

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 termsamount and currency, bound at creation on the server and not changeable from the browser.
  • The flowon_ramp (buy), off_ramp (sell), swap, or unlocked (the user picks a tab).
  • The destination — optional target_token / target_network, plus return_url / cancel_url.
  • Its own identity and lifecycle — a server-only id, a browser-safe client_secret, a status, and an expires_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).

FieldTypeNotes
idstringServer-assigned Mongo ObjectId hex, e.g. 67a1f3b9e4b0c10001234567.
object"gate_session"Discriminator for client-side type narrowing.
partner_idstringThe partner that owns this session.
modetest | liveInherited from the sk_* key that created the session.
flowon_ramp | off_ramp | swap | nullKit-block flow lock. null = open widget (user picks the tab).
amountstringDecimal string, bound at create.
currencystringISO 4217 three-letter code.
target_tokenstring | nullOptional partner constraint (e.g. USDC).
target_networkstring | nullOptional partner constraint (e.g. ETHEREUM, POLYGON).
return_urlstring (uri)Where the iframe sends the user on success.
cancel_urlstring | null (uri)Optional cancel destination.
wallet_addressstring | nullPre-filled destination wallet (enterprise input).
user_referencestring | nullOpaque partner-side id, echoed in webhooks.
kyc_pre_verifiedbooleantrue only for a trusted partner that supplied a kyc_package. Default false.
statusenumopen | completed | expired | cancelled.
expires_atstring (date-time)ISO-8601, default 24h after creation.
created_atstring | null (date-time)Creation timestamp.
metadataobjectOpaque 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.

ParamTypeRequiredDescription
amountstringYesDecimal 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.
currencystringYesISO 4217 three-letter code, pattern ^[A-Za-z]{3}$ (e.g. EUR, USD).
return_urlstring (uri)YesWhere the iframe sends the user after a successful flow. HTTPS only (loopback allowed in dev); its origin must be in partner.allowed_domains.
cancel_urlstring (uri)NoWhere the user is sent if they abandon the flow.
target_tokenstringNoLock the buy/swap target token. Pattern ^[A-Za-z0-9]{2,12}$ (e.g. USDC).
target_networkstringNoLock the network. Pattern ^[A-Za-z0-9_-]{2,30}$ (e.g. ETHEREUM, POLYGON).
flowon_ramp | off_ramp | swapNoKit-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_addressstringNoEnterprise input. Pre-filled destination wallet, max 128 chars. Validated against the resolved network at flow time, not at create. Accepted from any partner.
user_referencestringNoEnterprise 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_packageobjectNoEnterprise, 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.
metadataobjectNoOpaque 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

StatusWhen
400Validation error — missing amount/currency/return_url, malformed value, or a pattern violation. message may be an array, one entry per failed field.
401Missing or malformed credential.
403Credential 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).
429Over 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 idclient_secret
Shape67a1f3b9e4b0c10001234567gsec_<id>_<random>
Lives whereYour server, your databaseThe browser — safe to send to the page
ReturnedOn every retrieveOnce, only in the create response
Used forGET / cancel with sk_*Mounting the widget (bootstrap)
If leakedUseless without a secret keyCan 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.

StatusMeaningHow it's reachedDriving event
openLive and mountable. The user can run the flow.Set at creation.gate_session.created
completedAt least one intent linked to the session succeeded. Terminal.An on-ramp/off-ramp/swap settles.gate_session.completed (adds tx_refid)
cancelledYou cancelled it via the API. Terminal.POST /v1/gate_sessions/{id}/cancel.gate_session.cancelled
expiredPast 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 full GateSession for created/cancelled/expired. For completed the session is augmented with tx_refid (the refid of the underlying transaction — join it to your order record or deep-link to the receipt). For kyc_package_accepted the payload is the redacted KycPackageAcceptance (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-Signature header (value t=<unix>,v1=<hex>). It is HMAC-SHA256 over <timestamp>.<rawBody> keyed with your whsec_* secret, with a 300-second tolerance. (Never look for an x-0bit-signature header — it does not exist; the backend constant is locked to Gate-Signature.)
  • Companion headers ride alongside it: X-0bit-Timestamp (same value as the t=), X-0bit-Event-Id (the dedupe UUID), and X-0bit-Event-Type. The User-Agent is 0bit-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

On this page