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

How do I register an endpoint?

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

What events are emitted?

Webhook events are derived from public order.status transitions:
  • order.processing
  • order.confirming
  • order.bridging
  • order.swapping
  • order.awaiting_approval
  • order.refunding
  • order.delivering
  • order.completed
  • order.failed
  • order.unfulfilled
  • order.refunded
Internal paused transitions do not emit order.paused. Public webhook payloads continue to show data.status as processing until the order resumes or reaches a partner-visible final state. See Order Lifecycle for the full state machine, transition rules, and what each status means.

What does the payload look like?

Webhooks are delivered as an HTTP POST with Content-Type: application/json. Payload envelope:
{
  "event": "order.refunding",
  "timestamp": "2026-02-04T01:30:47.000Z",
  "data": {
    "id": "ord_...",
    "type": "order",
    "status": "refunding",
    "quoteId": "q_...",
    "amountIn": "250000",
    "amountOut": null,
    "feeBps": 5,
    "feeAmount": "100000",
    "slippageBps": 50,
    "source": {
      "chain": "bitcoin",
      "asset": "BTC",
      "address": null,
      "txHash": "<txid>",
      "sweepTxHash": null
    },
    "destination": {
      "chain": "base",
      "asset": "USDC",
      "address": "0x...",
      "txHash": null
    },
    "depositAddress": "bc1q...",
    "recipientAddress": "0x...",
    "payLinkId": "pl_abc123",
    "payLinkLabel": "October invoice #42",
    "flashnetRequestId": null,
    "sparkTxHash": null,
    "refund": {
      "asset": null,
      "amount": null,
      "txHash": null
    },
    "error": {
      "code": "slippage_exceeded",
      "message": "Pool moved past slippage tolerance between deposit and execution"
    },
    "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": "flashpartner",
          "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": "flashpartner",
          "recipient": "So1AffiliateOne...",
          "feeBps": 50,
          "amount": "24750",
          "platformCutAmount": "4950",
          "recipientAmount": "19800",
          "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",
          "platformCutAmount": null,
          "recipientAmount": null,
          "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.
  • paymentIntent is included only on exact-out orders.
  • payLinkId is included when the order originated from a pay-link. payLinkLabel is included when that pay-link was created with a label. Use either to reconcile webhook events back to the originating pay-link without maintaining an external mapping.
  • zeroconfOffer is included when a ZeroConf offer has been generated for a Bitcoin L1 deposit. See ZeroConf for the offer flow and Offer Fields for the field reference.
  • 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.
  • feePayouts.entries[*].amount is the gross fee. platformCutAmount is Flashnet’s 20% cut. recipientAmount is the 80% paid to the fee recipient. These fields are null for recipient_payout entries.
  • 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.

What stages appear in the status endpoint?

GET /v1/orchestration/status includes a stages array. Stages are monotonic markers recorded when a step completes. Common stage names:
  • deposit_confirmed
  • amount_reconciled (actual deposit differed from quoted amount; amountIn and feeAmount updated)
  • swept (accumulation/liquidation and other Bitcoin source flows)
  • bridged (bridge completed)
  • swapped (swap completed)
  • delivered
  • refund_requested
  • refunded
Bitcoin L1 deposits can also record:
  • zeroconf_offer_pending
  • zeroconf_offer_accepted
  • zeroconf_offer_declined
  • zeroconf_accepted
  • deposit_claimed
  • holdback_ready
Legacy multi-leg stages (swept_instant, swapped_instant, bridged_instant, delivered_instant, swept_holdback, swapped_holdback, bridged_holdback, delivered_holdback) are deprecated. Current orders use single-leg execution. Treat stages as informational. The primary state machine is data.status.

How do I verify webhook signatures?

Each delivery includes two headers:
  • X-Flashnet-Signature: hex-encoded HMAC-SHA256
  • X-Flashnet-Timestamp: millisecond epoch timestamp of the delivery attempt
The signature is computed as:
  • hex(HMAC_SHA256(secret, timestamp + "." + raw_body_json))
where timestamp is the value of X-Flashnet-Timestamp. Verify the signature against the raw request body bytes, not a re-serialized JSON object. Use the timestamp from the header, not from the payload.
The signature covers timestamp.body, not just body. If you are upgrading from a previous integration that verified against the raw body alone, you must update your verification code to prepend the timestamp.
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;
  timestampHeader: string | undefined;
  secret: string;
}): boolean {
  if (!params.signatureHeader || !params.timestampHeader) return false;

  const expected = crypto
    .createHmac('sha256', params.secret)
    .update(`${params.timestampHeader}.${params.rawBody}`)
    .digest('hex');

  return timingSafeEqualHex(expected, params.signatureHeader);
}

How are webhooks delivered?

  • 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.

What should my handler do?

  • 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.