Developers
Core Concepts

Environments & modes

How 0Gate separates test (sandbox) and live (production) — encoded in your key prefix, bound to dedicated API and widget hosts, with isolated data, webhook secrets, and sessions.

0Gate runs in two modes: test (sandbox) and live (production). The mode is not a flag you toggle at request time — it is baked into the prefix of the API key you authenticate with (pk_test_… / sk_test_… vs pk_live_… / sk_live_…) and reinforced by the host you send the request to. A test key talks to the sandbox; a live key talks to production. There is no third "staging" — localhost is simply the live/sandbox software running on your own machine during development.

The point of this split is simple: test exhaustively without moving real money, then flip to live by swapping one credential. Both modes run the same API surface, the same GateSession lifecycle, and the same webhook contract — so anything you build against sandbox behaves identically in production.

Test vs live

ModeKey prefixesWhat it does
test (sandbox)pk_test_…, sk_test_…Simulated end-to-end. Test cards and test bank flows, no real funds, no real KYC, no on-chain settlement. A sandbox test helper can force-complete a session to fire gate_session.completed on demand.
live (production)pk_live_…, sk_live_…The real ramp. Real fiat rails, real KYC and sanctions screening, real chargeback handling, and real on-chain settlement. Every payment moves money.

The mode is read from the key, server-side. You never pass a mode parameter — using a sk_test_ key is test mode, and the GateSession it creates carries mode: "test". To go live, you replace the test key with the matching live key and point at the live hosts. Nothing else in your code changes.

Mode is decided by the key prefix

Both key types carry the mode in the prefix itself: sk_<test|live>_<…> and pk_<test|live>_<…>. The backend reads it off the credential before it touches your request body — there is no mode field on POST /v1/gate_sessions, and you cannot ask a test key to create a live session (or vice versa). The mode then propagates: the session it mints carries mode: "test" or mode: "live", and the embed JWT issued at bootstrap stamps the same mode claim onto every downstream iframe call.

What sandbox simulates

In test mode the same non-custodial machinery runs, but its external effects are simulated rather than real:

  • No real funds move on any fiat rail.
  • No real KYC — identity verification and AML/sanctions screening are simulated, so you never need a real document or a verified wallet to exercise the flow.
  • No on-chain settlement — crypto delivery is simulated, so gate_session.completed fires without a real chain transaction.
  • Test helpers let you force-complete a session on demand instead of waiting for a real settlement (see below).

Forcing terminal states in sandbox

Sandbox exposes two helper endpoints (controller prefix v1/test_helpers) so you can drive your integration without performing a real payment. These exist only in test mode and are not part of the OpenAPI spec. They are guarded by PartnerSecretKeyGuard, so call them server-side with your sk_test_ key — a live key is rejected with a 403:

EndpointEffect
POST /v1/test_helpers/sessions/{id}/completeMarks the session completed and fires gate_session.completed.
POST /v1/test_helpers/sessions/{id}/failMarks the underlying intent failed; the parent session stays open and no webhook is emitted. Useful for exercising your in-widget error UX, not a webhook branch.

The complete helper is how you exercise the gate_session.completed webhook branch in CI before a single real payment exists.

Hosts & base URLs

Each mode has its own API base URL and its own widget iframe origin. Use the host that matches your key. These are the servers declared in the OpenAPI contract (0gate-v1.yaml, version 2026-05-25):

Surfacetest (sandbox)live (production)local (development)
API basehttps://gate-api-sandbox.0bit.app/v1https://gate-api.0bit.app/v1http://localhost:4000/v1
Widget iframehttps://gate-sandbox.0bit.apphttps://gate.0bit.apphttp://localhost:3000
Partner Hubhttps://portal.0bit.apphttps://portal.0bit.app

The Partner Hub is a single console for both modes — you switch between your test and live keys, webhook endpoints, and analytics inside it.

The OpenAPI base URLs include /v1, while the SDK constructors take the API origin only. If you override baseUrl / base_url in the SDK, use https://gate-api-sandbox.0bit.app or https://gate-api.0bit.app, not the /v1 URL.

SDK base URL shape

The audited Node and Python SDKs default to the production API origin. Resource methods append /v1/... internally. Override the origin when testing against sandbox or local development.

SDKConstantDefault value
@0bit/gateDEFAULT_BASE_URL (src/node/client.ts)https://gate-api.0bit.app
0bit-gate (Python)_DEFAULT_BASE_URL (python/zerobit/gate/client.py)https://gate-api.0bit.app

