Developers
Developer Resources

Developer Resources

SDKs, testing tools, idempotency, error handling, and a production go-live checklist for building on 0Bit.

Practical resources for building and shipping a 0Bit integration: the client libraries you can install today, how to generate one for any other language, how to test against the sandbox, the retry and error-handling patterns that keep money movement safe, and a checklist that takes you from pk_test_ to live traffic.

Everything here applies to 0Gate — the live buy / sell / swap product partners build on today. The same conventions (keys, idempotency, signed webhooks, the error envelope) carry forward to 0Pools and 0Base as those surfaces open up.

SDKs & client libraries

0Bit maintains source for server and browser SDKs in the languages partners ask for most. Confirm the package version available to your environment before publishing registry-specific install instructions. Everything else should be generated from the reviewed OpenAPI spec.

Official packages

PackageLanguageStatus
@0bit/gate (default export)Node / TypeScript (server)Source-backed preview
@0bit/gate/browserBrowser (ESM + UMD + IIFE for CDN)Source-backed preview
@0bit/gate/reactReact componentsSource-backed preview
0bit-gate (zerobit.gate)Python (server)Source-backed preview
PHP (Composer)PHPGenerate from spec
Ruby (gem)RubyGenerate from spec
Go (module)GoGenerate from spec
Java (Maven Central)JavaGenerate from spec
.NET (NuGet)C# / .NETGenerate from spec

Confirm package availability

The current package names are @0bit/gate and 0bit-gate. Confirm the published registry version before relying on install commands. There is no reviewed Gate package on Composer, RubyGems, Go modules, Maven, or NuGet; for those languages, generate a client from the spec below.

Install

npm install @0bit/gate
import { GateClient } from '@0bit/gate';

const gate = new GateClient({
  secretKey: process.env.OBIT_SECRET_KEY, // sk_test_... or sk_live_...
});
pip install 0bit-gate
# Webhook helpers for Django:
pip install "0bit-gate[django]"
from zerobit.gate import GateClient

gate = GateClient(secret_key=os.environ["OBIT_SECRET_KEY"])
npm install @0bit/gate
import { RampCheckout } from '@0bit/gate/react';

// clientSecret comes from POST /v1/gate_sessions on your server
<RampCheckout publishableKey={pk} clientSecret={clientSecret} />

Pin the version and keep the Subresource Integrity (SRI) hash and crossorigin="anonymous". The global is GateJS.

<script
  src="https://cdn.jsdelivr.net/npm/@0bit/[email protected]/dist/browser/gate.iife.js"
  integrity="sha384-EvKrhzmjkJcmdaIhU2l1JSEnrQexc6smjEuyY71Q6Y4eIoqcyGW20rwuSKSAQRq2"
  crossorigin="anonymous"></script>

Regenerate the hash whenever you bump the version:

curl -fsSL https://cdn.jsdelivr.net/npm/@0bit/gate@<version>/dist/browser/gate.iife.js \
  | openssl dgst -sha384 -binary | openssl base64 -A

Generate a client for any language

The OpenAPI spec is the source of truth for the entire API. For PHP, Ruby, Go, Java, .NET, or anything else, generate a typed client with openapi-generator.

# 1. Download the spec
curl -O "$OBIT_GATE_OPENAPI_URL"

# 2. Generate a client (swap -g for php | ruby | go | java | csharp | ...)
npx @openapitools/openapi-generator-cli generate \
  -i gate-v1.yaml \
  -g typescript-fetch \
  -o ./0bit-client \
  --additional-properties=npmName=@your-org/0bit-client
import { SessionsApi, Configuration } from './0bit-client';

const api = new SessionsApi(new Configuration({
  basePath: 'https://gate-api-sandbox.0bit.app/v1',
  accessToken: process.env.OBIT_SECRET_KEY,
}));

const session = await api.createSession({
  createSessionRequest: { amount: '100', currency: 'EUR', return_url: 'https://app.example/done' },
});

