Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.superbank.co/llms.txt

Use this file to discover all available pages before exploring further.

Quick start

The shortest path from zero to a verified delivery:
  1. Create an endpoint. POST /v0/webhooks with a public URL — save the secret from the response, you’ll need it to verify signatures.
  2. Verify the signature on incoming requests with HMAC-SHA256(secret, raw_body) against the X-Superbank-Signature header (jump to Verifying signatures).
  3. Return 200 quickly, then process asynchronously. Anything 2xx counts; anything else triggers a retry.

The basics

Supported events

EventWhen it fires
account.createdA new account was provisioned
account.updatedAccount balance or status changed
account.deletedAn account was deactivated
payment.createdA new payment was created
payment.updatedA payment status changed
settlement_request.createdA new settlement request was created
settlement_request.updatedA settlement request status changed
liquidity_pool.* events are deprecated. During the rename window (see ADR-030) Superbank emits both account.* and the legacy liquidity_pool.* events for the same state change, so existing handlers keep working. Subscribe to account.* going forward — the liquidity_pool.* aliases will be removed in the next major release. If you handle both, dedupe on data.id.

Envelope and headers

Every delivery uses the same JSON envelope. Field names are snake_case; the data object varies by event type. The full payload for each event is in Event payload reference at the bottom.
{
  "event": "<event_type>",
  "data": { ... },
  "timestamp": "2026-01-26T15:48:08.700Z"
}
HeaderDescription
Content-Typeapplication/json
X-Superbank-SignatureHMAC-SHA256 signature: sha256=<hex>
X-Superbank-EventEvent type (e.g., payment.updated)

Verifying signatures

Compute sha256=HMAC-SHA256(secret, request_body) over the raw request body and compare it to the X-Superbank-Signature header using a constant-time comparison. Verify before parsing — see Common pitfalls.
const crypto = require('crypto');

function verifyWebhookSignature(secret, payload, signatureHeader) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  const received = signatureHeader.replace('sha256=', '');
  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'hex'),
    Buffer.from(received, 'hex')
  );
}

// Express middleware example — note `express.raw` so the body stays bytes
app.post('/webhooks/superbank', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-superbank-signature'];
  const isValid = verifyWebhookSignature(
    process.env.WEBHOOK_SECRET,
    req.body,
    signature
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  // Handle event...
  res.status(200).send('OK');
});

Reliability

Retry policy

If your endpoint returns a non-2xx response or times out (30 seconds), Superbank retries with exponential backoff:
AttemptDelayCumulative
1Immediate
21 minute1 minute
35 minutes6 minutes
415 minutes21 minutes
51 hour~1.3 hours
61 day~1.3 days
72 days~3.3 days
84 days~7.3 days
91 week~14.3 days
102 weeks~28.3 days
After 10 failed attempts, the delivery is marked as permanently failed.

Common pitfalls

  • Verify before parse. Run signature verification on the raw bytes before JSON.parse. Frameworks that auto-parse JSON (Express default, NestJS body parser) will silently re-serialize the body and your HMAC will never match — use express.raw / request.data / equivalent to hold onto the original bytes.
  • Return 200 fast, process async. Heavy work on the request thread blows past the 30-second timeout and triggers retries. Acknowledge, then enqueue.
  • Expect at-least-once delivery. Retries are real — the same event can land twice. Make handlers idempotent on the event id (or on data.id + status transition).
  • Don’t filter by source IP. Egress IPs change without notice; rely on the signature.

Testing

Test deliveries

Test deliveries land at your endpoint with two markers your handler should expect:
  • data.test: true — every test payload sets a top-level test: true inside data. Branch on it if you want to short-circuit business logic for test events.
  • Sentinel resource IDs — IDs use the prefix 00000000-0000-0000-0000-..., with the last digit identifying the resource type (...001 settlement request, ...002 outbound payment, etc.). Allow-list these prefixes if your handler validates IDs against your database.
Headers and signature are computed exactly as in production, so a handler that verifies signatures accepts test deliveries without any code branch.

From your local machine with ngrok

ngrok creates a public tunnel to your localhost so sandbox deliveries land directly on your dev box.
# 1. Start your handler (whichever port it binds to)
node server.js

# 2. Tunnel it
ngrok http 3000

# 3. Register the public URL it prints
curl --request POST \
  --url https://api-sandbox.superbank.co/v0/webhooks \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --data '{ "url": "https://abc123.ngrok-free.app/webhooks/superbank" }'

From a browser with webhook.site

webhook.site gives you an instant public URL to inspect deliveries without writing any handler code — useful for eyeballing payloads before you write parsing logic.
# Register the unique URL it generates
curl --request POST \
  --url https://api-sandbox.superbank.co/v0/webhooks \
  --header 'Content-Type: application/json' \
  --header 'X-Api-Key: YOUR_API_KEY' \
  --data '{ "url": "https://webhook.site/YOUR_UNIQUE_ID" }'
Save the secret from the response, then trigger a sandbox event (e.g., POST /v0/settlement-requests) and watch the delivery land in the webhook.site browser tab.

Event payload reference

