0Bit Documentation

API Reference

Base URLs, authentication, versioning, conventions, idempotency, errors, rate limits, and pagination for the 0Bit unified API.

Legacy illustrative scaffold - not the live contract

The auto-generated generic endpoints in this section (/v1/quotes, /v1/checkouts, /v1/pools/balance) are illustrative scaffolding, not the real 0Bit API. Do not integrate against them. For the live endpoint contracts, use the real product references: the 0Pools API and the 0Gate API. The shared-conventions prose below (auth, idempotency, errors, rate limits, pagination) is accurate and still useful.

The 0Bit API is organized by product surface. Use the generated 0Gate reference for hosted sessions, capabilities, transactions, quotes, rails, embed bootstrap, branding, and webhooks. Use 0Pools and 0Base references only where your account is enabled for those product surfaces.

This page is the prose overview for shared conventions: base URLs, authentication, idempotency, error envelopes, rate limits, pagination, and webhook handling.

Use product references for endpoint contracts

The live hosted 0Gate integration is session-based: your server creates a gate_session, the browser bootstraps the embedded widget against it, and settlement is confirmed by webhooks. Do not use legacy order-style endpoints as a public integration contract.

Base URLs

Every endpoint is served from a single host per mode and versioned under /v1.

ModeBase URLKeys accepted
live (production)https://gate-api.0bit.app/v1*_live_* only
test (sandbox)https://gate-api-sandbox.0bit.app/v1*_test_* only

The host and the key's mode are strictly bound: a *_test_* key against the live host (or a *_live_* key against the sandbox host) is rejected. Develop and run CI against the sandbox host with test keys; flip the host and the keys together when you go to production. The hosted widget iframe has its own origins — https://gate.0bit.app (live) and https://gate-sandbox.0bit.app (test) — and you manage keys, webhook URLs, and allowed domains in the Partner Hub.

0Pools is served from its own host

The table above is the 0Gate host. The headless 0Pools API is served from https://pools-api.0bit.app/v1 and is approved-partner-only, not blanket-GA or self-serve. It is a secret-key, server-to-server surface (a pk_* is limited to discovery — listing the pools you're entitled to); there is no public sandbox host. Use the 0Pools reference for the exact endpoint contracts.

Authentication

All requests use bearer authentication:

Authorization: Bearer sk_live_AbCd1234...

0Bit issues two key types per organization per mode. They are platform credentials — the same pk_* / sk_* keys identify your organization across every product you're entitled to, not one product each.

CredentialPrefixWhere it livesWhat it can do
Publishable keypk_test_ / pk_live_Browser-safe; ship it in your front-endAuthorizes only POST /v1/embed/bootstrap to start a widget session
Secret keysk_test_ / sk_live_Server only — never the browserFull access: create, retrieve, and cancel sessions; rails; everything money-moving

Two short-lived, derived credentials complete the handshake and are normally managed by the SDK rather than handled by you:

  • Embed token — a short-lived JWT (default TTL ~1h) returned by POST /v1/embed/bootstrap. The widget sends it as the X-Embed-Token header on subsequent iframe-to-backend calls. This is the OAuth-like exchange that scopes the browser to one partner and (optionally) one session.
  • client_secret — format gsec_<sessionId>_<random>, returned once by POST /v1/gate_sessions. It is browser-safe and binds the widget to a single session.

Keep secret keys server-side

A leaked sk_* can move money. Never embed it in browser code, URLs, client bundles, or anything reachable by getServerSideProps-style client output. If a secret key leaks, rotate it immediately in the Partner Hub. A pk_* is browser-safe by design — even if copied, it can only call POST /v1/embed/bootstrap, which is itself gated by your origin allowlist.

Mismatching a key's mode against the host returns 403 with a stable code such as test_key_on_live or live_key_on_sandbox. A valid key with no entitlement for the product you're calling also returns 403. See Authentication for the full credential model, env-var conventions (OBIT_SECRET_KEY, OBIT_PUBLISHABLE_KEY, OBIT_WEBHOOK_SECRET), and the bootstrap flow.

Versioning