Migrating from the legacy widget

The old OmavonPaymentWidget (window.PaymentWidgetSDK / window.GateRamp, the widget.js loader) is retired and throws on construction. Replace new OmavonPaymentWidget({...}) with new GateRamp({ publishableKey, clientSecret }), widget.init('#c') with ramp.mount('#c', { onSuccess, onError, onClose }), and config.apiKey with config.publishableKey.

Testing

Everything starts in sandbox. Test keys (pk_test_ / sk_test_) hit the sandbox host, run on isolated test data, skip live KYC, and move no real money. Build and validate the whole flow there before requesting live keys.

Sandbox host & keys

SurfaceSandbox value
API base URLhttps://gate-api-sandbox.0bit.app/v1
Widget iframe originhttps://gate-sandbox.0bit.app
Keyspk_test_... (browser) · sk_test_... (server only)

A key's mode must match the host. The mode check runs before key lookup, so a mismatch returns a 403 with a stable code rather than a generic auth error:

KeyHostResult
*_test_*gate-api-sandbox.0bit.appAllowed
*_live_*gate-api.0bit.appAllowed
*_test_*gate-api.0bit.app (live)403 test_key_on_live
*_live_*gate-api-sandbox.0bit.app (sandbox)403 live_key_on_sandbox

Smoke-test the API

A 200 on a capabilities call confirms your key, host, and mode all line up.

export GATE_API_BASE=https://gate-api-sandbox.0bit.app/v1
export OBIT_SECRET_KEY=sk_test_...

# Confirm auth + mode
curl -sS -H "Authorization: Bearer $OBIT_SECRET_KEY" \
  "$GATE_API_BASE/capabilities/countries" | jq .

# Preview quotes for all available payment methods in one call
curl -sS -X POST -H "Authorization: Bearer $OBIT_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"currency":"EUR","asset":"USDC","amount":"100","side":"on_ramp","country_code":"DE"}' \
  "$GATE_API_BASE/quotes/preview" | jq .
const base = 'https://gate-api-sandbox.0bit.app/v1';
const headers = { Authorization: `Bearer ${process.env.OBIT_SECRET_KEY}` };

const res = await fetch(`${base}/capabilities/countries`, { headers });
console.log(res.status, await res.json());
import os, requests

base = "https://gate-api-sandbox.0bit.app/v1"
headers = {"Authorization": f"Bearer {os.environ['OBIT_SECRET_KEY']}"}

res = requests.get(f"{base}/capabilities/countries", headers=headers)
print(res.status_code, res.json())

Test card & the widget flow

In the sandbox widget, complete a checkout with the test card:

FieldValue
Card number4242 4242 4242 4242
ExpiryAny future date
CVCAny 3 digits
3DSApprove the test challenge

Within seconds a gate_session.completed webhook arrives at your endpoint, and the browser onSuccess callback fires at roughly the same time.

The webhook is the source of truth — onSuccess is UX only

Treat the browser onSuccess callback (and a hosted-redirect arriving at return_url) as "the user finished," not "the payment settled." It can be spoofed or dropped. Only fulfil against the signed gate_session.completed webhook (or a GET /v1/gate_sessions/{id} showing completed).

Simulating webhook events

The webhooks in the OpenAPI spec are server-to-partner deliveries — they are not callable Postman requests. To exercise your handler locally:

  1. Tunnel a public URL. Run ngrok or a Cloudflare Tunnel to your local server and point your sandbox webhook endpoint (set in Partner Hub) at it.
  2. Fire a synthetic event. POST /v1/webhooks/test enqueues a webhook.test event to your configured webhook_url (data.livemode is always false). Returns 400 if no webhook_url is set.
  3. Drive a real lifecycle. Run a sandbox checkout with the test card to produce genuine gate_session.created → processing → completed deliveries.
  4. Inspect & replay. GET /v1/webhooks/deliveries lists your outbound delivery log; POST /v1/webhooks/deliveries/{id}/replay re-queues a dead_lettered delivery.

