Skip to content

Webhooks

Register an HTTPS endpoint and we'll POST a signed event to it when one of your swaps settles on-chain, so you don't have to poll GET /v1/swaps/{id}.

Register an endpoint

POST /v1/webhooks
{
  "url": "https://your-app.com/webhooks/textile",
  "eventTypes": ["swap.filled"]
}

Requires webhooks:manage. The URL must be https. Four events exist: swap.filled (a swap you're tracking settled on-chain) and limit_order.filled / limit_order.expired / limit_order.cancelled (a limit order you submitted through the API moved through its lifecycle). The response includes a signingSecretshown once — that you use to verify deliveries:

json
{
  "data": {
    "id": "whe_…",
    "url": "https://your-app.com/webhooks/textile",
    "eventTypes": ["swap.filled"],
    "status": "active",
    "signingSecret": "whsec_…"
  }
}

List with GET /v1/webhooks (secrets are never returned again) and remove with DELETE /v1/webhooks/{id}.

The event

We POST a JSON body like:

json
{
  "id": "swap.filled:swp_…",
  "type": "swap.filled",
  "created": "2026-07-07T12:00:05.000Z",
  "data": {
    "swap": {
      "id": "swp_…",
      "chainId": 8453,
      "txHash": "0x…",
      "soldAmount": "1000000000",
      "proceeds": "612000000",
      "effectiveRateRay": "612000000000000000000000000",
      "ordersFilled": 2,
      "filledAt": "2026-07-07T12:00:05.000Z"
    }
  }
}

Limit-order events carry the order in your frame — what you sold and what the fill pays you, exactly the amounts the order named:

json
{
  "id": "limit_order.filled:…",
  "type": "limit_order.filled",
  "created": "2026-07-14T12:34:58.000Z",
  "data": {
    "limitOrder": {
      "id": "…",
      "chainId": 8453,
      "sellToken": "0xCNGN",
      "sellAmount": "1550000000",
      "buyToken": "0xUSDT",
      "buyAmount": "1089913",
      "rateRay": "…",
      "executionRateRay": "…",
      "deadline": "2026-07-14T13:00:00.000Z",
      "txHash": "0x…",
      "filledAt": "2026-07-14T12:34:56.000Z"
    }
  }
}

rateRay is the book rate (sellToken-per-buyToken); executionRateRay is the maker frame (buyToken-per-sellToken), matching /limit-orders/quote and prepare's assumedRateRay. limit_order.expired and limit_order.cancelled share the shape (txHash/filledAt are null). Expired and cancelled are notified with a deliberate ~10-minute lag past the deadline (plus the sweep's 5-minute cadence), so a fill that settled just before the deadline reconciles first; in the rare race where a cancel notification is followed by a late-reconciled fill, the limit_order.filled event supersedes it. Don't use these events as your repost trigger — run your own deadline timer and poll GET /v1/limit-orders/{id}, whose status is computed live (see the repost pattern).

Two headers come with it:

  • Textile-Event-Id — a stable id. Deliveries are at-least-once, so dedupe on this.
  • Textile-Signaturet=<unix>,v1=<hmac>.

Verify the signature

The signature is HMAC-SHA256(signingSecret, "<t>.<rawBody>"), hex-encoded. Recompute it over the raw request body and compare:

ts
import crypto from 'crypto'

function verify(secret: string, header: string, rawBody: string): boolean {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('=') as [string, string])
  )
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex')
  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(parts.v1, 'hex')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

Reject anything that doesn't verify. You can also reject deliveries whose t is too far from now to guard against replay.

Delivery & retries

Return a 2xx quickly to acknowledge. Non-2xx (or a timeout) is retried with exponential backoff. Do your slow work after acknowledging, keyed on Textile-Event-Id so a retry is a no-op.