Webhooks & Real-Time Events#
TetraFi provides two channels for real-time data: WebSocket for low-latency streaming (preferred for solvers and active takers) and webhooks for reliable server-to-server notifications.
60-second event stream test. Install wscat (npm install -g wscat), connect, and authenticate - you'll see the auth-success acknowledgment within a second of running this:
1wscat -c wss://api.tetrafi.io/api/v1/ws2> {"type":"auth","token":"tfk_test_..."}3< {"type":"auth.ok","environment":"sandbox"}4> {"type":"subscribe","topics":["orders:*"]}5< {"type":"subscribe.ok","topics":["orders:*"]}Once subscribed, every matching event will print to stdout in real time.
Choosing the Right Channel#
Use WebSocket when latency matters (sub-second updates) and your client is long-lived. Use webhooks when you need durable server-to-server delivery and your endpoint is publicly addressable.
| WebSocket | Webhooks | |
|---|---|---|
| Latency | Sub-second | 1-5 seconds typical |
| Reliability | Best-effort (reconnect on drop) | At-least-once delivery with retries |
| Best for | Active takers, solvers, dashboards | Backend systems, accounting pipelines |
| Client requirements | Persistent connection, long-lived process | Public HTTPS endpoint |
| Setup complexity | Connect + auth + subscribe | Register endpoint + verify signatures |
| Replay support | Last 100 events via cursor | All events stored 7 days, replayable |
| Backpressure handling | Server drops events on slow client | Server retries with backoff |
Most production integrations use both: WebSocket for the live UI / quote auction loop, and webhooks as a reliable fallback for settlement events that absolutely cannot be missed (e.g. accounting reconciliation).
WebSocket API#
Connect to the real-time event stream for sub-second quote and settlement updates:
Connection Flow#
- Open WebSocket to
wss://api.tetrafi.io/api/v1/ws - Send authentication message with your API key
- Subscribe to one or more event channels
- Receive events as JSON messages
- Respond to server
pingframes withpongevery 30 seconds
1const ws = new WebSocket("wss://api.tetrafi.io/api/v1/ws?token=" + API_KEY);23ws.onopen = () => {4 // Subscribe to order events5 ws.send(JSON.stringify({6 type: "subscribe",7 topics: ["orders:*"],8 }));9};1011ws.onmessage = (e) => {12 const event = JSON.parse(e.data);13 switch (event.type) {14 case "order.created": handleOrder(event.order); break;15 case "order.settled": handleSettled(event.order); break;16 case "ping": ws.send(JSON.stringify({ type: "pong" })); break;17 }18};1920// Reconnect with exponential backoff21let retryCount = 0;22ws.onclose = () => {23 const delay = Math.min(30000, 1000 * Math.pow(2, retryCount++));24 setTimeout(connect, delay);25};Authentication#
Two equivalent methods - server responds with {"type":"auth.ok"} on success:
| Method | Pattern | When to use |
|---|---|---|
| Query string | wss://.../ws?token=tfk_live_... | Browser clients (can't set headers) |
| Auth message | Connect, then send {"type":"auth","token":"..."} | Server clients (keeps keys out of URLs) |
Event Channels#
Subscribe using topic-based patterns:
| Topic | Matches |
|---|---|
orders:* | All order lifecycle events |
orders:{orderId} | Events for a specific order |
prices:{srcChain}:{inputToken}:{dstChain}:{outputToken}:{tier} | Price feed for a corridor |
Price Feed Schema#
Each message on a prices:* topic carries the following fields:
pricestring- Indicative mid price as decimal string (token-decimal precision preserved)
tierstring- Size tier: 'retail' | 'standard' | 'institutional'
timestampnumber- Unix ms when the price was observed
sourcestring- Solver ID or aggregator identifier that produced the quote
ttlMsnumber- Indicative validity window - stale prices should not be used for execution
Implement WebSocket reconnection with exponential backoff. Production connections may drop during deployments or network interruptions. The server sends a ping every 30 seconds - you must respond with pong to stay connected.
Event Types#
TetraFi emits order-centric events covering the settlement lifecycle:
11. order.created ← Order submitted with winning quote22. order.settled ← Both legs settled, funds delivered33. compliance.event ← Compliance check outcome recorded45Failure path:61. order.created72. order.failed ← Fill deadline expired, escrow refunded83. compliance.event ← Failure recorded in audit trailAdditional event types: credit.updated (workspace credit changes), member.added / member.removed (workspace membership).
Idempotency. Each event has a unique id (evt_*). Your handler must be idempotent - the same event may be delivered more than once during retries or reconnects. Store the evt_* IDs you've already processed and skip duplicates.
order.created#
Emitted when an order is submitted with quotes from competing solvers.
1{2 "type": "order.created",3 "id": "evt_abc123",4 "timestamp": "2024-03-10T12:00:00.000Z",5 "order": {6 "id": "ord_xyz789",7 "pair": "USDC/USDT",8 "side": "buy",9 "amount": "1000000.00",10 "corridor": "ethereum-optimism",11 "fillDeadline": 1710000035,12 "status": "pending"13 }14}order.settled#
Emitted when DvP settlement succeeds - both legs complete.
1{2 "type": "order.settled",3 "id": "evt_def456",4 "timestamp": "2024-03-10T12:00:42.000Z",5 "order": {6 "id": "ord_xyz789",7 "originTxHash": "0x7f3a...",8 "destinationTxHash": "0x9b2d...",9 "amount": "1000000.00",10 "settledAt": "2024-03-10T12:00:42.000Z"11 }12}compliance.event#
Emitted when a compliance check is recorded (approval, rejection, or audit entry).
1{2 "type": "compliance.event",3 "id": "evt_ghi789",4 "timestamp": "2024-03-10T12:00:42.100Z",5 "compliance": {6 "orderId": "ord_xyz789",7 "checkType": "pre_trade",8 "result": "approved",9 "hash": "0xab12...",10 "prevHash": "0xcd34..."11 }12}Other Events#
| Event | Category | Description |
|---|---|---|
credit.updated | Credit | Workspace credit balance changed |
member.added | Workspace | New member joined workspace |
member.removed | Workspace | Member removed from workspace |
Webhook Configuration#
Webhooks deliver events to your server endpoint via HTTPS POST. Use webhooks for reliable, persistent notifications that don't require an active connection.
Register a Webhook#
1curl -X POST https://api.tetrafi.io/api/v1/webhooks \2 -H "Authorization: Bearer tfk_live_..." \3 -H "Content-Type: application/json" \4 -d '{5 "url": "https://your-server.com/webhook/tetrafi",6 "events": ["order.*", "compliance.event"],7 "secret": "whsec_your_signing_secret"8 }'Manage Webhooks#
1# List webhooks2curl https://api.tetrafi.io/api/v1/webhooks \3 -H "Authorization: Bearer tfk_live_..."45# Delete6curl -X DELETE https://api.tetrafi.io/api/v1/webhooks/{webhook_id} \7 -H "Authorization: Bearer tfk_live_..."89# Send test event10curl -X POST https://api.tetrafi.io/api/v1/webhooks/{webhook_id}/test \11 -H "Authorization: Bearer tfk_live_..."Webhook Signature Verification#
Every webhook POST includes signature headers for security verification. Always verify before processing:
1import crypto from "crypto";23function verifyWebhook(4 body: string,5 signature: string,6 timestamp: string,7 secret: string8): boolean {9 // Prevent replay attacks: reject requests older than 5 minutes10 const age = Date.now() / 1000 - parseInt(timestamp);11 if (age > 300) return false;1213 // Compute expected signature14 const expected = crypto15 .createHmac("sha256", secret)16 .update(`{timestamp}.{body}`)17 .digest("hex");1819 // Timing-safe comparison20 return crypto.timingSafeEqual(21 Buffer.from(signature),22 Buffer.from(expected)23 );24}2526// Express handler27app.post("/webhook/tetrafi", express.raw({ type: "*/*" }), (req, res) => {28 const signature = req.headers["x-tetrafi-signature"] as string;29 const timestamp = req.headers["x-tetrafi-timestamp"] as string;30 const body = req.body.toString();3132 if (!verifyWebhook(body, signature, timestamp, WEBHOOK_SECRET)) {33 return res.status(401).json({ error: "Invalid signature" });34 }3536 const event = JSON.parse(body);37 handleEvent(event);38 res.status(200).json({ received: true });39});Webhook Headers#
X-TetraFi-Signaturestringrequired- HMAC-SHA256 signature of timestamp.body
X-TetraFi-Timestampstringrequired- Unix timestamp when the webhook was sent
X-TetraFi-Eventstring- Event type (e.g. settlement.complete)
X-TetraFi-WebhookIdstring- Your webhook endpoint ID
X-TetraFi-DeliveryIdstring- Unique delivery attempt ID
Retry Logic#
TetraFi retries failed webhook deliveries with exponential backoff:
| Attempt | Delay | Cumulative Wait |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 5s | 5s |
| 3 | 30s | 35s |
| 4 | 5min | 5m 35s |
| 5 | 1hr | 1h 5m 35s |
A delivery is considered failed if your endpoint returns a non-2xx status code or times out (30s timeout).
A webhook endpoint is automatically disabled after 5 consecutive delivery failures. Re-enable via the dashboard or POST /v1/webhooks/{id}/enable. You'll receive an email notification when a webhook is disabled.
Handling Duplicates#
Webhook deliveries may occasionally be duplicated due to retry logic. Use the X-TetraFi-DeliveryId header to deduplicate:
1// Store processed delivery IDs to prevent double-processing2const processedIds = new Set<string>();34app.post("/webhook/tetrafi", (req, res) => {5 const deliveryId = req.headers["x-tetrafi-delivery-id"] as string;67 if (processedIds.has(deliveryId)) {8 return res.status(200).json({ received: true, duplicate: true });9 }1011 processedIds.add(deliveryId);12 // Process event...13 res.status(200).json({ received: true });14});For production, store processed delivery IDs in a database with a TTL of 48 hours. An in-memory Set works for development but won't survive process restarts.
Quick Start: Event Subscription#
Subscribe to Events
1import { TetraFi } from '@tetrafi/sdk';23const tetrafi = new TetraFi({4 apiKey: process.env.TETRAFI_API_KEY!,5 environment: 'sandbox',6});78// Subscribe to all settlement events9tetrafi.events.on('settlement.*', (event) => {10 console.log(`[{event.type}]`, event.settlement.id);11});1213// Subscribe to quote events14tetrafi.events.on('quote.received', (event) => {15 console.log(`Quote: {event.quote.price} from {event.quote.solverId}`);16});