Every path is prefixed with /v1. We commit to never breaking a /v1 shape: when a breaking change is needed it ships as /v2, with both versions available during a deprecation window. New optional fields and new endpoints can be added to /v1 without notice, so write clients that ignore unknown fields and branch on documented values only.

Request and response conventions

Requests and responses are JSON. The conventions below hold across the API:

  • Bearer auth on every request via the Authorization header.
  • Content-Type: application/json on every request with a body.
  • snake_case field names (return_url, client_secret, created_at).
  • Money as string-formatted decimals ("100.50", not 100.50) to preserve precision and avoid floating-point drift.
  • ISO-8601 timestamps with a Z suffix ("2026-05-25T13:45:00Z").
  • null, not omitted/undefined, for absent optional fields.
  • Arrays for repeating values (allowed_domains: string[]), never comma-separated strings.

0Pools uses camelCase field names

The snake_case convention and the object discriminator described here are 0Gate shapes. The 0Pools API uses camelCase field names (quoteId, destAddress, spreadBps, nextCursor) and does not carry an object field. The money-and-decimals, ISO-8601, null-not-omitted, and arrays-not-CSV rules still hold; basis-point values (spreadBps, feeBps, totalBps) are integers, while money and rate values are decimal strings.

Every resource is a JSON object with an object discriminator field so you can narrow types in dynamic languages:

{
  "id": "67a1f3b9e4b0c10001234567",
  "object": "gate_session",
  "mode": "live",
  "amount": "100.00",
  "currency": "EUR",
  "status": "open"
}

HTTP methods

MethodUsed for
GETRetrieve a resource or list (safe, idempotent)
POSTCreate a resource, or perform an action such as /cancel

The core v1 surface is built on GET and POST — there is no PUT. Action verbs (like cancelling a session) are modeled as POST to a sub-resource, e.g. POST /v1/gate_sessions/:id/cancel.

Customers resource is preview/internal

You may see PATCH and DELETE referenced on a Customers resource (e.g. partial customer update, customer soft-delete). Treat the Customers resource as preview / internal rather than part of the stable v1 partner contract: in the standard session flow, one session maps to one ephemeral end-user, and partner-managed customer records are not generally available. Do not build production logic against PATCH/DELETE semantics here without confirming entitlement — contact support@0bit.io if you need it.

There is no public refunds endpoint in v1; refunds are handled partner-to-0Bit through support.

Idempotency

Send a unique Idempotency-Key (a UUID v4 is the obvious choice) on all POST requests. Retries that reuse the same key and the same body return the original response — even if the first attempt already succeeded — so a timed-out request is always safe to repeat.

Idempotency-Key: 3f8c2a1e-9b4d-4c7a-8e21-0a1b2c3d4e5f
  • Required on POST /v1/gate_sessions (the money-moving precursor). Recommended on every other state-changing POST.
  • Not needed on GET (already idempotent) or on POST .../cancel (already idempotent — cancelling a cancelled session returns 409, the correct signal).
  • The key must be stable across retries of the same operation. Regenerating per retry defeats deduplication.
  • Retention is 24 hours. After that the key is forgotten and a repeat creates a fresh resource.
  • Same key, different body returns the first response (a warning is logged for the mismatch); a differing body is a client-side retry bug.
  • Replays are transparent — identical status, body, and headers as the original, with no "this was a replay" marker. Your code should be indifferent.
  • Concurrent duplicates: if two requests with the same key race, one wins the write and the other gets 409 { "message": "Operation already in progress" }. Re-issue a GET for the result or simply retry.

For 0Pools, the equivalent money write is POST /v1/pools/{id}/transact, which requires an Idempotency-Key. That call is also idempotent on the quoteId it executes, so reusing a key with a different body returns 409.

curl -X POST https://gate-api.0bit.app/v1/gate_sessions \
  -H "Authorization: Bearer $OBIT_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f8c2a1e-9b4d-4c7a-8e21-0a1b2c3d4e5f" \
  -d '{ "amount": "100", "currency": "EUR", "return_url": "https://app.example.com/done" }'
import crypto from 'node:crypto';

