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
| Package | Language | Status |
|---|---|---|
@0bit/gate (default export) | Node / TypeScript (server) | Source-backed preview |
@0bit/gate/browser | Browser (ESM + UMD + IIFE for CDN) | Source-backed preview |
@0bit/gate/react | React components | Source-backed preview |
0bit-gate (zerobit.gate) | Python (server) | Source-backed preview |
| PHP (Composer) | PHP | Generate from spec |
| Ruby (gem) | Ruby | Generate from spec |
| Go (module) | Go | Generate from spec |
| Java (Maven Central) | Java | Generate from spec |
| .NET (NuGet) | C# / .NET | Generate 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/gateimport { 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/gateimport { 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 -AGenerate 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-clientimport { 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
| Surface | Sandbox value |
|---|---|
| API base URL | https://gate-api-sandbox.0bit.app/v1 |
| Widget iframe origin | https://gate-sandbox.0bit.app |
| Keys | pk_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:
| Key | Host | Result |
|---|---|---|
*_test_* | gate-api-sandbox.0bit.app | Allowed |
*_live_* | gate-api.0bit.app | Allowed |
*_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:
| Field | Value |
|---|---|
| Card number | 4242 4242 4242 4242 |
| Expiry | Any future date |
| CVC | Any 3 digits |
| 3DS | Approve 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:
- 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.
- Fire a synthetic event.
POST /v1/webhooks/testenqueues awebhook.testevent to your configuredwebhook_url(data.livemodeis alwaysfalse). Returns400if nowebhook_urlis set. - Drive a real lifecycle. Run a sandbox checkout with the test card to
produce genuine
gate_session.created → processing → completeddeliveries. - Inspect & replay.
GET /v1/webhooks/deliverieslists your outbound delivery log;POST /v1/webhooks/deliveries/{id}/replayre-queues adead_lettereddelivery.
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 onPOST /v1/rails/pay_ins/POST /v1/rails/pay_outsfor High-tier rails). - Recommended on every state-changing
POSTwhere you want safe retries. - Not needed on
GET(already idempotent) orPOST /v1/gate_sessions/{id}/cancel(cancelling an already-cancelled session returns409— 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
| Status | Meaning | What to do |
|---|---|---|
400 | Invalid request | Fix the payload. Common: amount doesn't match the session, an extra field on a closed-schema body, malformed currency/asset. Do not blind-retry. |
401 | Bad / missing key | Check the Authorization: Bearer header and that the key isn't revoked. |
403 | Forbidden | Mode/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. |
404 | Not found | Wrong id, wrong mode (test vs live data), or a kind-scoped id used on the wrong route. |
409 | Conflict | Already-terminal session, duplicate customer, or an idempotency operation already in progress. Re-read via GET to resolve. |
422 | Unprocessable | Corridor / schema mismatch (e.g. quote vs session, idempotency key with a different body). Correct the inputs. |
429 | Rate limited | Honor the Retry-After header and back off. |
5xx | Server error | Transient — 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 beyondPOST /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_urlin 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_domainsis shared across both modes. - Ensure
return_url/cancel_urlare HTTPS and their origin is inallowed_domains(validated at session create).
4. Switch host & keys
- Swap the API base URL
gate-api-sandbox.0bit.app→gate-api.0bit.app. - Swap the widget iframe origin
gate-sandbox.0bit.app→gate.0bit.app(or set the SDKenvironmenttoproduction). - Swap all
*_test_*keys for*_live_*(a mode/host mismatch returns403). - Re-run the capabilities smoke test against the live host to confirm the live key works.
5. Resilience & monitoring
- Send an
Idempotency-Keyon everyPOSTand reuse it across retries. - Implement
429(honorRetry-After) and5xxbackoff; never retry other4xx. - 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}andGET /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
Quickstart
Your first session, widget, and webhook end to end.
Authentication
Key types, the embed handshake, and signature verification.
API reference
Every endpoint, request shape, and the OpenAPI spec.
0Gate
The buy / sell / swap product these resources build on.
Partner Hub
API keys, webhook settings, entitled products, usage, and OpenAPI downloads.
Support
Live keys, allowed_domains changes, and integration help.