0Bit Documentation

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

SurfaceRunsKeyCalls
Server SDK (@0bit/gate, 0bit-gate)Your backendsk_test_… / sk_live_…gate_sessions create / retrieve / cancel
Browser SDK (@0bit/gate/browser, @0bit/gate/react)Your pagepk_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:

CredentialShapeLivesUsed for
Publishable keypk_test_…Browser (safe to expose)One call only: POST /v1/embed/bootstrap
Secret keysk_test_…Server only — never the browserCreate / retrieve / cancel sessions
Webhook signing secretwhsec_…Server onlyVerifying 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 — browser

Never 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:

FieldTypeRequiredNotes
amountstringDecimal string, ^[0-9]+(\.[0-9]{1,8})?$, > 0. Up to 8 fractional digits.
currencystringISO 4217 three-letter code, ^[A-Za-z]{3}$.
return_urlstring (uri)Origin must be in allowed_domains. HTTPS only (loopback in dev).
cancel_urlstring (uri)Where to send the user if they abandon.
target_tokenstring^[A-Za-z0-9]{2,12}$. Pre-pick the asset.
target_networkstring^[A-Za-z0-9_-]{2,30}$.
flowenumon_ramp | off_ramp | swap. Locks the widget to one flow and hides the tab strip. Omit for the open (tabbed) widget.
wallet_addressstring (≤128)Pre-filled destination wallet. Validated against the resolved network at flow time.
user_referencestring (≤128)Opaque partner-side order/user id. Echoed in webhook payloads for correlation.
kyc_packageobjectEnterprise: partner-supplied pre-verified KYC. Contract-gated — only accepted when your partner record has kyc_trusted: true, else 403 kyc_package_not_trusted.
metadataobjectArbitrary 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/gate
import { 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

MethodEndpointReturns
sessions.create(params, opts?)POST /v1/gate_sessionsGateSessionWithClientSecret
sessions.retrieve(id, opts?)GET /v1/gate_sessions/{id}GateSession (no client_secret)
sessions.cancel(id, opts?)POST /v1/gate_sessions/{id}/cancelGateSession

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/gate
import { 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:

OptionTypeDefaultNotes
publishableKeystringpk_test_* / pk_live_*. sk_* throws SecretKeyInBrowserError; any other shape throws ConfigError.
clientSecretstringgsec_… 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.
apiBaseUrlstring(from env)Override the API host. Self-hosted / local only.
iframeUrlstring(from env)Override the widget origin. Self-hosted / local only.
theme'light' | 'dark'Initial theme; switchable at runtime with setTheme().
targetToken / targetNetworkstringPartner constraints forwarded to the iframe.
returnUrlstringRedirect on success. Optional when session-bound.
flow'on_ramp' | 'off_ramp' | 'swap'Local flow lock; prefer setting flow on the session instead.
heightnumber600Iframe 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:

CallbackFires onPayload
onReadyWidget mounted and ready{ capabilities?: string[] }
onSuccessTerminal happy path{ txId, sessionId?, amount?, currency? }
onErrorTerminal failure{ code, message, intentId? }
onCloseUser dismissed the iframe{ reason: 'user_close' | 'session_expired' | 'unknown' }
onUnavailablePartner 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 fast

Use 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

SDKSignatureThrows
Nodeclient.webhooks.constructEvent(payload, header, secret, { tolerance? })WebhookSignatureError
Pythonclient.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:

EventWhenNotes
gate_session.createdSession minteddata is the GateSession.
gate_session.completedSettleddata adds tx_refid — the source of truth. Fulfill here.
gate_session.expiredTTL elapsed
gate_session.cancelledCancelled
Contract-specific failure eventIntent failedHandle only the event names documented for your contract.
gate_session.kyc_package_acceptedPre-verified KYC acceptedRedacted; raw kyc_package is never echoed.
kyc.requiredKYC needed
partner.quota.warning / partner.quota.exhaustedUsage 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

ChangeSandboxProduction
SDK API origin (baseUrl)https://gate-api-sandbox.0bit.apphttps://gate-api.0bit.app
Raw REST/OpenAPI basehttps://gate-api-sandbox.0bit.app/v1https://gate-api.0bit.app/v1
SDK environment'sandbox''production'
Secret keysk_test_…sk_live_…
Publishable keypk_test_…pk_live_…
Webhook secretwhsec_… (test)whsec_… (live)
Widget iframe originhttps://gate-sandbox.0bit.apphttps://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):

HelperEffectWebhook
POST /v1/test_helpers/sessions/{id}/completeMarks the session completedEmits gate_session.completed — use this to test your verifier end to end
POST /v1/test_helpers/sessions/{id}/failMarks the underlying intent failed; the session stays openEmits 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_domains allowlist (exact match, no wildcards in v1). - Point your live webhook_url at a verified endpoint and confirm it returns 2xx. - 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

On this page