0Pools webhooks
Receive signed 0Pools terminal settlement events - settled, failed, and released - for approved partners.
0Pools API pages are for approved headless partners. They cover the partner-visible quote, transact, status, trade, and balance lifecycle only.
0Pools emits three terminal webhook events so your backend can close a trade from verified server-side state instead of relying on poll timing or browser callbacks. Every delivery is signed; verify the signature over the raw body before you parse or trust the payload.
Events
0Pools delivers exactly these three terminal events. Each one corresponds to a final trade status and is sent once the trade reaches that state.
| Event type | Trade status | Meaning | Recommended action |
|---|---|---|---|
pool.transaction.settled | settled | The trade completed and delivery was performed. | Close your own record once and notify the customer. |
pool.transaction.failed | failed | The trade did not complete. | Close or route to support per your product policy. |
pool.transaction.released | released | A reserved trade was released without settling. | Close the attempt and free any local hold. |
Some states are poll-only, never webhooks
A settled trade can later flip to returned (for example, a payout returned by the receiving bank, or a downstream delivery that could not be completed) with an automatic refund to your partner balance. There is no returned webhook. Quote rejection (a quote moving to rejected) is also not delivered as a webhook. Discover both by polling the transaction status endpoint and reconciling from your trade reads.
Envelope
All three events share one JSON envelope. Money and rate values are decimal strings; basis-point fields are integers; timestamps are ISO 8601 UTC. The envelope deliberately omits any settlement provider, rail, or external reference - it carries only partner-visible trade facts.
| Field | Type | Description |
|---|---|---|
eventId | string | Stable dedupe key. Equals sha256(txnId:eventType). Treat as the idempotency key. |
type | string | One of pool.transaction.settled, pool.transaction.failed, pool.transaction.released. |
txnId | string | The trade (transaction) id this event is about. |
partnerId | string | The partner the trade belongs to. |
poolId | string | The pool the trade executed against. |
side | string | on_ramp or off_ramp. |
status | string | Terminal trade status: settled, failed, or released. Matches type. |
fiatAmount | decimal string | Fiat amount for the trade. |
cryptoAmount | decimal string | Crypto amount for the trade. |
quotedRate | decimal string | The locked output-per-input rate the trade executed at. |
feeBps | integer | Fee applied, in basis points. |
spreadBps | integer | Spread applied, in basis points. |
engineFillTxId | string | 0Bit fill reference for reconciliation. May be absent when no fill occurred. |
settledAt | string | ISO 8601 settlement timestamp. May be absent for non-settled terminal states. |
createdAt | string | ISO 8601 timestamp for when the event was created. |
{
"eventId": "5f2c1b9a4d3e6f7081a2b3c4d5e6f70812a3b4c5d6e7f8091a2b3c4d5e6f7081",
"type": "pool.transaction.settled",
"txnId": "txn_3xampl3000000000000",
"partnerId": "ptnr_3xampl3000000",
"poolId": "pool_3xampl3000000",
"side": "on_ramp",
"status": "settled",
"fiatAmount": "100.00",
"cryptoAmount": "99.40",
"quotedRate": "0.9940",
"feeBps": 30,
"spreadBps": 25,
"engineFillTxId": "fill_3xampl3000000",
"settledAt": "2026-06-28T12:00:05Z",
"createdAt": "2026-06-28T12:00:05Z"
}Verify the signature
Every 0Pools delivery carries a Gate-Signature header in the form t=<unix timestamp>,v1=<hex hmac>. The HMAC is computed with your webhook secret over <timestamp>.<raw body>. Verify on the raw request body before parsing - framework body parsers can change bytes and break the signature.
import crypto from 'node:crypto';
function verifyPoolsWebhook(input: {
rawBody: string;
signature: string; // the Gate-Signature header value
secret: string;
skewSeconds?: number;
}) {
const skewSeconds = input.skewSeconds ?? 300;
const parts = Object.fromEntries(
input.signature.split(',').map((part) => {
const [key, value] = part.split('=');
return [key, value];
}),
);
const timestamp = Number(parts.t);
if (!Number.isInteger(timestamp)) throw new Error('invalid_signature');
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > skewSeconds) {
throw new Error('stale_signature');
}
const expected = crypto
.createHmac('sha256', input.secret)
.update(`${timestamp}.${input.rawBody}`)
.digest('hex');
const expectedBuffer = Buffer.from(expected, 'hex');
const receivedBuffer = Buffer.from(parts.v1 ?? '', 'hex');
if (
expectedBuffer.length !== receivedBuffer.length ||
!crypto.timingSafeEqual(expectedBuffer, receivedBuffer)
) {
throw new Error('signature_mismatch');
}
return JSON.parse(input.rawBody);
}See Verify webhook signatures for the shared verification convention and failure-handling table.
Dedupe on event id
Delivery is at least once, so the same event can arrive more than once. The eventId is deterministic - it equals sha256(txnId:eventType) - so a duplicate delivery carries the identical id. Insert the eventId into a unique-keyed event log and only apply the terminal transition on first insert.
async function handlePoolsEvent(event: { eventId: string; type: string; txnId: string }) {
const inserted = await eventLog.insertOnce({ eventId: event.eventId, type: event.type });
if (!inserted) return; // already processed
await poolTrades.applyTerminalOnce({
txnId: event.txnId,
status: event.type.split('.').pop(), // settled | failed | released
eventId: event.eventId,
});
}Delivery semantics
| Property | Behavior |
|---|---|
| Guarantee | At least once. Acknowledge with a 2xx; non-2xx or timeout is treated as a failure. |
| Retries | Failed deliveries are retried with exponential backoff: base * 2 ^ attempts. |
| Max attempts | A configurable maxAttempts cap bounds retries per event. |
| Dead-letter | After the cap is exhausted, the event is moved to a dead-letter store for later replay. |
| Ordering | Not guaranteed. Branch on type / status, not on arrival order. |
Reconcile alongside webhooks
Treat webhooks as the primary close signal and the status poll as the backstop. Poll-only states (returned, quote rejected) never arrive as webhooks, and dead-lettered deliveries are recovered by reconciling against your own trade reads.
Public boundary
This reference covers partner-visible discovery, quote, transact, status, trade, and balance behavior. Liquidity operations, routing internals, provider details, reserve logic, and runbooks are outside the public API contract.
Related pages
Listen for 0Bit webhooks
Route 0Pools events through one verified, idempotent intake path.
Verify webhook signatures
Implement raw-body HMAC verification.
Get 0Pools transaction status
Poll for poll-only states such as returned and rejected.
List 0Pools trades
Reconcile dead-lettered or missed deliveries from durable reads.