Tools
The Partner Hub dashboard, sandbox + test cards, OpenAPI codegen, HTTP/Postman, and webhook inspection for building on 0Bit.
This page covers the tooling 0Bit ships for building, testing, and operating an integration: the Partner Hub dashboard for keys, webhooks, branding, and logs; the sandbox environment and test cards; the OpenAPI spec and generating clients from it; raw HTTP / Postman usage; and inspecting webhook deliveries.
These tools are oriented around 0Gate — the embeddable buy / sell / swap product that is live in both sandbox and production. Buy and Sell are generally available; Swap and the High-tier Rails API are advanced, entitlement-gated capabilities. 0Pools (LP program + partner liquidity API) and 0Base are forthcoming; 0Link is an internal routing layer beneath the products, not yet a public partner product. So the workflows below assume a 0Gate integration.
One login, every product
Partner Hub uses an organization-first model. A single login covers every product your org is entitled to — products you are not entitled to are hidden. Your pk_/sk_ keys are platform credentials issued once per org/mode, not per product. See the account model for the full picture.
Partner Hub (dashboard)
The Partner Hub at portal.0bit.app is your management surface for everything outside of raw API calls. If you do not yet have access, email [email protected] for account, key, and entitlement review.
What the Hub manages:
| Area | What you do here |
|---|---|
| API keys | Mint, view, and revoke publishable (pk_) and secret (sk_) keys per mode. |
| Webhook endpoints | Register your webhook_url per mode, view the signing secret, and inspect delivery attempts. |
| Branding | Pick the widget theme (light / dark). Deeper visual customization (colors, fonts) ships as a per-partner theme — contact support. |
| Logs & usage | View request volume, transaction receipts, and webhook delivery history. |
| Products / entitlements | See which products your org is enabled for (operator- or contract-managed). |
Managing API keys
0Bit issues three credential types. Two are managed in the Hub; the third is minted at runtime.
| Credential | Prefix / shape | Where it lives | Authorizes |
|---|---|---|---|
| Publishable key | pk_test_ / pk_live_ | Browser / client bundle (safe to expose) | Only POST /v1/embed/bootstrap. Cannot create sessions or move money. |
| Secret key | sk_test_ / sk_live_ | Your server only — never the browser | Everything: create / retrieve / cancel sessions, quotes, rails. |
| Embed token | short-lived JWT (~1h) | Inside the iframe | X-Embed-Token on iframe→backend calls. Returned by POST /v1/embed/bootstrap. |
A key encodes its mode in the prefix, and the mode must match the host it hits. Both pk_ and sk_ are shown once on creation — store the secret key in a secret manager immediately.
Never ship a secret key to the browser
A leaked sk_ can create sessions and move funds. The most common leak path is SSR (getServerSideProps and similar). The browser only ever needs the publishable key plus a session's client_secret. Use the neutral env-var convention in your code: OBIT_SECRET_KEY (server), OBIT_PUBLISHABLE_KEY (client), OBIT_WEBHOOK_SECRET (webhook verification).
Rotation. Mint a new key of the same type + mode, update your environment / source, then revoke the old one after traffic has moved. There is no forced grace period — the old key works until you explicitly revoke it. Keys are org-wide, so rotating affects every entitled product using that key. Rotate secret keys roughly quarterly; rotate publishable keys mainly after a security event.
Configuring webhook endpoints
In the Hub you register a webhook_url for each mode and read its signing secret (HMAC-SHA256). 0Bit POSTs signed JSON events to that URL as a session moves through its lifecycle, with the signature in the Gate-Signature header. Each mode has its own secret; verify every event before trusting it. See inspecting webhook deliveries below.
Webhooks are the source of truth for settlement
The browser onSuccess callback (and a hosted-redirect return_url arrival) means "the user finished the flow," not "money has settled." Always treat the signed webhook — or a server-side GET /v1/gate_sessions/{id} — as authoritative, and make every settlement handler idempotent.
Embedding the Hub in your own admin UI
If you want your team to see keys, usage, and webhook logs without leaving your product, you can iframe the Gate Portal via dashboard-embed SSO: your server mints a short-lived (≈5-min) embed token with POST /v1/portal/dashboard_embed_tokens, then renders portal.0bit.app/<route>?embed=true&embed_token=.... Allowed origins are configured per partner. Treat the embed token like a password for its short life and strip it from access logs.
Sandbox environment
Everything starts in sandbox. Test keys hit the sandbox host, run on isolated test data, and move no real money.
| Environment | API base URL | Widget iframe | Keys accepted |
|---|---|---|---|
| Sandbox / test | https://gate-api-sandbox.0bit.app/v1 | https://gate-sandbox.0bit.app | *_test_ only |
| Production / live | https://gate-api.0bit.app/v1 | https://gate.0bit.app | *_live_ only |
In test mode, KYC is skipped, payments use test cards, and webhooks fire to your test endpoint. The mode check runs before key lookup, so a wrong-mode key fails fast with a stable code rather than a generic auth error:
# A test key against the LIVE host -> 403 test_key_on_live
curl -sS -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer sk_test_..." \
https://gate-api.0bit.app/v1/capabilities/countriesThe reverse (*_live_ on the sandbox host) returns 403 with code live_key_on_sandbox.
Test cards
Use the canonical sandbox card to complete a flow end to end:
| Field | Value |
|---|---|
| Card number | 4242 4242 4242 4242 |
| Expiry / CVC | Any future date / any 3 digits |
| 3DS | Completes automatically in sandbox |
Drive session state directly in test mode
Rather than clicking through the widget every time, sandbox exposes test helpers to move a session to a terminal state and fire the corresponding webhook: POST /v1/test_helpers/sessions/{id}/complete and POST /v1/test_helpers/sessions/{id}/fail. These reject sk_live_ keys with 403 live_keys_not_allowed, so they can never run against production.
Going to production
Promoting an integration requires no code changes beyond credentials and configuration:
- Obtain
pk_live_andsk_live_keys in Partner Hub. - Register a separate live
webhook_url(with its own live signing secret). - Swap the host from
gate-api-sandbox.0bit.apptogate-api.0bit.app. - Swap
*_test_keys for*_live_keys.
Same routes, same request/response shapes.
The OpenAPI spec
The OpenAPI spec is the source of truth for the entire API surface — every endpoint, parameter, and response shape. Official server SDKs are shipped for Node (@0bit/gate) and Python (0bit-gate, import from zerobit.gate import GateClient), plus browser (@0bit/gate/browser) and React (@0bit/gate/react) packages. For any other language — PHP, Ruby, Go, Java, .NET — there is no published package yet; generate a typed client from the spec instead of running composer require / gem install against a package that does not exist.
Get the spec from the Partner Hub (it ships the current gate-v1.yaml for your entitled products), then run codegen against the downloaded file. The spec is distributed through the Hub rather than a guaranteed public raw path.
Generating a client
The recommended tool is openapi-generator, which supports 50+ targets (typescript-fetch, python, 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.0bit.app/v1',
accessToken: process.env.OBIT_SECRET_KEY,
}));
const session = await api.createSession({
createSessionRequest: { amount: '100', currency: 'EUR', return_url: 'https://your.app/done' },
});npx @openapitools/openapi-generator-cli generate \
-i gate-v1.yaml \
-g go \
-o ./0bit-client \
--additional-properties=packageName=obitnpx @openapitools/openapi-generator-cli generate \
-i gate-v1.yaml \
-g php \
-o ./0bit-client \
--additional-properties=invokerPackage=Obit\\ClientMinimal future churn
When official SDKs ship for more languages, their method names will mirror the spec's operationId values, so a generated client is the closest match to the future SDK. Until then: generate the HTTP client from the spec, hand-write your webhook verifier (the algorithm is stable), and keep your own Idempotency-Key retry logic. The biggest delta when SDKs land is ergonomics, not behaviour.
There is no 0Bit CLI to scaffold projects. "Scaffolding" means the SDKs above plus OpenAPI codegen — that is the supported path.
HTTP and Postman
Every endpoint is a standard HTTPS REST call. Authenticate with Authorization: Bearer <key> and send Idempotency-Key on every mutating POST (replayed safely for 24 hours, keyed on the request body).
A quick auth + quote smoke test with cURL:
export GATE_API_BASE=https://gate-api-sandbox.0bit.app/v1
export OBIT_SECRET_KEY=sk_test_...
# Confirm auth works
curl -sS -H "Authorization: Bearer $OBIT_SECRET_KEY" \
"$GATE_API_BASE/capabilities/countries" | jq .
# Preview quotes (all payment methods, fee breakdown) in one call
curl -sS -X POST "$GATE_API_BASE/quotes/preview" \
-H "Authorization: Bearer $OBIT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"currency":"EUR","asset":"USDC","amount":"100","side":"on_ramp","country_code":"DE"}' | jq .
# Create a session (Idempotency-Key required)
curl -sS -X POST "$GATE_API_BASE/gate_sessions" \
-H "Authorization: Bearer $OBIT_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"amount":"100","currency":"EUR","return_url":"https://your.app/done"}' | jq .const base = 'https://gate-api-sandbox.0bit.app/v1';
const headers = {
Authorization: `Bearer ${process.env.OBIT_SECRET_KEY}`,
'Content-Type': 'application/json',
};
// Sanity check
const countries = await fetch(`${base}/capabilities/countries`, { headers });
console.log(countries.status, await countries.json());
// Create a session
const session = await fetch(`${base}/gate_sessions`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({ amount: '100', currency: 'EUR', return_url: 'https://your.app/done' }),
});
const data = await session.json();
console.log(data.client_secret); // gsec_<sessionId>_<random> — browser-safeimport os, uuid, requests
BASE = "https://gate-api-sandbox.0bit.app/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['OBIT_SECRET_KEY']}"}
# Sanity check
print(requests.get(f"{BASE}/capabilities/countries", headers=HEADERS).json())
# Create a session
resp = requests.post(
f"{BASE}/gate_sessions",
headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
json={"amount": "100", "currency": "EUR", "return_url": "https://your.app/done"},
)
print(resp.json()["client_secret"]) # gsec_<sessionId>_<random>Postman collection
A ready-made Postman collection is generated from the OpenAPI spec, covering the full v1 REST surface (sessions, capabilities, quotes, transactions, embed). Import these files from the monorepo:
| File | Purpose |
|---|---|
0Bit_apps/Gate/Gate_Widget/postman/gate.postman_collection.json | Full v1 REST surface |
0Bit_apps/Gate/Gate_Widget/postman/gate-sandbox.postman_environment.json | Sandbox baseUrl + variable placeholders |
After importing:
- Select the Gate Sandbox environment.
- Set
secretKey(sk_test_) andpublishableKey(pk_test_). - Run Capabilities → List countries to confirm auth.
- Run Quotes → Preview quotes, then Sessions → Create a gate session (optionally pass the
quote_preview_idfrom the preview response to bind the fees you displayed).
Webhooks are server -> partner deliveries
Webhooks appear in the OpenAPI spec for documentation, but they are not callable Postman requests — they are events 0Bit POSTs to your endpoint, not requests you send to us.
Error model when debugging
Errors return a JSON envelope. Branch on statusCode; some errors also carry a stable string code. The message is for human display and requestId (echoed from the X-Request-Id response header) is what to quote to support.
{
"statusCode": 403,
"message": "Origin \"https://attacker.example\" is not in allowed_domains for this partner",
"error": "Forbidden",
"requestId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789"
}A 429 includes a Retry-After value (read the rate-limit headers and back off with jitter). When you open a support ticket, include the full error response with credentials redacted, the requestId, a timestamp, and your partner_id.
Inspecting webhook deliveries
Webhooks are the authoritative settlement signal, so being able to see what 0Bit sent — and replay it — is central to debugging.
Verify before you trust
The signature lives in the Gate-Signature: t=<unix>,v1=<hex> header (Stripe-compatible). Recompute the HMAC-SHA256 of <t>.<rawBody> with your mode's signing secret and compare with a timing-safe equal. Always pass the raw request body — re-serializing the JSON in your framework will break verification.
const crypto = require('crypto');
function verifyGateSignature(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!ok) throw new Error('Invalid signature');
// optional: reject if Math.abs(Date.now()/1000 - parts.t) > 300 (5-min skew)
return JSON.parse(rawBody);
}import hmac, hashlib
def verify_gate_signature(raw_body: bytes, header: str, secret: str) -> bytes:
parts = dict(kv.split("=", 1) for kv in header.split(","))
expected = hmac.new(
secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(parts["v1"], expected):
raise ValueError("Invalid signature")
# optional: reject if abs(time.time() - int(parts["t"])) > 300
return raw_bodyThe official SDKs wrap this as a webhooks.constructEvent(rawBody, sig, secret) one-liner.
Common webhook failures
Replaying webhooks locally
To receive deliveries against a machine without a public URL, expose a tunnel (ngrok / Cloudflare Tunnel) and point your Hub webhook endpoint at the tunnel URL. Then drive a sandbox session to completion — either by walking the widget with the test card, or by calling POST /v1/test_helpers/sessions/{id}/complete — and watch the signed event land. The Hub's webhook log shows each delivery attempt and its response, so you can confirm whether a missed event was never sent or your endpoint rejected it.
Reference
- Hosts. API:
https://gate-api.0bit.app/v1(live),https://gate-api-sandbox.0bit.app/v1(sandbox). Widget:https://gate.0bit.app(live),https://gate-sandbox.0bit.app(sandbox). Dashboard: portal.0bit.app. - Test card.
4242 4242 4242 4242, any future expiry, any CVC. - Test helpers.
POST /v1/test_helpers/sessions/{id}/complete·POST /v1/test_helpers/sessions/{id}/fail(sandbox only). - OpenAPI spec.
gate-v1.yaml— download it from the Partner Hub, then generate a client via OpenAPI codegen. - Support. [email protected]