Postman & CI

A Postman collection covering the full v1 surface ships in the monorepo, along with a sandbox environment file. Import both, select the Gate Sandbox environment, set secretKey / publishableKey, and run Capabilities → List countries to confirm auth.

Idempotency & safe retries

The network is unreliable. When a POST times out you can't tell whether 0Bit processed it and the response was lost, or it never arrived. The Idempotency-Key header makes a retry safe: send a unique UUID per logical operation, and any retry with the same key and same body returns the original response instead of creating a second resource or moving funds twice.

  • Required on POST /v1/gate_sessions (and on POST /v1/rails/pay_ins / POST /v1/rails/pay_outs for High-tier rails).
  • Recommended on every state-changing POST where you want safe retries.
  • Not needed on GET (already idempotent) or POST /v1/gate_sessions/{id}/cancel (cancelling an already-cancelled session returns 409 — the correct signal).

Keys are remembered for 24 hours. After that, the same key + same body creates a fresh resource — so don't retry across a window longer than that.

const idempotencyKey = crypto.randomUUID(); // ONE key for this logical operation

async function createSessionWithRetry() {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await fetch('https://gate-api-sandbox.0bit.app/v1/gate_sessions', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.OBIT_SECRET_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey, // SAME key across all retries
        },
        body: JSON.stringify({
          amount: '100',
          currency: 'EUR',
          return_url: 'https://app.example/done',
        }),
      });
    } catch (err) {
      if (attempt === 2) throw err;
      await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt)); // exponential backoff
    }
  }
}

Error handling patterns

Errors return a JSON envelope. Branch on statusCode (the HTTP status), not on human-readable message text. Many errors also carry a stable string code for finer-grained branching, and every response includes a requestId you should log and quote in support requests.

{
  "statusCode": 403,
  "error": "forbidden",
  "code": "test_key_on_live",
  "message": "A test-mode key was used against the live host.",
  "requestId": "req_8f2a1c"
}

The requestId is also returned as the X-Request-Id response header. 5xx bodies are scrubbed to a generic message — don't try to parse detail out of them.

How to handle each status

StatusMeaningWhat to do
400Invalid requestFix the payload. Common: amount doesn't match the session, an extra field on a closed-schema body, malformed currency/asset. Do not blind-retry.
401Bad / missing keyCheck the Authorization: Bearer header and that the key isn't revoked.
403ForbiddenMode/host mismatch (test_key_on_live / live_key_on_sandbox), origin not in allowed_domains, or an entitlement you don't have. Fix config; don't retry.
404Not foundWrong id, wrong mode (test vs live data), or a kind-scoped id used on the wrong route.
409ConflictAlready-terminal session, duplicate customer, or an idempotency operation already in progress. Re-read via GET to resolve.
422UnprocessableCorridor / schema mismatch (e.g. quote vs session, idempotency key with a different body). Correct the inputs.
429Rate limitedHonor the Retry-After header and back off.
5xxServer errorTransient — retry with backoff and the same Idempotency-Key so a retry can't double-execute.

Retry only what's safe

Retry on network errors, 429, and 5xx. Do not retry 4xx other than 429 — the request won't succeed until you change it. When you do retry a POST, always reuse the original Idempotency-Key.

async function call(path, init, idempotencyKey) {
  for (let attempt = 0; attempt < 4; attempt++) {
    const res = await fetch(`https://gate-api.0bit.app/v1${path}`, {
      ...init,
      headers: {
        Authorization: `Bearer ${process.env.OBIT_SECRET_KEY}`,
        'Content-Type': 'application/json',
        ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
        ...init.headers,
      },
    });

    if (res.ok) return res.json();

    if (res.status === 429) {
      const wait = Number(res.headers.get('Retry-After') ?? 1) * 1000;
      await new Promise((r) => setTimeout(r, wait));
      continue;
    }
    if (res.status >= 500) {
      await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
      continue;
    }

    const body = await res.json(); // 4xx — surface code + requestId, don't retry
    throw new Error(`${body.code ?? body.error} (${body.requestId}): ${body.message}`);
  }
  throw new Error('exhausted retries');
}