There is no auto-detection from the key prefix. A sk_test_ key should use the sandbox origin; a sk_live_ key should use the production origin.

Overriding baseUrl on the server SDKs

Set the option to the origin for the mode you're using: https://gate-api.0bit.app for live or https://gate-api-sandbox.0bit.app for sandbox. The SDKs strip any trailing slash for you (.replace(/\/+$/, '')).

Pointing the Node SDK at sandbox
import { GateClient } from '@0bit/gate';

const gate = new GateClient({
  apiKey: process.env.GATE_KEY,          // sk_test_…
  baseUrl: 'https://gate-api-sandbox.0bit.app',
  // timeoutMs?: number  — default 30_000 (30s)
  // maxRetries?: number — default 2 (up to 3 total attempts; 0 disables)
});
Pointing the Python SDK at sandbox
from zerobit.gate import GateClient

gate = GateClient(
    api_key=os.environ["GATE_KEY"],        # sk_test_…
    base_url="https://gate-api-sandbox.0bit.app",
    # timeout_seconds=30.0, max_retries=2  — same defaults as Node
)

The example env var GATE_KEY is a convention used in the SDK examples only — the SDKs never read environment variables themselves. The API key and base URL are always passed explicitly to the constructor.

Browser SDK: environment, apiBaseUrl, iframeUrl

The browser SDK (@0bit/gate/browser, class GateRamp) resolves hosts differently from the server SDKs. Instead of one baseUrl, it takes an environment label and looks up a pair of hosts — the API origin and the iframe origin — from a built-in table (src/browser/environments.ts):

environmentapiBaseUrliframeUrl
'production' (default)https://gate-api.0bit.apphttps://gate.0bit.app
'sandbox'https://gate-api-sandbox.0bit.apphttps://gate-sandbox.0bit.app
'development'http://localhost:3000http://localhost:3001

Override them with explicit apiBaseUrl and iframeUrl options only for local development or a reviewed environment override:

Browser SDK pointed at the real sandbox hosts
import { GateRamp } from '@0bit/gate/browser';

const ramp = new GateRamp({
  publishableKey: 'pk_test_…',
  clientSecret: session.client_secret,   // recommended (session-bound)
  apiBaseUrl: 'https://gate-api-sandbox.0bit.app',  // bare origin — see note
  iframeUrl: 'https://gate-sandbox.0bit.app',
});

await ramp.mount('#ramp', {
  onSuccess: ({ txId, sessionId }) => { /* … */ },
  onError:   ({ message }) => { /* … */ },
  onClose:   () => { /* … */ },
});

The browser apiBaseUrl is a bare origin, not a /v1 URL. The browser SDK builds its only API call as `${apiBaseUrl}/v1/embed/bootstrap`, so it appends /v1 itself — pass https://gate-api-sandbox.0bit.app, not …/v1. This is the opposite of the server SDKs, whose baseUrl includes /v1. The iframeUrl is always a bare origin (it is parsed with new URL(...).origin for the postMessage targetOrigin check).

Hosts feed the postMessage origin check

The resolved iframeUrl is not cosmetic — GateRamp derives iframeOrigin = originOf(iframeUrl) and rejects any message event whose event.origin doesn't match (and whose event.source isn't the iframe it created). A wrong iframeUrl therefore doesn't just load the wrong widget; it silently drops every WIDGET_READY / PAYMENT_* message, so mount() rejects after its 15s WIDGET_READY timeout. Hosted-redirect mode (GateRamp.redirectToCheckout) uses the same resolution to build the redirect URL.

Key–mode binding

The mode lives in the credential, and the credential is enforced at three layers.

One prefix, one mode, one host

A sk_test_ / pk_test_ key only authenticates against the sandbox host; a sk_live_ / pk_live_ key only against production. The prefix and the host enforce this on both ends — there is no request-time switch, no shared "staging" key, and no way to mix a test key with the live host. If a request's key prefix and target host disagree on the mode, it fails.

Publishable vs secret — by mode

Each mode issues both kinds of key, and the kind dictates where it may be used and what it may do — independently of the mode:

KeyPrefix (per mode)Where it livesWire usage
Secretsk_test_… / sk_live_…Server only — never in browser/mobileAuthorization: Bearer <key> (or X-Secret-Key); never accepted via query string. Creates/retrieves/cancels sessions.
Publishablepk_test_… / pk_live_…Browser-safeAuthorization: Bearer <key> (or X-Publishable-Key; pk_ may also go in a query string). Bootstrap onlyPOST /v1/embed/bootstrap.

The browser SDK enforces this client-side too: GateRamp throws SecretKeyInBrowserError if you hand it a key matching ^sk_, and ConfigError if the key doesn't match ^pk_(test|live)_.

The embed token inherits the mode

When the browser exchanges a pk_ (plus an optional clientSecret) at POST /v1/embed/bootstrap, it gets back a short-lived HS256 JWT (default ~1h TTL) sent as X-Embed-Token on subsequent iframe calls. That JWT carries a mode claim copied straight from the publishable key, so the mode flows all the way through the iframe runtime — a test bootstrap can never produce a live-mode embed token.

Never put a secret key (sk_test_ or sk_live_) in browser, mobile, or any client-side code. Secret keys create and manage sessions server-side; the browser only ever sees a publishable key (pk_…, used solely for POST /v1/embed/bootstrap) and the one-time client_secret. This rule applies in both modes — a leaked sk_test_ key is still a credential leak.

What's shared vs separate across modes

This is the most important mental model: the two modes share your identity but share nothing else.

How a request resolves to a mode

The client_secret (gsec_<id>_<rand>) minted in one mode only works against that mode's widget host — a sandbox session cannot be opened in the production widget, and vice versa.

What stays identical across modes

Because both modes run the same software, the things you actually integrate against do not change between test and live:

  • The endpointsPOST /v1/gate_sessions, GET /v1/gate_sessions/{id}, POST /v1/gate_sessions/{id}/cancel, POST /v1/embed/bootstrap.
  • The GateSession shape and its lifecycle statuses (opencompleted / expired / cancelled; failure is modeled at the intent level / surfaced via webhooks, not as a session status).
  • The webhook verification schemeGate-Signature over the raw request body. Event names and payload fields should match the current webhook reference for your contract.
  • The non-custodial model — 0Bit runs KYC, AML/sanctions screening, chargebacks, fiat rails, and on-chain settlement in both modes — sandbox simulates these, live performs them for real, but you never rebuild the ramp UI or the compliance stack either way.

This is what makes the test → live cutover a one-line credential swap rather than a re-integration.

Going from sandbox to live

Because the surface is identical, the cutover is a small, mechanical set of swaps — not a re-integration.

The cutover checklist

  1. Swap the server key. Replace sk_test_… with sk_live_… in your server environment.
  2. Repoint the server SDK origin. Change baseUrl / base_url from https://gate-api-sandbox.0bit.app to https://gate-api.0bit.app.
  3. Swap the publishable key. Replace pk_test_… with pk_live_… wherever the browser SDK is constructed.
  4. Repoint the browser SDK. Set environment: 'production', or explicitly set apiBaseUrl to https://gate-api.0bit.app and iframeUrl to https://gate.0bit.app for a reviewed override.
  5. Swap the webhook secret. Verify live deliveries with your live whsec_…, not the test one.
  6. Allowlist your live domain. Make sure the page that embeds the widget is in your partner allowed_domains (exact match — no wildcards), or POST /v1/embed/bootstrap will reject the live Origin.

Before you flip: drive the completed webhook path in sandbox

Use the sandbox complete test helper (POST /v1/test_helpers/sessions/{id}/complete, called server-side with your sk_test_ key) to force a completion and confirm your webhook handler correctly verifies the Gate-Signature and reacts to gate_session.completed. The fail helper marks the test intent failed while the session stays open and emits no webhook — use it to exercise your in-widget error UX, not a webhook branch. Settlement truth is the webhook, not the browser PAYMENT_SUCCESS postMessage — so this is the path that must be solid before live money moves.

Don't forget the live-only events. Some events you will only ever observe in live: real kyc.required (sandbox KYC is simulated) and the partner.quota.warning / partner.quota.exhausted quota signals. The browser SDK surfaces quota exhaustion to your UI via the onUnavailable mount callback (driven by the bootstrap response's available: false / unavailable_reason), so handle that state before launch.

There is nothing else to change. The endpoints, the GateSession object, the idempotency contract (Idempotency-Key on writes), and the postMessage protocol are byte-for-byte the same between modes. If it worked in sandbox, it works in live.

See also

On this page