// SAME key across every retry of this logical operation.
const idempotencyKey = crypto.randomUUID();

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

# SAME key across every retry of this logical operation.
idempotency_key = str(uuid.uuid4())

def create_session():
    return requests.post(
        "https://gate-api.0bit.app/v1/gate_sessions",
        headers={
            "Authorization": f"Bearer {os.environ['OBIT_SECRET_KEY']}",
            "Content-Type": "application/json",
            "Idempotency-Key": idempotency_key,
        },
        json={
            "amount": "100",
            "currency": "EUR",
            "return_url": "https://app.example.com/done",
        },
    )

Errors

Every error returns the same JSON envelope — it is identical across products. Every response also carries an X-Request-Id header. Branch on the machine-readable type (enum: invalid_request | unauthorized | forbidden | not_found | conflict | rate_limited | server_error) and the stable code, never on the human-readable message.

{
  "type": "not_found",
  "code": "not_found",
  "message": "human-readable message",
  "request_id": "uuid",
  "doc_url": null,
  "statusCode": 404
}
FieldTypeDescription
typestringMachine-readable error class — the value your code should branch on. One of invalid_request, unauthorized, forbidden, not_found, conflict, rate_limited, server_error.
codestringStable, finer-grained code (e.g. test_key_on_live, kyc_package_not_trusted, live_keys_not_allowed). Safe to branch on.
messagestringHuman-readable detail. For display, not for routing.
request_idstringPer-request id, also returned as the X-Request-Id response header. Quote it to support.
doc_urlstring | nullLink to relevant docs for this error, when available.
statusCodenumberHTTP status code.
StatusMeaningWhat your client should do
400Validation error — bad inputFix the request shape. Don't auto-retry; the same input fails again.
401Missing / malformed credential, or wrong key mode for the hostCheck the Authorization header and that the key's mode matches the host. Don't retry.
403Credential rejected (revoked key, wrong origin, wrong partner, missing entitlement, mode mismatch)Don't retry. Surface to your operator.
404Resource doesn't exist or isn't scoped to your credentialDon't retry. (We return 404, not 403, on cross-partner lookups to prevent id enumeration.)
409State conflict (e.g. cancelling an already-cancelled session; idempotency race)Don't retry; the state is already what you wanted, or re-fetch with GET.
429Rate limitedRead Retry-After-short (seconds), back off with jitter, retry.
500Server error on our sideRetry with exponential backoff; open a ticket if persistent.
502 / 503 / 504Transient upstream / networkRetry with backoff.

5xx response bodies are scrubbed to a generic message. Always log type, code, and the request_id (never client_secret values or PII). The full reference and common error patterns live in the Errors section.

The envelope and type enum are identical for 0Pools, which adds two product-specific statuses on its money paths: 402 (insufficient balance at transact) and 501 (off_ramp is not yet available). 0Pools 403s carry a denial code you should branch on — pools_not_enabled, pool_access_suspended, kyc_not_approved, pool_not_allowed, or key_mode_mismatch. (0Pools does not use 425.)

Rate limits

Limits are per client IP, not per key

The throttler tracks callers by client IP, not by API key. Two keys behind the same egress IP share a bucket; one key across many IPs gets a bucket per IP. Plan capacity around your server's outbound IP, not your key count. The exact per-route limits are versioned and can change — treat the values below as current guidance and read the live headers to be precise.

Each route carries its own bucket. Indicative limits (per IP):

