Choose your integration
Four ways to embed 0Gate — full widget, kit blocks, hosted redirect, or the server API on its own. Use the decision tree to pick the right path, then copy a teaser to start.
There is no single "right" way to embed 0Gate. The same session — created server-side with POST /v1/gate_sessions and rendered in the browser — can power a full buy/sell/swap surface, a single locked flow, a hosted redirect, or a UI you build yourself. What differs is how much of the frontend you hand to 0Bit versus how much you own.
This page is a decision page. Start with the tree, land on a path, copy the teaser, then follow the linked guide.
Three things are true of every path
- You always create the
GateSessionon your server with yoursk_secret key — never in the browser. - The browser only ever sees the browser-safe
client_secret(gsec_...) and yourpk_publishable key. - Webhooks are the source of truth for settlement — the browser
onSuccesscallback is UX only.
These hold whether you use the widget, kit blocks, a redirect, or raw API.
SDKs are preview packages
The audited SDK package is @0bit/gate for npm and 0bit-gate for Python. Raw REST examples use the /v1 OpenAPI base URL; SDK constructors take the API origin, such as https://gate-api-sandbox.0bit.app, and append /v1/... internally.
Decision tree
Answer three questions, in order: do you control the frontend, do you need all three flows (buy + sell + swap) in one surface, and can you render an iframe in your environment?
| If you… | Use | Why |
|---|---|---|
| Want the complete ramp (buy/sell/swap tabs) with the least code | Full widget | One mount, 0Bit owns every screen including KYC. |
| Want exactly one flow with the tab strip hidden | Kit blocks | A flow-locked iframe — same hosted screens, no tab UI. |
| Can't (or won't) render an iframe — WebView, hostile CSP, no-code page, email | Hosted redirect | Full-page checkout on 0Bit's domain; come back via return_url. |
| Need full control of the UI and accept owning status, errors, and KYC routing | Server API only | You build the front end against /v1/gate_sessions; advanced. |
React partners
If your frontend is React, use @0bit/gate/react instead of the imperative @0bit/gate/browser calls. It wraps the same iframe and protocol as declarative components — <RampCheckout /> (full widget) and <GateOnRamp /> / <GateOffRamp /> / <GateSwap /> (kit blocks). The decision tree is identical; only the syntax differs.
What every path shares
Whichever leaf you land on, three SDK invariants hold — and they explain why even the "Server API only" path still embeds or redirects for the user-facing leg:
- The browser SDK refuses secret keys.
new GateRamp(...)andGateRamp.redirectToCheckout(...)both throwSecretKeyInBrowserErrorthe moment a key matches/^sk_/, and reject anything that isn'tpk_test_/pk_live_with aConfigError. There is no way to drive settlement from the browser. - The pre-flight is
POST /v1/embed/bootstrap. Bothmount()and the React components call it first withAuthorization: Bearer <pk_>(plusclientSecretin the body when present), so a bad key or non-allowlisted origin fails before a blank iframe renders. It returns a 1-hour embed JWT the iframe then sends asX-Embed-Tokenon its own downstream calls. - Success is
{ txId, sessionId? }, not settlement. Every embedded path firesPAYMENT_SUCCESSwith{ txId, sessionId?, amount?, currency? }. That is a UX signal. Grant value only on the signedgate_session.completedwebhook.
1. Full widget
The complete on/off-ramp. One call to GateRamp.mount() renders an iframe with the buy / sell / swap tab strip; 0Bit hosts every screen — quote, KYC, payment method, on-chain settlement.
When to use it
- You want the fastest, most complete integration and are happy to let users choose the flow.
- You control your own page markup and can render an iframe (most web apps).
- You want buy, sell, and swap available from a single surface.
How it works. Your server creates a session (omit flow to keep all tabs). Your page constructs GateRamp with the publishableKey + clientSecret and calls .mount(target, handlers). The SDK pre-flights POST /v1/embed/bootstrap with the pk_, loads the iframe at gate.0bit.app, and resolves the mount promise on WIDGET_READY.
npm install @0bit/gate/browserimport { GateRamp } from '@0bit/gate/browser';
const ramp = new GateRamp({
publishableKey: 'pk_test_AbCdEfGh1234567890abcdefghijklmn',
clientSecret: 'gsec_..._...', // from POST /v1/gate_sessions on your server
environment: 'sandbox', // -> gate-sandbox.0bit.app
});
await ramp.mount('#ramp', {
onReady: () => console.log('widget ready'),
onSuccess: ({ txId, sessionId }) => { /* UX only — wait for the webhook before granting value */ },
onError: ({ code, message }) => console.warn(code, message),
onClose: ({ reason }) => console.log('closed:', reason),
});npm install @0bit/gate/reactimport { RampCheckout } from '@0bit/gate/react';
export function Checkout({ clientSecret }: { clientSecret: string }) {
return (
<RampCheckout
publishableKey={import.meta.env.VITE_GATE_PUBLISHABLE_KEY}
clientSecret={clientSecret}
environment="sandbox"
onSuccess={({ txId }) => {/* UX only — webhook is authoritative */}}
onError={(e) => console.warn(e.code, e.message)}
/>
);
}<div id="ramp"></div>
<script src="https://cdn.jsdelivr.net/npm/@0bit/gate/browser"></script>
<script>
// The IIFE bundle exposes the global GateJS.
const ramp = new GateJS.GateRamp({
publishableKey: 'pk_test_...',
clientSecret: 'gsec_..._...',
environment: 'sandbox',
});
ramp.mount('#ramp', { onSuccess: () => {/* UX only */} });
</script>Theme it with ramp.setTheme('dark'), or pass theme in the constructor. → 0Gate quickstart walks the full widget end to end.
GateRamp constructor options
Every field on the constructor config, from the SDK source. Only publishableKey is required.
| Option | Type | Default | Notes |
|---|---|---|---|
publishableKey | string | — | Required. Must match pk_test_ / pk_live_. An sk_ key throws SecretKeyInBrowserError. |
clientSecret | string | — | From POST /v1/gate_sessions. Optional, but binds the iframe to a server-minted session — pass it. |
environment | 'production' | 'sandbox' | 'development' | 'production' | Picks the API + iframe hosts. |
apiBaseUrl | string | env default | Override the bootstrap host. Trailing slashes are stripped. |
iframeUrl | string | env default | Override the iframe origin. Self-hosted / local only. |
theme | 'light' | 'dark' | none | Initial theme; flip at runtime with setTheme(). |
targetToken / targetNetwork | string | from session | Partner constraint forwarded to the iframe. |
returnUrl | string | from session | Where the iframe redirects on success when session-bound. |
flow | 'on_ramp' | 'off_ramp' | 'swap' | none | Kit-block lock; hides the tab strip. Prefer setting flow on the session server-side. |
height | number | 600 | Iframe height in px. Must be a positive number or ConfigError. |
fetchImpl | typeof fetch | global fetch | Injectable for tests. |
The rendered iframe is fixed at max-width: 440px, centered, allow="payment *", with data-0gate="ramp".
Mount lifecycle and handlers
mount(target, options) accepts a selector string or an HTMLElement (a missing/invalid target throws MountTargetError), and returns a Promise<void> that resolves on WIDGET_READY or rejects after a 15-second timeout if the iframe never signals ready. Calling mount() twice without unmount() throws StateError.
| Handler | Payload | When |
|---|---|---|
onReady | { capabilities?: string[] } | Iframe mounted and received its INIT_CONFIG. |
onSuccess | { txId, sessionId?, amount?, currency? } | Terminal happy path. UX only. |
onError | { code, message, intentId? } | Terminal sad path against a known intent. |
onClose | { reason: 'user_close' | 'session_expired' | 'unknown' } | User dismissed without completing. |
onUnavailable | { reason, message } | Partner over quota / suspended — see below. The mount promise still resolves (does not reject); the iframe renders a neutral overlay. |
Other instance methods: setTheme('light' \| 'dark'), updateConfig({ amount?, currency?, targetToken?, targetNetwork? }), and unmount() (safe to call when not mounted; tears down the iframe + the message listener). Calls made before mount() or after unmount() are dropped silently rather than thrown.
onUnavailable is a soft signal, not an error
When the bootstrap returns available: false (e.g. unavailable_reason: 'quota_exhausted'), the SDK still mounts the iframe and forwards the unavailable state so the iframe-app greys itself out. onUnavailable fires so you can layer your own UX, but onError does not fire and the mount promise resolves normally.
postMessage protocol
The host SDK and the iframe speak a typed postMessage contract with strict origin + source checks (messages from any origin other than the loaded iframe are ignored). You rarely touch this directly, but it explains the lifecycle:
- host → iframe:
INIT_CONFIG(first config push after ready),SET_THEME,UPDATE_CONFIG. - iframe → host:
WIDGET_READY,PAYMENT_SUCCESS,PAYMENT_ERROR,PAYMENT_CLOSE.
MessageType and the isIframeMessage() type-guard are exported from @0bit/gate/browser if you run your own listener.
2. Kit blocks
The same hosted iframe, locked to one flow with the tab strip hidden. Reach for a kit block when the surrounding page already implies the action — a "Top up" button (on-ramp), a "Cash out" screen (off-ramp), or a "Convert" panel (swap).
When to use it
- You want a single flow, not a tab picker — buy or sell or swap.
- You still want 0Bit to host KYC, payments, and settlement (same as the full widget).
- You want the surface to read as part of your own UI, not a generic ramp.
How it works. Identical to the full widget — server-created session, embed bootstrap, iframe — except the flow is pinned. Pin it either by passing flow when you create the session (on_ramp / off_ramp / swap) or by using the matching kit-block factory in the browser. The factories are thin wrappers over GateRamp that set flow for you.
import { createGateOnRamp } from '@0bit/gate/browser';
// also: createGateOffRamp(cfg), createGateSwap(cfg)
const onramp = createGateOnRamp({
publishableKey: 'pk_test_...',
clientSecret: 'gsec_..._...',
environment: 'sandbox',
});
await onramp.mount('#topup', {
onSuccess: ({ txId }) => { /* UX only — confirm via webhook */ },
});import { GateOnRamp } from '@0bit/gate/react';
// also: <GateOffRamp />, <GateSwap />
export function TopUp({ clientSecret }: { clientSecret: string }) {
return (
<GateOnRamp
publishableKey={import.meta.env.VITE_GATE_PUBLISHABLE_KEY}
clientSecret={clientSecret}
environment="sandbox"
onSuccess={() => {/* UX only */}}
/>
);
}The factories are GateRamp + a pinned flow
Each factory returns a normal GateRamp instance — same constructor options, same mount() lifecycle and handlers as path 1 — with flow already set. The factory config type is GateRampConfig with flow omitted, so you cannot pass a conflicting flow:
| Factory | Sets flow | Surface |
|---|---|---|
createGateOnRamp(cfg) | 'on_ramp' | Buy crypto with fiat. |
createGateOffRamp(cfg) | 'off_ramp' | Sell crypto for fiat. |
createGateSwap(cfg) | 'swap' | Convert one asset to another. |
Because the return value is a GateRamp, setTheme(), updateConfig(), and unmount() all work identically. The React equivalents (<GateOnRamp />, <GateOffRamp />, <GateSwap />) are <RampCheckout /> with the same flow baked in.
Pin the flow server-side too
Setting flow when you create the session — not just in the browser block — is the durable guarantee. In the SDK, local config wins (config.flow ?? bootstrap.flow), so a factory flow overrides whatever the session carries. The session value is the canonical source of truth; the factory is a local assertion / a fallback for partners not yet using sessions, and it keeps a hosted redirect (path 3) locked to the same single flow.
3. Hosted redirect
No iframe at all. GateRamp.redirectToCheckout() sends the user to a full-page 0Bit-hosted checkout and brings them back to the session's return_url. This is the escape hatch for any environment where embedding fails.
When to use it
- The page can't render an iframe — a mobile WebView, a strict / hostile
Content-Security-Policy, an<iframe>-stripping CMS, or an email/link surface. - You have no real frontend to mount into (no-code builders, marketing pages).
- You'd rather not manage iframe lifecycle and prefer a clean full-page handoff.
How it works. Your server still creates the session — clientSecret is required here, hosted mode is always session-bound and anonymous access is disabled. The redirect drops the user on the hosted checkout; on completion they return to return_url. There is no onSuccess callback in this mode, so the webhook (and the return_url arrival) is your only completion signal.
import { GateRamp } from '@0bit/gate/browser';
// Full-page navigation — no element to mount into.
GateRamp.redirectToCheckout({
publishableKey: 'pk_test_...',
clientSecret: 'gsec_..._...', // REQUIRED — hosted mode is session-bound
environment: 'sandbox',
theme: 'dark',
});<!-- Trigger the redirect from a button when you ship the SDK on the page. -->
<button id="buy">Buy crypto</button>
<script src="https://cdn.jsdelivr.net/npm/@0bit/gate/browser"></script>
<script>
document.getElementById('buy').addEventListener('click', () => {
GateJS.GateRamp.redirectToCheckout({
publishableKey: 'pk_test_...',
clientSecret: 'gsec_..._...',
environment: 'sandbox',
});
});
</script>redirectToCheckout signature
It is a static method — no instance, no mount(), no DOM target — and it returns void (the page unloads as window.location.assign(...) fires). It validates input and throws before navigating:
| Input | Required | Validation |
|---|---|---|
publishableKey | yes | sk_ → SecretKeyInBrowserError; not pk_test_/pk_live_ → ConfigError. |
clientSecret | yes | Missing → ConfigError("clientSecret is required for hosted-redirect mode"). |
environment | no | Resolves the iframe host. |
iframeUrl | no | Override the hosted-checkout origin. |
theme | no | 'light' | 'dark', appended as a query param. |
It builds the URL by appending client_secret, publishableKey, and (optional) theme query params onto the resolved iframe origin, then navigates. Unlike mount(), it does not call /v1/embed/bootstrap from the host — the hosted page performs the bootstrap itself.
Settlement only arrives by webhook in redirect mode
There is no browser callback to hook. Treat the return_url arrival as "the user came back," not "the payment cleared." Grant value strictly on the signed gate_session.completed webhook.
4. Server API only
The advanced path: skip the SDK widget entirely, call the REST API from your server, and build your own UI against the session lifecycle. You take on more — quote display, status polling, error states, and routing the user into KYC when an event demands it — in exchange for total control of the experience.
When to use it
- You need a fully custom, branded flow that an iframe can't deliver.
- You're integrating into a non-web surface, a backend job, or your own native client.
- You accept owning the status / error / KYC UX that the widget would otherwise handle.
You still embed or redirect for the user-facing leg
"Server API only" means you own the server calls and your own UI chrome — but the user still completes KYC, payment, and on-chain settlement on a 0Bit surface. You create the session here, then hand the resulting client_secret to one of paths 1–3 to render the actual money-movement step. There is no headless way to charge a card or release crypto from your own front end.
How it works. Create a session, retrieve it to track status, drive your own UI off the lifecycle, and cancel if the user backs out. Use a server SDK or raw HTTP — both hit the same routes. Auth is Authorization: Bearer <sk_...>; the SDKs add the Idempotency-Key on writes automatically (send your own uuid v4 if you call the API directly).
# Create
curl -X POST https://gate-api-sandbox.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://yourapp.example/checkout/done"
}'
# Retrieve status
curl https://gate-api-sandbox.0bit.app/v1/gate_sessions/{id} \
-H "Authorization: Bearer $GATE_KEY"
# Cancel
curl -X POST https://gate-api-sandbox.0bit.app/v1/gate_sessions/{id}/cancel \
-H "Authorization: Bearer $GATE_KEY"npm install @0bit/gateimport { GateClient } from '@0bit/gate';
const client = new GateClient({
apiKey: process.env.GATE_KEY!, // sk_... — server only
baseUrl: 'https://gate-api-sandbox.0bit.app', // SDK origin; resource paths append /v1
});
const session = await client.sessions.create({
amount: '100.00',
currency: 'EUR',
return_url: 'https://yourapp.example/checkout/done',
});
const current = await client.sessions.retrieve(session.id);
// await client.sessions.cancel(session.id);
// Build your own UI off session.status; confirm settlement via webhook.pip install 0bit-gateimport os
from zerobit.gate import GateClient # PyPI dist is "0bit-gate"
client = GateClient(
api_key=os.environ["GATE_KEY"], # sk_... — server only
base_url="https://gate-api-sandbox.0bit.app", # SDK origin; resource paths append /v1
)
session = client.sessions.create({
"amount": "100.00",
"currency": "EUR",
"return_url": "https://yourapp.example/checkout/done",
})
current = client.sessions.retrieve(session["id"])
# client.sessions.cancel(session["id"])Endpoints and rate limits
The whole user-facing leg runs on four routes. The widget paths call them for you; here you call them yourself.
| Method + path | Key | Rate limit | Purpose |
|---|---|---|---|
POST /v1/gate_sessions | sk_ | 10/min | Create a session. Idempotency-Key required. Returns the session + one-time client_secret. |
GET /v1/gate_sessions/{id} | sk_ | 60/min | Poll status (open / completed / expired / cancelled). |
POST /v1/gate_sessions/{id}/cancel | sk_ | 30/min | Cancel an open session. |
POST /v1/embed/bootstrap | pk_ | 30/min | Exchange pk_ + client_secret for the 1-hour embed JWT (the widget calls this). |
Create-session parameters
client.sessions.create(params) body, from the SDK types. Required: amount, currency, return_url.
| Field | Type | Notes |
|---|---|---|
amount | string | Decimal string, up to 8 fractional digits. |
currency | string | ISO 4217 three-letter code (e.g. EUR). |
return_url | string | Where the user lands after a completed flow. |
cancel_url | string? | Optional return target on cancel. |
target_token / target_network | string? | Pin the destination asset / chain. |
flow | 'on_ramp' | 'off_ramp' | 'swap'? | Lock the session to one flow (durable kit-block lock). |
metadata | Record<string, unknown>? | Echoed back on the session and in webhooks. |
The response is a GateSession (id, object: 'session', status, expires_at, created_at, metadata, …) plus a one-time client_secret shaped gsec_<id>_<rand>, returned only on create — later reads omit it.
Session status vs intent failure
A GateSession.status is only ever open, completed, expired, or cancelled. There is no failed status — a failed payment is an intent-level event surfaced via onError / a webhook, while the session itself stays open until it completes, expires, or is cancelled. Don't poll for a "failed" status; listen for the webhook.
Error handling
Both server SDKs throw a typed hierarchy (mirrored across Node and Python) so you can instanceof or branch on .code. The HTTP status maps to a concrete subclass:
| Subclass | .code | HTTP | Meaning |
|---|---|---|---|
AuthenticationError | authentication_error | 401 | Bad / missing / expired key. |
PermissionError | permission_error | 403 | Key has no access to the resource. |
InvalidRequestError | invalid_request | 400 / 422 | Body or shape rejected. |
IdempotencyError | idempotency_error | 409 | Idempotency-Key reused with a different body. |
RateLimitError | rate_limit | 429 | Carries retryAfterSeconds from Retry-After. |
APIError | api_error | 5xx | Server-side failure. |
APIConnectionError | connection_error | — | Network failure before any response. |
TimeoutError | timeout | — | Client-side abort. |
WebhookSignatureError | invalid_request | — | Webhook payload failed signature / timestamp verification. |
Every GateError carries statusCode?, code, requestId? (the echoed Request-Id — quote it in support tickets), and raw?. The HTTP client auto-retries 408 / 425 / 429 / 500 / 502 / 503 / 504 and connection/timeout failures (2 retries, exponential backoff with jitter, honoring Retry-After) — so the errors you actually catch are the non-retryable ones.
Verify webhooks with the Gate-Signature header
Whatever UI you build, settlement is confirmed by webhook. The signature header is Gate-Signature, value t=<unix>,v1=<hex> — HMAC-SHA256 over <timestamp>.<rawBody>, 300s tolerance, constant-time compare. Use client.webhooks.constructEvent(rawBody, sigHeader, secret) (Node) / client.webhooks.construct_event(...) (Python). The secret is whsec_... from POST /portal/webhook-secret/rotate. Never read x-0bit-signature — that header does not exist and will never match. See Authentication.
React components
@0bit/gate/react wraps the same iframe and protocol declaratively. <RampCheckout /> (also exported as <GateCheckout />) is the full widget; <GateOnRamp />, <GateOffRamp />, <GateSwap /> are the flow-locked blocks (each is <RampCheckout /> with flow pinned). The package re-exports the whole @0bit/gate/browser error + type surface, so you don't install both.
Props
RampCheckoutProps is the GateRampConfig (minus fetchImpl) plus handlers and container styling:
| Prop group | Props |
|---|---|
| Identity (re-mount on change) | publishableKey, clientSecret, environment, apiBaseUrl, iframeUrl, theme, targetToken, targetNetwork, returnUrl, flow, height |
| Handlers | onReady, onSuccess, onError, onClose, onUnavailable, onMountError |
| Container | className, style |
onMountError(err) is React-specific — it fires when the constructor throws (e.g. an sk_ key) or the mount() promise rejects (bootstrap failure, ready timeout). The other handlers carry the same payloads as the imperative SDK.
Re-mount semantics
The component lists every identity prop in its mount effect, so changing any of them re-mounts a fresh iframe with the new config — exactly what you want for "a different checkout." Re-renders that only swap handler closures do not re-mount: handlers are read through a pinned ref. On unmount it calls ramp.unmount() and clears the instance.
import { GateOnRamp } from '@0bit/gate/react';
<GateOnRamp
publishableKey={import.meta.env.VITE_GATE_PUBLISHABLE_KEY}
clientSecret={clientSecret}
environment="sandbox"
onSuccess={({ txId }) => router.push('/done')}
onMountError={(err) => console.error('could not mount', err)}
/>Comparison at a glance
| Full widget | Kit blocks | Hosted redirect | Server API only | |
|---|---|---|---|---|
| Renders | iframe (tabs) | iframe (one flow) | full-page on 0Bit | your own UI |
| Entry point | GateRamp.mount() | createGateOnRamp/OffRamp/Swap | GateRamp.redirectToCheckout() (static) | client.sessions.* |
| Flows | buy + sell + swap | one of the three | one or all | your choice |
| Frontend control | low | low–medium | none | total |
| KYC / payment screens | 0Bit-hosted | 0Bit-hosted | 0Bit-hosted | you route into them |
clientSecret | recommended | recommended | required | you mint it |
onSuccess callback | yes (UX only) | yes (UX only) | none | n/a |
| Needs iframe support | yes | yes | no | no |
| Effort | lowest | low | low | highest |
| Settlement truth | webhook | webhook | webhook | webhook |
Next steps
Quickstart
Embed the full widget end to end in sandbox — session, mount, test purchase, signed webhook.
0Gate product guide
Sessions, flows, KYC, and the embed bootstrap handshake explained in depth.
Authentication
pk_ vs sk_ keys, embed tokens, idempotency, and verifying the Gate-Signature webhook header.
API reference
Every endpoint — POST /v1/gate_sessions, retrieve, cancel, and /v1/embed/bootstrap.