0Bit Documentation

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 typeTrade statusMeaningRecommended action
pool.transaction.settledsettledThe trade completed and delivery was performed.Close your own record once and notify the customer.
pool.transaction.failedfailedThe trade did not complete.Close or route to support per your product policy.
pool.transaction.releasedreleasedA 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.

FieldTypeDescription
eventIdstringStable dedupe key. Equals sha256(txnId:eventType). Treat as the idempotency key.
typestringOne of pool.transaction.settled, pool.transaction.failed, pool.transaction.released.
txnIdstringThe trade (transaction) id this event is about.
partnerIdstringThe partner the trade belongs to.
poolIdstringThe pool the trade executed against.
sidestringon_ramp or off_ramp.
statusstringTerminal trade status: settled, failed, or released. Matches type.
fiatAmountdecimal stringFiat amount for the trade.
cryptoAmountdecimal stringCrypto amount for the trade.
quotedRatedecimal stringThe locked output-per-input rate the trade executed at.
feeBpsintegerFee applied, in basis points.
spreadBpsintegerSpread applied, in basis points.
engineFillTxIdstring0Bit fill reference for reconciliation. May be absent when no fill occurred.
settledAtstringISO 8601 settlement timestamp. May be absent for non-settled terminal states.
createdAtstringISO 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

PropertyBehavior
GuaranteeAt least once. Acknowledge with a 2xx; non-2xx or timeout is treated as a failure.
RetriesFailed deliveries are retried with exponential backoff: base * 2 ^ attempts.
Max attemptsA configurable maxAttempts cap bounds retries per event.
Dead-letterAfter the cap is exhausted, the event is moved to a dead-letter store for later replay.
OrderingNot 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.

On this page