Method + pathLimitWindowAuth
POST /v1/gate_sessions1060ssk_*
POST /v1/gate_sessions/:id/cancel3060ssk_*
POST /v1/embed/bootstrap3060spk_*
POST /v1/quotes/preview3060ssk_*
GET /v1/gate_sessions, GET /v1/transactions, GET /v1/capabilities/*6060ssk_*

POST /v1/gate_sessions is the tightest tier (10/60s) because it's the money-moving precursor; reads and capability lookups sit at 60/60s. Routes without an explicit limit fall back to global tiers (short 10/10s, medium 60/60s, long 300/1h), all applied simultaneously. Health probes (/health) are never throttled.

A 429 returns the standard error envelope and these headers (note the -short tier suffix — this is the tier name, not a typo):

HTTP/1.1 429 Too Many Requests
Retry-After-short: 12
X-RateLimit-Limit-short: 10
X-RateLimit-Remaining-short: 0
X-RateLimit-Reset-short: 12
X-Request-Id: a1b2c3d4-e5f6-7890-abcd-ef0123456789

Read Retry-After-short, not Retry-After

The retry hint is Retry-After-short (seconds); the bare Retry-After is not set, so a client that auto-reads the standard header will not find one. The X-RateLimit-*-short headers are emitted on every response, so you can back off pre-emptively when X-RateLimit-Remaining-short hits 0.

When you retry after a 429, add jitter — without it, every throttled client retries at the same instant when the hint expires and re-hits the limit. Note that idempotency replays still tick the bucket (the request reaches the throttler before the cached response resolves); inbound webhook callbacks from 0Bit do not count against your limits.

async function postRespectingLimits(url, init) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(url, init);
    if (res.status !== 429) return res;
    // Note the `-short` suffix — the bare `Retry-After` is not set.
    const retryAfter = parseInt(res.headers.get('retry-after-short') ?? '5', 10);
    const jitter = Math.random() * 1000;
    await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitter));
  }
  throw new Error('Rate limit retries exhausted');
}

Pagination

Partner list endpoints (e.g. GET /v1/gate_sessions, GET /v1/transactions, the GET /v1/capabilities/* discovery lists) use cursor-style pagination. Pass ?limit=N (default 10, max 100) and walk forward with starting_after, set to the id of the last item in the previous page.

{
  "object": "list",
  "data": [
    { "id": "67a1f3b9e4b0c10001234567", "object": "gate_session" }
  ],
  "has_more": false,
  "url": "/v1/gate_sessions"
}

Iterate until has_more is false:

async function listAll(path) {
  const items = [];
  let startingAfter;
  do {
    const url = new URL(`https://gate-api.0bit.app${path}`);
    url.searchParams.set('limit', '100');
    if (startingAfter) url.searchParams.set('starting_after', startingAfter);
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.OBIT_SECRET_KEY}` },
    });
    const page = await res.json();
    items.push(...page.data);
    startingAfter = page.has_more ? page.data.at(-1).id : undefined;
  } while (startingAfter);
  return items;
}

0Pools trade lists use a cursor envelope

The object: "list" / starting_after / has_more shape above is the 0Gate convention. The 0Pools trade list (GET /v1/pools/trades) returns { data, nextCursor, hasMore }: pass ?cursor=<last transactId> to page forward, with limit between 1 and 100 (default 50), and stop when nextCursor is null. See List 0Pools trades.

How it fits together

For 0Gate, the production integration is a session-based handshake rather than a single stateless call:

1. Create a session (server)

Your server calls POST /v1/gate_sessions with your sk_* and an Idempotency-Key, binding the amount, currency, and return_url. The response includes a client_secret (gsec_...) shown only once — store it.

2. Bootstrap the widget (browser)

The browser hands the client_secret to the widget, which calls POST /v1/embed/bootstrap with your pk_*. The origin must be in your allowlist. This returns the short-lived embed token (sent thereafter as X-Embed-Token).

3. The user transacts (hosted iframe)

The user completes buy / sell / swap inside the hosted iframe (KYC, payment, wallet). The browser onSuccess callback is for UX only.

4. Confirm by webhook (server)

0Bit POSTs HMAC-signed webhooks through the session lifecycle (gate_session.createdprocessingcompleted / failed / expired / cancelled). Webhooks are the source of truth for settlement — verify the Gate-Signature header, dedupe on the event id, and fulfill on gate_session.completed.

Don't over-promise capabilities

Supported assets, countries, payment methods, fees, spreads, limits, and TTLs vary by deployment and entitlement. Don't hard-code them: read the live GET /v1/capabilities/* endpoints, check the Partner Hub, or contact support@0bit.io. Advanced surfaces — Swap and the Rails / High-tier quote-and-settle endpoints — are entitlement- or contract-gated, not blanket-GA.

Next steps

On this page