Appearance
Webhooks
Register an HTTPS endpoint and we POST a signed event to it when one of your RFQs reaches a terminal state, so you don't have to poll.
Webhooks are a partner feature: they need an API key with webhooks:manage, and events only fire for RFQs that key's partner record created. Anonymous callers should poll GET /v2/rfq/{id} instead.
Register an endpoint
Endpoint management lives on /v1/webhooks. One registry for the whole API, whichever events you subscribe to. The path is v1 but the registry is shared and current, so it is not affected by v1's deprecation.
POST /v1/webhooks
{
"url": "https://your-app.com/webhooks/textile",
"eventTypes": ["rfq.filled", "rfq.failed", "rfq.expired"]
}The URL must be https. 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": ["rfq.filled", "rfq.failed", "rfq.expired"],
"status": "active",
"signingSecret": "whsec_…"
}
}List with GET /v1/webhooks (secrets are never returned again), remove with DELETE /v1/webhooks/{id}.
Events
| Event | Fires when |
|---|---|
rfq.filled | The swap settled on chain. |
rfq.failed | The signed order's deadline passed and the reported transaction never landed (failReason: "not_landed"). |
rfq.expired | The firm quote lapsed without being executed. |
json
{
"id": "rfq.filled:rfq_…",
"type": "rfq.filled",
"created": "2026-08-19T12:00:09.000Z",
"data": {
"apiVersion": 2,
"rfq": {
"id": "rfq_…",
"chainId": 56,
"sellToken": "0xCNGN",
"buyToken": "0xUSDT",
"sellAmount": "1000000000",
"buyAmount": "612000000",
"feeAmount": "99990",
"taker": "0xYourWallet",
"txHash": "0xabc…",
"filledAt": "2026-08-19T12:00:09.000Z",
"failReason": null,
"late": false,
"supersedes": null,
"previousFailReason": null
}
}
}Two fields to watch:
apiVersion: 2is on every RFQ event. If one handler serves several event families, branch on this rather than on the event name prefix.- The amounts are what settled, not what you asked for:
sellAmountis the actual debit including fee,buyAmountis what was delivered,feeAmountis the fee between them. On an exact-input RFQ the debit is the quote'stakerPays, which can sit an atomic unit or two under the cap you sent.
late, supersedes and previousFailReason appear on rfq.filled only. A happy-path fill sends late: false and nulls. A correction of an earlier terminal sends late: true, supersedes: "rfq.expired" | "rfq.failed", and the failReason it just cleared.
Two headers come with every delivery:
Textile-Event-Id: a stable id. Delivery is at-least-once, so dedupe on it.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:
IMPORTANT
Hash the raw body bytes, not a re-serialized object. Most frameworks parse JSON before your handler runs, and JSON.stringify(req.body) gives back different bytes (key order, whitespace, number formatting), so every signature fails. Capture the raw body first: express.raw({ type: 'application/json' }), await req.text() on a Fetch Request, or your framework's equivalent.
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.
Return a 2xx quickly to acknowledge. Non-2xx or a timeout is retried with exponential backoff. Do slow work after acknowledging, keyed on Textile-Event-Id so a retry is a no-op.
Corrections
rfq.expired and rfq.failed are provisional for 24 hours. Quote expiry is not order death: the signed order stays fillable until orderDeadline, and a fill that indexes late still corrects the record afterwards. The correction arrives as a second rfq.filled with late: true. Treat that as "unbook the miss."
A safe handler:
- keys on
rfq.id, - lets
rfq.filledoverwrite an earlierfailed/expired, - never lets
failed/expiredoverwrite afilled.
After 24 hours we stop correcting. GET /v2/rfq/{id} uses the same cutoff. Status and the last webhook then stay expired or failed even if a fill later appears on chain.