Skip to content

Webhooks

v1 is closed to new integrations

Do not build on v1. The current API is v2 RFQ. v1 stays served for integrations already running on it, takes no new features, and gets no new endpoints.

Endpoint registration is shared

/v1/webhooks is the registry for both versions. v2 RFQ events are delivered to endpoints registered here, so this path is not going away with the rest of v1.

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 signingSecret (shown 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-Signature: t=<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.