Developers

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

  1. You always create the GateSession on your server with your sk_ secret key — never in the browser.
  2. The browser only ever sees the browser-safe client_secret (gsec_...) and your pk_ publishable key.
  3. Webhooks are the source of truth for settlement — the browser onSuccess callback 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…UseWhy
Want the complete ramp (buy/sell/swap tabs) with the least codeFull widgetOne mount, 0Bit owns every screen including KYC.
Want exactly one flow with the tab strip hiddenKit blocksA flow-locked iframe — same hosted screens, no tab UI.
Can't (or won't) render an iframe — WebView, hostile CSP, no-code page, emailHosted redirectFull-page checkout on 0Bit's domain; come back via return_url.
Need full control of the UI and accept owning status, errors, and KYC routingServer API onlyYou 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(...) and GateRamp.redirectToCheckout(...) both throw SecretKeyInBrowserError the moment a key matches /^sk_/, and reject anything that isn't pk_test_ / pk_live_ with a ConfigError. There is no way to drive settlement from the browser.
  • The pre-flight is POST /v1/embed/bootstrap. Both mount() and the React components call it first with Authorization: Bearer <pk_> (plus clientSecret in 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 as X-Embed-Token on its own downstream calls.
  • Success is { txId, sessionId? }, not settlement. Every embedded path fires PAYMENT_SUCCESS with { txId, sessionId?, amount?, currency? }. That is a UX signal. Grant value only on the signed gate_session.completed webhook.

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/browser
checkout.client.ts
import { 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/react
Checkout.tsx
import { 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.

OptionTypeDefaultNotes
publishableKeystringRequired. Must match pk_test_ / pk_live_. An sk_ key throws SecretKeyInBrowserError.
clientSecretstringFrom 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.
apiBaseUrlstringenv defaultOverride the bootstrap host. Trailing slashes are stripped.
iframeUrlstringenv defaultOverride the iframe origin. Self-hosted / local only.
theme'light' | 'dark'noneInitial theme; flip at runtime with setTheme().
targetToken / targetNetworkstringfrom sessionPartner constraint forwarded to the iframe.
returnUrlstringfrom sessionWhere the iframe redirects on success when session-bound.
flow'on_ramp' | 'off_ramp' | 'swap'noneKit-block lock; hides the tab strip. Prefer setting flow on the session server-side.
heightnumber600Iframe height in px. Must be a positive number or ConfigError.
fetchImpltypeof fetchglobal fetchInjectable 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.

HandlerPayloadWhen
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.

topup.client.ts
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 */ },
});
TopUp.tsx
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:

FactorySets flowSurface
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.

redirect.client.ts
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',
});

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:

InputRequiredValidation
publishableKeyyessk_SecretKeyInBrowserError; not pk_test_/pk_live_ConfigError.
clientSecretyesMissing → ConfigError("clientSecret is required for hosted-redirect mode").
environmentnoResolves the iframe host.
iframeUrlnoOverride the hosted-checkout origin.
themeno'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/gate
server.ts
import { 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-gate
server.py
import 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 + pathKeyRate limitPurpose
POST /v1/gate_sessionssk_10/minCreate a session. Idempotency-Key required. Returns the session + one-time client_secret.
GET /v1/gate_sessions/{id}sk_60/minPoll status (open / completed / expired / cancelled).
POST /v1/gate_sessions/{id}/cancelsk_30/minCancel an open session.
POST /v1/embed/bootstrappk_30/minExchange 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.

FieldTypeNotes
amountstringDecimal string, up to 8 fractional digits.
currencystringISO 4217 three-letter code (e.g. EUR).
return_urlstringWhere the user lands after a completed flow.
cancel_urlstring?Optional return target on cancel.
target_token / target_networkstring?Pin the destination asset / chain.
flow'on_ramp' | 'off_ramp' | 'swap'?Lock the session to one flow (durable kit-block lock).
metadataRecord<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.codeHTTPMeaning
AuthenticationErrorauthentication_error401Bad / missing / expired key.
PermissionErrorpermission_error403Key has no access to the resource.
InvalidRequestErrorinvalid_request400 / 422Body or shape rejected.
IdempotencyErroridempotency_error409Idempotency-Key reused with a different body.
RateLimitErrorrate_limit429Carries retryAfterSeconds from Retry-After.
APIErrorapi_error5xxServer-side failure.
APIConnectionErrorconnection_errorNetwork failure before any response.
TimeoutErrortimeoutClient-side abort.
WebhookSignatureErrorinvalid_requestWebhook 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 groupProps
Identity (re-mount on change)publishableKey, clientSecret, environment, apiBaseUrl, iframeUrl, theme, targetToken, targetNetwork, returnUrl, flow, height
HandlersonReady, onSuccess, onError, onClose, onUnavailable, onMountError
ContainerclassName, 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.

GateBlocks.tsx — flow-locked, same protocol
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 widgetKit blocksHosted redirectServer API only
Rendersiframe (tabs)iframe (one flow)full-page on 0Bityour own UI
Entry pointGateRamp.mount()createGateOnRamp/OffRamp/SwapGateRamp.redirectToCheckout() (static)client.sessions.*
Flowsbuy + sell + swapone of the threeone or allyour choice
Frontend controllowlow–mediumnonetotal
KYC / payment screens0Bit-hosted0Bit-hosted0Bit-hostedyou route into them
clientSecretrecommendedrecommendedrequiredyou mint it
onSuccess callbackyes (UX only)yes (UX only)nonen/a
Needs iframe supportyesyesnono
Effortlowestlowlowhighest
Settlement truthwebhookwebhookwebhookwebhook

Next steps

On this page