The data object differs per event. Expand the relevant section for a worked example. All examples carry production-shape fields; sandbox and production payloads have identical shape.
{
  "event": "payment.created",
  "data": {
    "id": "04621f85-bd40-46a9-a9a9-9fe14be09354",
    "type": "PAYIN",
    "status": "PENDING",
    "fee": "0.50000000",
    "source": {
      "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "amount": "100.00000000",
      "currency": "USDC",
      "rail": "SOLANA",
      "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "transaction_hash": null
    },
    "destination": {
      "account_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "amount": "100.00000000",
      "currency": "USD",
      "rail": "ACH",
      "wallet_address": null,
      "transaction_hash": null
    },
    "created_at": "2026-01-26T14:12:08.354Z"
  },
  "timestamp": "2026-01-26T14:12:08.700Z"
}
{
  "event": "payment.updated",
  "data": {
    "id": "04621f85-bd40-46a9-a9a9-9fe14be09354",
    "type": "PAYIN",
    "status": "COMPLETED",
    "fee": "0.50000000",
    "settlement_request_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
    "source": {
      "account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "amount": "100.00000000",
      "currency": "USDC",
      "rail": "SOLANA",
      "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "transaction_hash": "5aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5aB6c"
    },
    "destination": {
      "account_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "amount": "100.00000000",
      "currency": "USD",
      "rail": "ACH",
      "wallet_address": null,
      "transaction_hash": null
    },
    "updated_at": "2026-01-26T15:48:08.670Z"
  },
  "timestamp": "2026-01-26T15:48:08.700Z"
}
settlement_request.created
{
  "event": "settlement_request.created",
  "data": {
    "id": "39760060-846e-4d5a-8583-7ee62553f79b",
    "type": "STABLECOIN_TO_STABLECOIN",
    "payment_reason": "REMITTANCES",
    "status": "REQUEST_STARTED",
    "amount": "20.00000000",
    "external_id": "txn_abc123",
    "metadata": { "user_id": "usr_42", "source": "mobile_app" },
    "source": null,
    "destination": {
      "currency": "USDC",
      "rail": "SOLANA",
      "is_third_party": true,
      "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "beneficiary": {
        "type": "BUSINESS",
        "business_name": "Acme Corp",
        "address": { "country_code": "US" }
      }
    },
    "payment_instructions": {
      "currency": "USDC",
      "rail": "SOLANA",
      "prefunded_wallet_address": "AWE1XaAdRuxzjqy8Q7q75MFbPTs1W6Zbp3zvYWcDGjTj"
    },
    "outbound_payment": null,
    "inbound_payment": null,
    "created_at": "2026-01-26T15:45:10.047Z",
    "updated_at": "2026-01-26T15:45:10.047Z",
    "processing_at": null,
    "completed_at": null,
    "reconciliation_expected_at": null,
    "failure_code": null,
    "failure_reason": null
  },
  "timestamp": "2026-01-26T15:45:10.100Z"
}
settlement_request.updated
{
  "event": "settlement_request.updated",
  "data": {
    "id": "39760060-846e-4d5a-8583-7ee62553f79b",
    "type": "STABLECOIN_TO_STABLECOIN",
    "payment_reason": "REMITTANCES",
    "status": "SETTLEMENT_COMPLETED",
    "previous_status": "FUNDS_SENT",
    "amount": "20.00000000",
    "external_id": "txn_abc123",
    "metadata": { "user_id": "usr_42", "source": "mobile_app" },
    "source": null,
    "destination": {
      "currency": "USDC",
      "rail": "SOLANA",
      "is_third_party": true,
      "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
      "beneficiary": {
        "type": "BUSINESS",
        "business_name": "Acme Corp",
        "address": { "country_code": "US" }
      }
    },
    "payment_instructions": {
      "currency": "USDC",
      "rail": "SOLANA",
      "prefunded_wallet_address": "AWE1XaAdRuxzjqy8Q7q75MFbPTs1W6Zbp3zvYWcDGjTj"
    },
    "outbound_payment": {
      "id": "2a9d2a2c-d97b-4973-baf1-78e31d37a024",
      "type": "PAYOUT",
      "status": "COMPLETED",
      "amount": "20.00000000",
      "currency": "USDC",
      "created_at": "2026-01-26T15:45:51.589Z"
    },
    "inbound_payment": null,
    "created_at": "2026-01-26T15:45:10.047Z",
    "updated_at": "2026-01-26T15:48:08.670Z",
    "processing_at": "2026-01-26T15:45:51.594Z",
    "completed_at": "2026-01-26T15:48:08.657Z",
    "reconciliation_expected_at": "2026-01-29T15:45:51.594Z",
    "failure_code": null,
    "failure_reason": null
  },
  "timestamp": "2026-01-26T15:48:08.700Z"
}
{
  "event": "account.created",
  "data": {
    "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "currency_code": "USDC",
    "rail": "SOLANA",
    "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "name": "USDC Account",
    "status": "ACTIVE",
    "balance": "0.00000000",
    "available_balance": "0.00000000",
    "created_at": "2026-01-26T14:12:08.354Z"
  },
  "timestamp": "2026-01-26T14:12:08.700Z"
}
{
  "event": "account.updated",
  "data": {
    "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "currency_code": "USDC",
    "rail": "SOLANA",
    "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "name": "USDC Account",
    "status": "ACTIVE",
    "balance": "1500.00000000",
    "available_balance": "1200.00000000",
    "reserved": "200.00000000",
    "updated_at": "2026-01-26T15:48:08.670Z"
  },
  "timestamp": "2026-01-26T15:48:08.700Z"
}
{
  "event": "account.deleted",
  "data": {
    "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
    "currency_code": "USDC",
    "rail": "SOLANA",
    "wallet_address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
    "deleted_at": "2026-01-26T16:00:00.000Z"
  },
  "timestamp": "2026-01-26T16:00:00.100Z"
}
During the rename window each account.* event is also emitted under its liquidity_pool.* alias with an identical data payload (the event field is the only difference). Existing handlers will keep receiving these until the next major release. Subscribe to the canonical account.* events going forward.

Next steps

Looking for the end-to-end on-ramping flow? See Real-Time On-Ramping — the section Detecting Completion shows where webhook events fit into the settlement lifecycle.