Buy, sell & swap
The three flows 0Gate runs — buy (fiat to crypto, on_ramp), sell (crypto to fiat, off_ramp), and swap (crypto to crypto) — what each is for, the high-level path every one follows, and how a session's flow parameter selects between them.
0Gate moves value across exactly one boundary in three directions. Buy takes fiat and delivers crypto to a wallet. Sell takes crypto and pays fiat to a bank or payout method. Swap takes one crypto and returns another. They are three faces of the same machine: the same session object, the same embed handshake, the same KYC and screening, the same signed webhook closing the loop. What changes between them is only the direction — what the user provides, and where the value lands.
This page is the mental model for those three flows. You do not build any of them screen-by-screen; you create a session, hand the widget a client_secret, and 0Bit runs the conversion and settles to the destination. The how-to — installing an SDK, mounting the widget, picking an integration shape — lives in 0Gate quickstart and Choose your integration. Here we explain what each flow is and the path it follows.
One session, one direction
A flow is not a different product or a different API. It is a property of the session — the flow field, set to on_ramp, off_ramp, or swap. Everything else about creating, mounting, and settling a session is identical across the three.
The three flows at a glance
Every flow is fiat ↔ crypto (or crypto ↔ crypto) executed by 0Bit between two endpoints. The user supplies one side; 0Bit converts and settles the other side to the destination.
| Flow | flow value | Direction | What the user provides | Where value lands |
|---|---|---|---|---|
| Buy | on_ramp | Fiat → crypto | Fiat payment (card / bank rail) | Destination wallet (crypto) |
| Sell | off_ramp | Crypto → fiat | Crypto sent to 0Bit | Destination bank / payout method (fiat) |
| Swap | swap | Crypto → crypto | Crypto sent to 0Bit | Destination wallet (the target crypto) |
In all three, 0Bit is the regulated operator in the middle: it runs KYC and AML/sanctions screening, takes the inbound leg, performs the conversion, and settles the outbound leg to the destination — non-custodially for you, the partner. You never touch the money leg. See Custody & compliance for exactly where that line sits.
The fields that shape any flow
Whatever the direction, a flow's behaviour is set by the same handful of fields on the session you create with POST /v1/gate_sessions. These are the levers — each is described in full in Sessions and the API reference, but the ones that change what a flow does are:
| Field | Type | Required | What it does to the flow |
|---|---|---|---|
amount | string | yes | Decimal string, pattern ^[0-9]+(\.[0-9]{1,8})?$, max 8 fractional digits, > 0. Server-bound — the buy/sell/swap leg must match exactly or it is rejected. |
currency | string | yes | ISO 4217 three-letter code (pattern ^[A-Za-z]{3}$). The fiat side for buy/sell; the quote currency for swap. |
return_url | string (uri) | yes | Where the iframe sends the user after a successful flow. HTTPS only (loopback in dev); origin must be in partner.allowed_domains. |
flow | enum | no | on_ramp | off_ramp | swap. Locks the direction. Omit for the open widget where the user picks via tabs (see Selecting a flow). |
target_token | string | no | Constrains the crypto side (e.g. USDC); pattern ^[A-Za-z0-9]{2,12}$. |
target_network | string | no | Constrains the chain (e.g. ETHEREUM, POLYGON); pattern ^[A-Za-z0-9_-]{2,30}$. |
wallet_address | string | no | Pre-filled destination wallet (max 128 chars). Validated against the resolved network at flow time, not at session-create. |
cancel_url | string (uri) | no | Where to send the user on cancel. |
user_reference | string | no | Opaque partner-side id (max 128 chars). Echoed in webhook payloads for correlation. |
metadata | object | no | Opaque per-session notes, returned verbatim; not used for business logic. |
target_token / target_network are partner constraints, not requirements: set them to pin the user to one asset/chain, or leave them off to let the user choose inside the widget from 0Bit's live capability set.
Buy — fiat to crypto (on_ramp)
Buy is an on-ramp: the user pays fiat and crypto is delivered to a wallet. It is the flow you reach for when your product needs to get someone into crypto — funding a wallet, topping up an in-app balance, acquiring a token to use elsewhere. The user brings money (a card or bank payment in their local currency); 0Bit converts it and settles the purchased asset to the destination wallet.
The high-level path:
- The user picks an amount (or you bind one in at session create).
- 0Bit runs KYC inside the widget if the user is not already verified for this tier and amount.
- The user pays via a supported fiat rail — the payment screens are 0Bit's, served inside the iframe.
- 0Bit converts the fiat to the target crypto and settles it on-chain to the destination wallet.
- A signed
gate_session.completedwebhook tells your server the buy is done.
Buy is one of 0Gate's two core, production flows. (Its coverage, like every flow's, is operator-governed — see Coverage & maturity.)
Direction & where value lands
| Aspect | Buy |
|---|---|
flow value | on_ramp |
| Inbound leg (user provides) | Fiat payment — currency (e.g. EUR), amount |
| Outbound leg (value lands) | Crypto, on-chain, to the destination wallet |
| Crypto side controlled by | target_token / target_network (or user choice in the widget); wallet_address for the destination |
| Settlement signal | gate_session.completed webhook, with data.tx_refid |
What you bind, what the user supplies
The fiat side (amount, currency) is server-bound when you create the session — the user cannot tamper with the locked value. The crypto side is either pinned by you (target_token/target_network/wallet_address) or left to the user to choose in the widget. The payment method (card vs bank rail) and the KYC step are 0Bit's screens, rendered inside the iframe; you do not collect card data or run identity checks yourself. See Custody & compliance.
Sell — crypto to fiat (off_ramp)
Sell is an off-ramp: the user sends crypto and fiat is paid out to a bank or payout method. This is the flow for getting value out of crypto — cashing out a balance, paying a user in their local currency, settling earnings to a bank account. The user brings crypto; 0Bit converts it and pays the proceeds to the destination fiat payout method.
The high-level path:
- The user picks an amount to sell (or you bind it in at create).
- 0Bit runs KYC inside the widget where required.
- The user sends the crypto to 0Bit (the widget shows the address / on-chain instructions).
- 0Bit confirms receipt, converts to the chosen fiat currency, and settles to the destination bank / payout method.
- A signed
gate_session.completedwebhook confirms the payout leg.
Sell is the other core production flow.
Direction & where value lands
| Aspect | Sell |
|---|---|
flow value | off_ramp |
| Inbound leg (user provides) | Crypto sent on-chain to a 0Bit-shown address |
| Outbound leg (value lands) | Fiat, to the destination bank / payout method |
| Fiat side controlled by | currency (the payout currency) + amount |
| Settlement signal | gate_session.completed webhook (with data.tx_refid); PAYMENT_SUCCESS may carry amount / currency |
Why "success" is server-confirmed, not browser-confirmed
The inbound leg is an on-chain deposit and the outbound leg is a real bank payout — neither completes the instant the user finishes the widget. The iframe's PAYMENT_SUCCESS postMessage (and the SDK's onSuccess callback) carries { txId, sessionId?, amount?, currency? } and means the user finished their part, not the payout settled. Treat the gate_session.completed webhook as the source of truth for sell — it fires only when the linked intent succeeds.
Buy and sell are the core
On-ramp and off-ramp are the mature, day-one flows of 0Gate. If your integration is about getting users into or out of crypto, these are the two you build on.
Swap — crypto to crypto (swap)
Swap is crypto-to-crypto: the user sends one asset and receives another at the destination wallet. No fiat rail is involved on either side — the user brings crypto, 0Bit converts it across, and settles the target asset to a wallet. It is useful when a user already holds crypto and needs a different asset without leaving your surface.
The high-level path mirrors a sell on the inbound side and a buy on the outbound side:
- The user picks the source asset, the target asset, and an amount.
- 0Bit runs KYC inside the widget where required.
- The user sends the source crypto to 0Bit.
- 0Bit converts to the target asset and settles it on-chain to the destination wallet.
- A signed
gate_session.completedwebhook confirms the swap.
Direction & where value lands
| Aspect | Swap |
|---|---|
flow value | swap |
| Inbound leg (user provides) | Source crypto sent on-chain to a 0Bit-shown address |
| Outbound leg (value lands) | Target crypto, on-chain, to the destination wallet |
| Target asset controlled by | target_token / target_network (or user choice); wallet_address for the destination |
| Settlement signal | gate_session.completed webhook, with data.tx_refid |
Swap is part of the widget — don't over-promise it
Swap ships as a flow in the 0Gate widget alongside buy and sell, but it is less mature than the two fiat ramps. Treat its asset coverage, network support, and limits as live, operator-governed values surfaced by the widget — not a fixed catalogue to advertise to your users. If your integration depends on swap availability for a specific pair, confirm it against the live widget rather than documenting guarantees.
Selecting a flow
There are three independent places the direction can be expressed: the session's flow field (the authority), the widget's tab strip, and the browser SDK's kit-block factories. They must agree — and where they disagree, the session wins.
The flow field on the session
The flow is chosen by the session's flow field, set when you create the session on your server with your sk_ secret key. It takes one of three values — on_ramp, off_ramp, or swap — and it locks the widget to that single direction.
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",
"flow": "on_ramp",
"return_url": "https://app.example.com/return"
}'If you omit flow, the session is not locked to a direction and the widget renders with tabs — the user chooses buy, sell, or swap themselves. (On the session object the unset value is null; the API treats null and omitted identically.) Setting flow is how you ship a single-purpose surface (a "Buy crypto" button that only ever buys) instead of the full three-tab ramp.
flow value | What the widget shows |
|---|---|
(omitted / null) | The full widget with buy / sell / swap tabs — the user picks. |
on_ramp | Buy only — fiat to crypto, no tab strip. |
off_ramp | Sell only — crypto to fiat, no tab strip. |
swap | Swap only — crypto to crypto, no tab strip. |
How the lock reaches the iframe
The flow you set on the session is carried all the way to the widget, so the iframe can hide the tab strip before the user sees it:
POST /v1/gate_sessionsstoresflowon theGateSession.- The browser bootstraps with
POST /v1/embed/bootstrap(publishable key +clientSecret); theEmbedBootstrapResponseechoes backflowalongsideamount,currency,target_token,target_network, andreturn_url. - The SDK forwards
flowto the iframe in theINIT_CONFIGpostMessage. When it is set, the iframe-app hides the buy/sell/swap tab strip and forces the chosen view.
Because the value originates from a session created with your sk_ key, the direction is bound server-side — the browser cannot switch a "buy" session into a "sell".
Kit-block factories (browser SDK)
The browser SDK @0bit/gate/browser exposes the same choice ergonomically. You can mount the full widget and let the user pick a tab, or use a flow-locked kit block:
import { createGateOnRamp } from '@0bit/gate/browser';
const ramp = createGateOnRamp({
publishableKey: 'pk_test_...',
clientSecret: session.client_secret,
});
await ramp.mount('#buy-crypto');Each factory is a thin wrapper that returns a GateRamp with flow pre-set:
| Factory | Sets flow | React component |
|---|---|---|
createGateOnRamp(config) | on_ramp | <GateOnRamp /> |
createGateOffRamp(config) | off_ramp | <GateOffRamp /> |
createGateSwap(config) | swap | <GateSwap /> |
A kit block is the same hosted screens as the full widget with the direction pinned and the tab UI hidden. The React package (@0bit/gate/react) ships the matching <GateOnRamp>, <GateOffRamp>, and <GateSwap> wrappers around <RampCheckout>. See Choose your integration for when to lock a flow versus show tabs.
The session is the authority — the factory is a fallback
The factory's flow is, per the SDK source, "a local assertion / fallback for partners not yet using sessions." The canonical place to set the direction is the session created on your server. Keep the kit-block factory and the session's flow in agreement; if they disagree, the session wins. Because flow is bound at session-create with your sk_ key, the browser can't override it.
Coverage & maturity
Be honest with your own users about what each flow guarantees today. The three flows are not equally mature: buy and sell are the mature, day-one ramps, while swap is the newer widget flow. Coverage inside each — fiat currencies, payment rails, networks, and assets — is operator-governed and entitlement-gated, surfaced by the live widget at runtime. Read it from that live capability set rather than hard-coding a coverage matrix.
Capability is signalled, not assumed
The platform tells you when a flow can't run rather than failing silently. The EmbedBootstrapResponse carries available and unavailable_reason; when available is false, the SDK forwards it through INIT_CONFIG and the iframe renders a neutral "service unavailable" overlay instead of the checkout UI. Stable reason strings include quota_exhausted and partner_suspended. On the server side, partner.quota.warning and partner.quota.exhausted webhooks warn you before and when entitlements run out. Treat these signals — not a documented coverage table — as the truth about what is available.
0Gate is the partner-ready product
Buy, sell, and swap are 0Gate (fiat↔crypto). 0Pools is a gated, approved-partner liquidity API (early access, provisioned per partner by 0Bit) — not the default public path; most integrations should use 0Gate.
One path, three directions
Strip away the direction and all three flows are the same shape: the user picks an amount, KYC runs in the widget if needed, value moves in on one side, 0Bit converts, and value settles out to the destination — confirmed by a signed webhook.
The settlement signal is identical
In every case the loop closes the same way: a gate_session.completed webhook, signed with the Gate-Signature header (t=<unix>,v1=<hex>, HMAC-SHA256 over <timestamp>.<rawBody>, 300-second tolerance), verified against your whsec_ secret. The completed event's payload adds data.tx_refid — the refid of the underlying crypto transaction — so you can join the settlement back to your order record. Companion headers on every webhook delivery are X-0bit-Timestamp, X-0bit-Event-Id (your dedupe key — retries reuse it), X-0bit-Event-Type, and User-Agent: 0bit-webhooks/1.0. (Never look for an x-0bit-signature header; it does not exist on the wire.)
The browser onSuccess callback / PAYMENT_SUCCESS postMessage means "the user finished," not "value settled" — wait for the webhook.
Events that fire across the flows
The same event vocabulary covers all three directions. Beyond gate_session.completed, your endpoint may receive:
| Event | When |
|---|---|
gate_session.created | POST /v1/gate_sessions succeeds. |
gate_session.completed | A buy/sell/swap linked to the session succeeds (adds data.tx_refid). |
gate_session.expired | Fired lazily on the first read past expires_at (default 24h). |
gate_session.cancelled | POST /v1/gate_sessions/:id/cancel succeeds. |
gate_session.failed | A linked intent failed. (Emitted by the backend; not in the OpenAPI spec.) |
gate_session.kyc_package_accepted | A kyc_trusted partner's pre-verified KYC was accepted (redacted, no PII). |
kyc.required | The user must complete KYC before the flow can proceed. |
partner.quota.warning / partner.quota.exhausted | Entitlement/quota thresholds — relevant to coverage (see above). |
Build your handler to ignore unknown types so undocumented events don't break it.
Where KYC fits
KYC is not a fourth flow — it runs inside whichever flow needs it. 0Bit's verification partner verifies the user in the widget, and your server may see a kyc.required event. On contract-gated tiers, a trusted-KYC pass-through (kyc_package, accepted only when partner.kyc_trusted is true) sets kyc_pre_verified: true on the session and fires a redacted gate_session.kyc_package_accepted event, letting an already-verified user skip re-verification. The raw kyc_package is never echoed in any API response or webhook. Identity is always owned by 0Bit — see Custody & compliance.
See also
Sessions
The GateSession object that carries the flow — created server-side, mounted with a client_secret, settled by webhook.
Choose your integration
Full widget with tabs, flow-locked kit blocks, hosted redirect, or server API only — and how each sets the flow.
Quickstart
Create a session and mount a buy, sell, or swap in about ten minutes.
Custody & compliance
Why every flow is non-custodial for you, and who owns KYC, rails, and settlement.