Sample projects
Runnable 0Gate-shaped server, browser, React, and webhook examples based on the audited SDK and OpenAPI contract.
These samples show the production shape of a 0Gate integration without exposing internal systems or unsupported product claims. They use fake ids, fake keys, and partner-owned URLs.
Run these against your own sandbox account
The SDK unit tests pass locally, but these app examples still need your sandbox keys, allowed origins, webhook URL, and partner configuration before they can run end-to-end.
Architecture
The browser callback is not settlement truth. Treat it as a UI signal and keep the order in a pending state until the signed webhook is processed.
Environment
OBIT_GATE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
OBIT_GATE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
OBIT_GATE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
OBIT_GATE_API_ORIGIN=https://gate-api-sandbox.0bit.app
PUBLIC_BASE_URL=https://partner.exampleThe SDK API value is the origin only. Raw HTTP calls use the OpenAPI base URL https://gate-api-sandbox.0bit.app/v1.
Express server
import express from 'express';
import { GateClient, WebhookSignatureError } from '@0bit/gate';
const app = express();
const gate = new GateClient({
apiKey: process.env.OBIT_GATE_SECRET_KEY!,
baseUrl: process.env.OBIT_GATE_API_ORIGIN ?? 'https://gate-api-sandbox.0bit.app',
});
const orders = new Map<
string,
{
status: 'draft' | 'pending' | 'paid' | 'cancelled' | 'expired';
gateSessionId?: string;
lastEventId?: string;
}
>();
app.post('/api/0gate/session', express.json(), async (req, res) => {
const orderId = String(req.body.orderId);
const order = orders.get(orderId) ?? { status: 'draft' as const };
const session = await gate.sessions.create(
{
amount: '100.00',
currency: 'EUR',
return_url: `${process.env.PUBLIC_BASE_URL}/checkout/${orderId}/complete`,
cancel_url: `${process.env.PUBLIC_BASE_URL}/checkout/${orderId}/cancel`,
metadata: { order_id: orderId },
},
{ idempotencyKey: `order:${orderId}:gate-session` },
);
orders.set(orderId, {
...order,
status: 'pending',
gateSessionId: session.id,
});
res.json({
clientSecret: session.client_secret,
publishableKey: process.env.OBIT_GATE_PUBLISHABLE_KEY,
});
});
app.post('/webhooks/0gate', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = gate.webhooks.constructEvent(
req.body,
req.headers['gate-signature'],
process.env.OBIT_GATE_WEBHOOK_SECRET!,
);
} catch (error) {
if (error instanceof WebhookSignatureError) return res.sendStatus(400);
return res.sendStatus(500);
}
const orderId = String(event.data?.metadata?.order_id ?? '');
const order = orders.get(orderId);
if (!order || order.lastEventId === event.id) {
return res.sendStatus(200);
}
if (event.type === 'gate_session.completed') {
orders.set(orderId, { ...order, status: 'paid', lastEventId: event.id });
}
if (event.type === 'gate_session.cancelled') {
orders.set(orderId, { ...order, status: 'cancelled', lastEventId: event.id });
}
if (event.type === 'gate_session.expired') {
orders.set(orderId, { ...order, status: 'expired', lastEventId: event.id });
}
res.sendStatus(200);
});
app.listen(4242, () => {
console.log('0Gate sample listening on http://localhost:4242');
});For a real app, replace the in-memory map with your database. Persist the order id, session id, idempotency key, event id, event type, and timestamps.
Browser mount
import { GateRamp } from '@0bit/gate/browser';
async function startCheckout(orderId: string) {
const response = await fetch('/api/0gate/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId }),
});
const { clientSecret, publishableKey } = await response.json();
const ramp = new GateRamp({
publishableKey,
clientSecret,
environment: 'sandbox',
});
await ramp.mount('#gate-container', {
onSuccess: ({ sessionId }) => {
window.location.href = `/checkout/${orderId}/pending?session=${encodeURIComponent(sessionId)}`;
},
onClose: () => {
window.location.href = `/checkout/${orderId}`;
},
});
}React mount
import { useEffect, useState } from 'react';
import { RampCheckout } from '@0bit/gate/react';
export function Checkout({ orderId }: { orderId: string }) {
const [session, setSession] = useState<{
clientSecret: string;
publishableKey: string;
} | null>(null);
useEffect(() => {
fetch('/api/0gate/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId }),
})
.then((response) => response.json())
.then(setSession);
}, [orderId]);
if (!session) return null;
return (
<RampCheckout
publishableKey={session.publishableKey}
clientSecret={session.clientSecret}
environment="sandbox"
onSuccess={({ sessionId }) => {
window.location.href = `/checkout/${orderId}/pending?session=${encodeURIComponent(sessionId)}`;
}}
/>
);
}Production notes
| Area | Requirement |
|---|---|
| Keys | Store sk_* and whsec_* in server-side secret storage only. |
| Origins | Configure exact embed origins and return URLs before using the widget. |
| Webhooks | Use a raw-body parser, verify Gate-Signature, dedupe on event.id, and return 2xx only after durable handling. |
| Idempotency | Use one stable idempotency key per logical order/session creation attempt. |
| Support | Log request ids, session ids, event ids, order ids, environment, and timestamps. Do not log secrets or raw PII payloads. |
What is not proven by this sample
- It does not prove live regional availability or KYC/KYB eligibility.
- It does not prove 0Pools, 0Base, or 0Link public access.
- It does not replace legal, compliance, or product approval.
- It does not guarantee npm/PyPI registry publishing status.
For production partner help, contact [email protected] with environment, session id, request id, event id, and the smallest support-safe reproduction.