The official SDKs do the 429 backoff, retry, and automatic Idempotency-Key for you and expose typed errors — prefer them where they exist.

Webhook signature verification

Verify every webhook before trusting it. The signature is in the Gate-Signature header in Stripe-compatible format t=<unix>,v1=<hex>. Compute HMAC-SHA256 of ${t}.${rawBody} with your mode's webhook signing secret, compare against v1 with a timing-safe equal, and reject if the timestamp is older than the skew window (default 300s).

Pass the raw body, and use the right secret

Verify against the raw request bytes, before any JSON parsing or re-serialization — re-stringifying changes the bytes and breaks the signature. Each mode (test/live) has its own webhook secret; use the one matching the host that sent the event. Dedupe on the event id (retries reuse it) and acknowledge with any 2xx.

import crypto from 'node:crypto';

function verifyWebhook(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // stale

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  return crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}

See Authentication for the full credential model and 0Gate for the end-to-end session and webhook lifecycle.

Go-live checklist

Going from sandbox to production needs no code changes beyond credentials and hosts — the routes and payload shapes are identical. Work through the list below before flipping to live keys.

1. Credentials

  • Request live keys (pk_live_ / sk_live_) from [email protected] — the self-service portal is in private beta.
  • Store sk_live_ in a secret manager; it is shown once. Never ship it to the browser or put it in a URL.
  • Confirm pk_live_ is the only key exposed client-side (it authorizes nothing beyond POST /v1/embed/bootstrap).
  • Plan rotation: rotate sk_* periodically; mint the new key, deploy, then revoke the old after traffic confirms.

2. Webhooks

  • Configure a separate live webhook_url in Partner Hub (live has its own signing secret).
  • Verify the Gate-Signature (HMAC-SHA256 over ${t}.${rawBody}) against the live secret, using the raw body.
  • Enforce timestamp skew (default 300s) and dedupe on the event id.
  • Make the handler idempotent and fulfil only on gate_session.completed.
  • Acknowledge with 2xx; let non-2xx flow into the delivery log / dead-letter + replay.

3. Domains & origins

  • Add every production origin to allowed_domains (via [email protected] today) — exact origins, no wildcards.
  • Keep dev origins (e.g. http://localhost:3000) listed too; allowed_domains is shared across both modes.
  • Ensure return_url / cancel_url are HTTPS and their origin is in allowed_domains (validated at session create).

4. Switch host & keys

  • Swap the API base URL gate-api-sandbox.0bit.appgate-api.0bit.app.
  • Swap the widget iframe origin gate-sandbox.0bit.appgate.0bit.app (or set the SDK environment to production).
  • Swap all *_test_* keys for *_live_* (a mode/host mismatch returns 403).
  • Re-run the capabilities smoke test against the live host to confirm the live key works.

5. Resilience & monitoring

  • Send an Idempotency-Key on every POST and reuse it across retries.
  • Implement 429 (honor Retry-After) and 5xx backoff; never retry other 4xx.
  • Log requestId (X-Request-Id) on every error for support and tracing.
  • Alert on webhook failures, dead-letters, and settlement that stalls before completed.
  • Reconcile independently via GET /v1/gate_sessions/{id} and GET /v1/transactions/{refid} — don't rely on browser callbacks.

Advanced capabilities are gated

Swap, the High-tier Rails API (POST /v1/quotes, /v1/rails/pay_ins, /v1/rails/pay_outs), and kyc_package require an entitlement or signed contract, not just live keys. Until enabled, those endpoints return 403 (e.g. rails_api_not_enabled, kyc_package_not_trusted). Contact us before building against them.

Next steps

On this page