0Gate quickstart
Your first 0Gate integration path: get sandbox keys, create a session server-side, embed the widget, verify signed webhooks, and prepare a reviewed production cutover.
This is the fastest path from zero to a working 0Gate buy/sell/swap sandbox flow. You will get sandbox keys, create a session on your server, embed the widget in your page, verify the signed webhook path, and then prepare a reviewed production cutover.
0Gate is 0Bit's embeddable fiat ↔ crypto ramp surface for approved buy, sell, and swap flows. You bring the users, order intent, backend records, and product-specific obligations; the hosted 0Gate surface handles the approved user-facing verification, payment, ramp, and completion journey. The integration is two parts: a session you create server-side with your secret key, and an iframe widget you mount client-side with your publishable key. The structure deliberately mirrors Stripe Checkout.
SDKs are preview packages
The audited SDK package is @0bit/gate on npm and 0bit-gate on Python. The package source is version 0.1.4, while
the Node runtime user agent still reports gate-node/0.1.0, so pin versions and confirm the package published for
your environment before treating the SDK as GA. You can always call the REST API directly.
How it works
The key idea: the onSuccess callback is for UX only. The authoritative settlement signal is the signed webhook delivered to your server. Fulfill on gate_session.completed, never on the browser callback alone.
The two keys, the two surfaces
| Surface | Runs | Key | Calls |
|---|---|---|---|
Server SDK (@0bit/gate, 0bit-gate) | Your backend | sk_test_… / sk_live_… | gate_sessions create / retrieve / cancel |
Browser SDK (@0bit/gate/browser, @0bit/gate/react) | Your page | pk_test_… / pk_live_… | POST /v1/embed/bootstrap only |
The secret key never touches the browser; the publishable key can only bootstrap the embed (it cannot create or read sessions). This split is enforced in code — the browser SDK throws SecretKeyInBrowserError if it sees an sk_* value.
1. Get sandbox keys
Sign in to Partner Hub and grab your sandbox credentials. Every integration uses three values:
| Credential | Shape | Lives | Used for |
|---|---|---|---|
| Publishable key | pk_test_… | Browser (safe to expose) | One call only: POST /v1/embed/bootstrap |
| Secret key | sk_test_… | Server only — never the browser | Create / retrieve / cancel sessions |
| Webhook signing secret | whsec_… | Server only | Verifying inbound webhooks |
Store the secret key and webhook secret in your secret manager. The publishable key is safe to ship in your front-end bundle — that is by design.
# .env (sandbox)
GATE_KEY=sk_test_AbCd1234... # secret key — server only
WEBHOOK_SECRET=whsec_QrSt5678... # webhook signing secret — server only
VITE_GATE_PUBLISHABLE_KEY=pk_test_EfGh9012... # publishable key — browserNever ship sk_ to the browser
The secret key can create sessions and (on entitled tiers) move money — treat it like a database password. The browser
only ever needs the publishable key and a session's client_secret. The SDK refuses an sk_* value passed where a
publishable key is expected, but that is a backstop, not a substitute for keeping it server-side.
Key naming convention
The SDKs do not read any environment variable themselves — apiKey / api_key= / publishableKey is always passed explicitly to the constructor. The names above (GATE_KEY, WEBHOOK_SECRET, VITE_GATE_PUBLISHABLE_KEY) are the conventions used throughout the shipped examples; you are free to name your own variables differently as long as you pass the values through.
Allow-list your embed origins
Bootstrap (step 3) is origin-checked against your partner record's allowed_domains (exact match — no wildcards in v1). Register every origin you will embed from in Partner Hub before you mount the widget, or POST /v1/embed/bootstrap returns a 403 and the SDK throws EmbedBootstrapError.
2. Create a session (server-side)
A session locks the amount and currency server-side so a user cannot tamper with them in the browser. POST /v1/gate_sessions with your secret key and you get back a GateSession whose client_secret (shaped gsec_<id>_<random>) is browser-safe and binds the widget to exactly that one session.
Use the right URL shape
Raw REST examples call the OpenAPI base URL with /v1, such as https://gate-api-sandbox.0bit.app/v1. SDK
constructors take the API origin only, such as https://gate-api-sandbox.0bit.app, because resource methods append
/v1/gate_sessions internally.
Request body
amount, currency, and return_url are required; everything else is optional. The full shape from the OpenAPI spec:
| Field | Type | Required | Notes |
|---|---|---|---|
amount | string | ✓ | Decimal string, ^[0-9]+(\.[0-9]{1,8})?$, > 0. Up to 8 fractional digits. |
currency | string | ✓ | ISO 4217 three-letter code, ^[A-Za-z]{3}$. |
return_url | string (uri) | ✓ | Origin must be in allowed_domains. HTTPS only (loopback in dev). |
cancel_url | string (uri) | — | Where to send the user if they abandon. |
target_token | string | — | ^[A-Za-z0-9]{2,12}$. Pre-pick the asset. |
target_network | string | — | ^[A-Za-z0-9_-]{2,30}$. |
flow | enum | — | on_ramp | off_ramp | swap. Locks the widget to one flow and hides the tab strip. Omit for the open (tabbed) widget. |
wallet_address | string (≤128) | — | Pre-filled destination wallet. Validated against the resolved network at flow time. |
user_reference | string (≤128) | — | Opaque partner-side order/user id. Echoed in webhook payloads for correlation. |
kyc_package | object | — | Enterprise: partner-supplied pre-verified KYC. Contract-gated — only accepted when your partner record has kyc_trusted: true, else 403 kyc_package_not_trusted. |
metadata | object | — | Arbitrary key/value bag, echoed back to you. |
curl 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://app.partner.example/done"
}'npm install @0bit/gateimport { GateClient } from '@0bit/gate';
const client = new GateClient({
apiKey: process.env.GATE_KEY, // sk_test_…
baseUrl: 'https://gate-api-sandbox.0bit.app', // SDK origin; resource paths append /v1
});
// The SDK attaches an Idempotency-Key (uuid v4) automatically on writes.
const session = await client.sessions.create({
amount: '100.00',
currency: 'EUR',
return_url: 'https://app.partner.example/done',
});
// session.client_secret → gsec_… (browser-safe). Send this to your page.pip install 0bit-gate # PyPI dist is "0bit-gate"; import path is "zerobit.gate"import os
from zerobit.gate import GateClient
client = GateClient(
api_key=os.environ["GATE_KEY"], # sk_test_…
base_url="https://gate-api-sandbox.0bit.app", # SDK origin; resource paths append /v1
)
# The SDK attaches an Idempotency-Key (uuid v4) automatically on writes.
session = client.sessions.create({
"amount": "100.00",
"currency": "EUR",
"return_url": "https://app.partner.example/done",
})
# session["client_secret"] → gsec_… (browser-safe). Send this to your page.The response object
The response is a GateSession plus the 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"
}status is one of open | completed | expired | cancelled — a session has no failed status (failure is intent-level and arrives as a webhook, not a session state). expires_at defaults to 24h out. The raw client_secret is returned exactly once on create; the backend stores only a SHA-256 hash, so retrieve() never returns it again.
Read and cancel
| Method | Endpoint | Returns |
|---|---|---|
sessions.create(params, opts?) | POST /v1/gate_sessions | GateSessionWithClientSecret |
sessions.retrieve(id, opts?) | GET /v1/gate_sessions/{id} | GateSession (no client_secret) |
sessions.cancel(id, opts?) | POST /v1/gate_sessions/{id}/cancel | GateSession |
cancel() only works on an open session — cancelling one already in a terminal state returns 409 (IdempotencyError in Node). Re-cancelling is deliberately an error, not a no-op, so double-cancel bugs surface.
Idempotency and retries
Every write carries an Idempotency-Key (UUID). The SDK generates one per call automatically; pass your own via RequestOptions (idempotencyKey in Node, idempotency_key= in Python) when you want a known retry to collapse to the original response server-side. The server SDK retries 408 / 425 / 429 / 500 / 502 / 503 / 504 and connection/timeout errors up to maxRetries (default 2, so 3 attempts total) with exponential backoff + jitter (0.5s → 1s → 2s → 4s, capped 30s), honouring Retry-After. Writes are safe to retry because the idempotency key dedupes them.
3. Embed the widget (client-side)
Mount the iframe with @0bit/gate/browser using your publishable key and the client_secret. The SDK calls POST /v1/embed/bootstrap (authorized by pk_*, origin-checked against your allowed_domains), receives a short-lived embed token, and renders the buy/sell/swap UI.
npm install @0bit/gateimport { GateRamp } from '@0bit/gate/browser';
const ramp = new GateRamp({
publishableKey: 'pk_test_EfGh9012...', // browser-safe
clientSecret, // gsec_… from your server (step 2)
environment: 'sandbox',
});
await ramp.mount('#ramp', {
onReady: () => console.log('widget ready'),
onSuccess: ({ txId, sessionId }) => {
// UX only — show a spinner / optimistic state.
// Do NOT fulfill here; wait for the webhook (step 4).
console.log('user finished in-widget', txId, sessionId);
},
onError: ({ code, message }) => console.error(code, message),
onClose: ({ reason }) => console.log('widget closed', reason),
});<div id="ramp"></div>
<script src="https://cdn.jsdelivr.net/npm/@0bit/gate/dist/browser/gate.iife.js"></script>
<script>
// UMD/IIFE build exposes the global `GateJS`.
const ramp = new GateJS.GateRamp({
publishableKey: 'pk_test_EfGh9012...',
clientSecret: window.__CLIENT_SECRET__, // injected by your server
environment: 'sandbox',
});
ramp.mount('#ramp', {
onSuccess: () => {
/* UX only — fulfill on the webhook, not here */
},
});
</script>Constructor options
new GateRamp(config) accepts:
| Option | Type | Default | Notes |
|---|---|---|---|
publishableKey | string | — | pk_test_* / pk_live_*. sk_* throws SecretKeyInBrowserError; any other shape throws ConfigError. |
clientSecret | string | — | gsec_… from step 2. Optional, but required to bind a session (and required for hosted redirect). |
environment | 'production' | 'sandbox' | 'development' | 'production' | Picks the API + iframe hosts. |
apiBaseUrl | string | (from env) | Override the API host. Self-hosted / local only. |
iframeUrl | string | (from env) | Override the widget origin. Self-hosted / local only. |
theme | 'light' | 'dark' | — | Initial theme; switchable at runtime with setTheme(). |
targetToken / targetNetwork | string | — | Partner constraints forwarded to the iframe. |
returnUrl | string | — | Redirect on success. Optional when session-bound. |
flow | 'on_ramp' | 'off_ramp' | 'swap' | — | Local flow lock; prefer setting flow on the session instead. |
height | number | 600 | Iframe height in px. Must be positive. |
Set the sandbox environment
@0bit/gate/browser resolves environment: 'sandbox' to https://gate-api-sandbox.0bit.app and
https://gate-sandbox.0bit.app. Use explicit apiBaseUrl and iframeUrl only for local development or a reviewed
environment override. Production resolves to https://gate-api.0bit.app and https://gate.0bit.app.
Lifecycle callbacks
mount(target, options) returns a Promise<void> that resolves on WIDGET_READY (15s timeout, else it rejects). The callbacks map to the iframe's postMessage events:
| Callback | Fires on | Payload |
|---|---|---|
onReady | Widget mounted and ready | { capabilities?: string[] } |
onSuccess | Terminal happy path | { txId, sessionId?, amount?, currency? } |
onError | Terminal failure | { code, message, intentId? } |
onClose | User dismissed the iframe | { reason: 'user_close' | 'session_expired' | 'unknown' } |
onUnavailable | Partner over quota / unavailable | { reason, message } (mount still resolves; the iframe renders a neutral overlay) |
The success payload is { txId, sessionId? } — not a refid. Use txId for client-side correlation and a spinner, but do not treat it as proof of settlement. When you later read transaction or receipt records for a local-rail flow, prefer local_rail_transaction_id for the rail-side transaction id; older records may still include facilita_transaction_id as a deprecated compatibility alias with the same value.
Runtime controls and hosted redirect
After mounting you can call ramp.setTheme('dark'), ramp.updateConfig({ amount: '250.00' }), and ramp.unmount(). For platforms that can't iframe-embed (mobile webviews, in-app browsers, hostile CSPs), use the static one-shot navigation instead of mounting:
GateRamp.redirectToCheckout({
publishableKey: 'pk_test_EfGh9012...',
clientSecret, // REQUIRED — hosted mode is always session-bound
environment: 'sandbox',
});Using React?
Install @0bit/gate and import React components from @0bit/gate/react. Render <RampCheckout publishableKey={…} clientSecret={…} onSuccess={…} /> and keep callbacks as UX signals only.
Browser error types
The browser SDK throws a small, typed hierarchy (all extend GateError): SecretKeyInBrowserError (an sk_* reached the browser), ConfigError (bad constructor config), MountTargetError (selector not found / not an element), StateError (mount() called twice), and EmbedBootstrapError (bootstrap rejected or unreachable — carries .status).
4. Verify the webhook
When the session settles, 0Bit POSTs a signed event to your webhook_url. Each request carries a Gate-Signature header in the format t=<unix>,v1=<hex>, where v1 is HMAC-SHA256(webhook_secret, "<timestamp>.<rawBody>"), with a 300-second tolerance and a constant-time compare.
Treat the webhook as the source of truth
Fulfill on the verified gate_session.completed webhook — not on the browser onSuccess, which is UX-only and
can be dropped, closed, or spoofed. Make the handler idempotent and dedupe on the event id, because delivery is
at-least-once.
Use the SDK's constructEvent / construct_event — it parses the header, checks the timestamp, computes the HMAC, and constant-time-compares for you, throwing on a bad signature. Pass the raw request bytes, not re-serialized JSON, or the HMAC will never match.
# A delivered webhook looks like this on the wire:
POST /webhooks/0bit HTTP/1.1
Host: app.partner.example
Gate-Signature: t=1700000000,v1=a1b2c3d4e5f6...
X-0bit-Event-Type: gate_session.completed
X-0bit-Event-Id: evt_9f8e7d...
Content-Type: application/json
{"id":"evt_9f8e7d...","type":"gate_session.completed","data":{ "id":"67a1f3b9e4b0c10001234567", "tx_refid":"..." }}# Verify by hand (the algorithm is stable): recompute the HMAC and compare to v1.
SIGNED="${TIMESTAMP}.${RAW_BODY}"
printf '%s' "$SIGNED" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"
# → compare the hex output against the v1=… value in Gate-Signature (constant-time)import express from 'express';
import { GateClient, WebhookSignatureError } from '@0bit/gate';
const client = new GateClient({
apiKey: process.env.GATE_KEY,
baseUrl: 'https://gate-api-sandbox.0bit.app',
});
const app = express();
// Capture the RAW body — a JSON parser would destroy the bytes the HMAC is computed over.
app.post('/webhooks/0bit', express.raw({ type: 'application/json' }), (req, res) => {
try {
const event = client.webhooks.constructEvent(
req.body, // raw Buffer
req.headers['gate-signature'], // NOT x-0bit-signature
process.env.WEBHOOK_SECRET,
);
if (event.type === 'gate_session.completed') {
// SOURCE OF TRUTH — fulfill here, idempotently. Dedupe on event.id.
// event.data is a GateSession plus { tx_refid }.
// fulfillOrder(event.data.id, event.data.tx_refid);
}
res.status(200).end(); // 2xx fast; do heavy work async
} catch (err) {
if (err instanceof WebhookSignatureError) return res.status(400).end();
throw err;
}
});import os
from flask import Flask, request
from zerobit.gate import GateClient, WebhookSignatureError
client = GateClient(
api_key=os.environ["GATE_KEY"],
base_url="https://gate-api-sandbox.0bit.app",
)
app = Flask(__name__)
@app.post("/webhooks/0bit")
def webhook():
try:
event = client.webhooks.construct_event(
request.get_data(), # RAW bytes — not request.json
request.headers.get("Gate-Signature"), # NOT x-0bit-signature
os.environ["WEBHOOK_SECRET"],
)
except WebhookSignatureError:
return "", 400
if event["type"] == "gate_session.completed":
# SOURCE OF TRUTH — fulfill here, idempotently. Dedupe on event["id"].
# fulfill_order(event["data"]["id"], event["data"].get("tx_refid"))
pass
return "", 200 # 2xx fastUse Gate-Signature — never x-0bit-signature
The signature header the backend actually sends is Gate-Signature (a locked constant on the delivery service).
Some older README snippets read x-0bit-signature; that header does not exist on the wire and will silently never
match. Read gate-signature (Node lowercases header names) / Gate-Signature (Python).
The constructEvent signature
| SDK | Signature | Throws |
|---|---|---|
| Node | client.webhooks.constructEvent(payload, header, secret, { tolerance? }) | WebhookSignatureError |
| Python | client.webhooks.construct_event(payload, sig_header, secret, *, tolerance=300) | WebhookSignatureError |
payload must be the raw Buffer / bytes / string. tolerance defaults to 300 seconds (matching the backend WebhookSignerService); tighten it only if you trust your clocks. The helper throws WebhookSignatureError on any failure — malformed header, missing t=/v1=, stale timestamp, signature mismatch, or non-JSON body. Catch it and return 400.
Event types
You will receive these from the delivery engine:
| Event | When | Notes |
|---|---|---|
gate_session.created | Session minted | data is the GateSession. |
gate_session.completed | Settled | data adds tx_refid — the source of truth. Fulfill here. |
gate_session.expired | TTL elapsed | |
gate_session.cancelled | Cancelled | |
| Contract-specific failure event | Intent failed | Handle only the event names documented for your contract. |
gate_session.kyc_package_accepted | Pre-verified KYC accepted | Redacted; raw kyc_package is never echoed. |
kyc.required | KYC needed | |
partner.quota.warning / partner.quota.exhausted | Usage thresholds |
Handle the events you care about and 200 the rest. Use the signed body and documented event id as your dedupe source. Treat delivery retry timing and replay controls as current-contract details that should be confirmed against the webhook reference before publication. For local-rail receipts and transaction reads, use local_rail_transaction_id; keep facilita_transaction_id only for backward-compatible reads of older integrations.
5. Prepare production
Going from sandbox to production keeps the same integration shape, but it is not just a code toggle. Use separate live credentials and hosts, confirm your account entitlements, register production origins, verify webhook delivery, and use product-approved wording for availability, fees, regions, KYC/KYB, support, and settlement.
What changes
| Change | Sandbox | Production |
|---|---|---|
SDK API origin (baseUrl) | https://gate-api-sandbox.0bit.app | https://gate-api.0bit.app |
| Raw REST/OpenAPI base | https://gate-api-sandbox.0bit.app/v1 | https://gate-api.0bit.app/v1 |
SDK environment | 'sandbox' | 'production' |
| Secret key | sk_test_… | sk_live_… |
| Publishable key | pk_test_… | pk_live_… |
| Webhook secret | whsec_… (test) | whsec_… (live) |
| Widget iframe origin | https://gate-sandbox.0bit.app | https://gate.0bit.app |
Exercise the flow before you switch
In sandbox you can drive a session to a terminal state without going through the iframe, using the test helpers (these require an sk_test_* key — they reject sk_live_* and are physically inert in production):
| Helper | Effect | Webhook |
|---|---|---|
POST /v1/test_helpers/sessions/{id}/complete | Marks the session completed | Emits gate_session.completed — use this to test your verifier end to end |
POST /v1/test_helpers/sessions/{id}/fail | Marks the underlying intent failed; the session stays open | Emits no webhook |
Drive complete against a freshly created sandbox session to confirm your constructEvent handler verifies the signature and fulfills exactly once.
Pre-flight checklist
Before production cutover
- List every origin you embed from in your
allowed_domainsallowlist (exact match, no wildcards in v1). - Point your livewebhook_urlat a verified endpoint and confirm it returns2xx. - Load the live webhook secret in your verifier — using the test secret after a switch is the classic cause of 100% verification failures. - Confirm your organization is entitled to the flows you use and that Product, Legal, Compliance, and Operations have approved the relevant live markets, user-facing copy, and support model.
Next steps
Authentication
Keys, modes, the embed handshake, idempotency, and webhook signature verification in depth.
0Gate
The full buy / sell / swap product — flows, kit blocks, and the session lifecycle.
Core concepts
Sessions, the embed token, modes, and the settlement model.
API reference
Every endpoint, object schema, and the typed error model.