Skip to main content
Partner webhooks deliver order status changes for your partner account. They are the primary integration for:
  • orchestration orders
  • accumulation address deposits (orders have quoteId = null)
  • liquidation address deposits (orders have quoteId = null)

Register an Endpoint

Create an endpoint:
  • POST /v1/webhooks with { "url": "https://..." }
The response includes:
  • webhookId
  • secret (returned once)

Events

Webhook events are derived from order.status:
  • order.processing
  • order.confirming
  • order.bridging
  • order.swapping
  • order.awaiting_approval
  • order.refunding
  • order.delivering
  • order.completed
  • order.failed
  • order.refunded

Payload

Webhooks are delivered as an HTTP POST with Content-Type: application/json. Payload envelope:
{
  "event": "order.awaiting_approval",
  "timestamp": "2026-02-04T01:30:47.000Z",
  "data": {
    "id": "ord_...",
    "type": "order",
    "status": "awaiting_approval",
    "quoteId": "q_...",
    "amountIn": "250000",
    "amountOut": null,
    "feeBps": 5,
    "feeAmount": "100000",
    "slippageBps": 0,
    "source": {
      "chain": "bitcoin",
      "asset": "BTC",
      "address": null,
      "txHash": "<txid>",
      "sweepTxHash": null
    },
    "destination": {
      "chain": "base",
      "asset": "USDC",
      "address": "0x...",
      "txHash": null
    },
    "depositAddress": "bc1q...",
    "recipientAddress": "0x...",
    "flashnetRequestId": null,
    "sparkTxHash": null,
    "refund": {
      "asset": null,
      "amount": null,
      "txHash": null
    },
    "error": {
      "code": "reprice_approval_required",
      "message": "Received 249900 sats, requires at least 250000 sats"
    },
    "priceLock": {
      "version": 1,
      "mode": "approval_required",
      "amountIn": "250000",
      "quotedAmountOut": "100100000",
      "lockedMinAmountOut": "100000000",
      "slippageBps": 0,
      "createdAt": "2026-02-04T01:00:00.000Z",
      "expiresAt": "2026-02-04T01:30:00.000Z"
    },
    "reprice": {
      "version": 1,
      "status": "awaiting_approval",
      "previousLockedMinAmountOut": "100000000",
      "proposedMinAmountOut": "99800000",
      "latestExpectedAmountOut": "99800000",
      "requestedAt": "2026-02-04T01:30:47.000Z",
      "resolvedAt": null,
      "reason": "insufficient_input"
    },
    "paymentIntent": {
      "version": 1,
      "amountMode": "exact_out",
      "targetAmountOut": "100000000",
      "requiredAmountIn": "250000",
      "maxAcceptedAmountIn": "250050",
      "inputBufferBps": 2,
      "actualAmountIn": "249900",
      "refundAddress": "bc1q...",
      "exactOutExecution": "strict"
    },
    "feePlan": {
      "version": 1,
      "settlementChain": "solana",
      "settlementAsset": "USDC",
      "appFees": [
        {
          "affiliateId": "swapkit",
          "recipient": "So1AffiliateOne...",
          "feeBps": 50
        },
        {
          "recipient": "So1AffiliateTwo...",
          "feeBps": 50
        }
      ]
    },
    "feePayouts": {
      "version": 1,
      "entries": [
        {
          "idempotencyKey": "order:ord_...:full:appfee:0",
          "leg": "full",
          "chain": "solana",
          "role": "app_fee",
          "affiliateId": "swapkit",
          "recipient": "So1AffiliateOne...",
          "feeBps": 50,
          "amount": "24750",
          "txHash": "5kW...",
          "recordedAt": "2026-02-04T01:35:00.000Z"
        },
        {
          "idempotencyKey": "order:ord_...:full:payout",
          "leg": "full",
          "chain": "solana",
          "role": "recipient_payout",
          "recipient": "So1RecipientAddress...",
          "feeBps": null,
          "amount": "2475000",
          "txHash": "3hN...",
          "recordedAt": "2026-02-04T01:35:02.000Z"
        }
      ]
    },
    "createdAt": "2026-02-04T01:30:00.000Z",
    "updatedAt": "2026-02-04T01:30:47.000Z",
    "completedAt": null
  }
}
Notes:
  • data is an order snapshot at the moment the event was emitted.
  • priceLock, reprice, and paymentIntent are included only when applicable.
  • feePlan is included when quote appFees or affiliateId was requested.
  • feePayouts is included once payout legs are recorded.
  • feePayouts.entries[*].role is app_fee or recipient_payout.
  • feePayouts.entries[*].affiliateId is present for registry-based app-fee entries.
  • feePayouts.entries[*].leg is full, instant, or holdback.
  • When a Flashnet swap has executed, data.swap is included with simulation and execution metadata.
  • Some fields are null until the engine reaches that step.

Stages (Status Endpoint)

GET /v1/orchestration/status includes a stages array. Stages are monotonic markers recorded when a step completes. Common stage names:
  • deposit_confirmed
  • swept (accumulation/liquidation and other Bitcoin source flows)
  • bridged (bridge completed)
  • swapped (swap completed)
  • delivered
  • reprice_requested
  • reprice_approved
  • reprice_rejected
  • refund_requested
  • refunded
Bitcoin L1 deposits can also record:
  • zeroconf_accepted
  • deposit_claimed
  • holdback_ready
Some Bitcoin orders use multiple legs. In those cases, stage names can be leg-qualified, for example:
  • swept_instant, swapped_instant, bridged_instant, delivered_instant
  • swept_holdback, swapped_holdback, bridged_holdback, delivered_holdback
  • reprice_requested_instant, reprice_requested_holdback
Treat stages as informational. The primary state machine is data.status.

Signature Verification

Each delivery includes X-Flashnet-Signature. It is computed as:
  • hex(HMAC_SHA256(secret, raw_body_json))
Verify the signature against the raw request body bytes, not a re-serialized JSON object. Example (Node):
import crypto from 'node:crypto';

function timingSafeEqualHex(aHex: string, bHex: string): boolean {
  if (!/^[0-9a-f]+$/i.test(aHex) || !/^[0-9a-f]+$/i.test(bHex)) return false;
  if (aHex.length !== bHex.length) return false;

  const a = Buffer.from(aHex, 'hex');
  const b = Buffer.from(bHex, 'hex');
  if (a.length !== b.length) return false;

  return crypto.timingSafeEqual(a, b);
}

export function verifyFlashnetWebhook(params: {
  rawBody: string;
  signatureHeader: string | undefined;
  secret: string;
}): boolean {
  if (!params.signatureHeader) return false;

  const expected = crypto
    .createHmac('sha256', params.secret)
    .update(params.rawBody)
    .digest('hex');

  return timingSafeEqualHex(expected, params.signatureHeader);
}

Delivery Semantics

  • At least once: the same event may be delivered multiple times.
  • Consider deliveries successful only when your endpoint returns a 2xx status.
Retries use a fixed backoff schedule:
  • 10 seconds
  • 30 seconds
  • 2 minutes
  • 10 minutes
  • 30 minutes
  • 2 hours
  • 6 hours
  • 24 hours
After the final retry attempt, the delivery is marked failed.
  • Verify the signature before parsing JSON.
  • Make processing idempotent.
    • A safe key is (data.id, event, timestamp).
  • Return 2xx only after you have persisted the event.