Skip to main contentSkip to FAQSkip to contact
For Opss7 min read

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:

Bash
1wscat -c wss://api.tetrafi.io/api/v1/ws
2> {"type":"auth","token":"tfk_test_..."}
3< {"type":"auth.ok","environment":"sandbox"}
4> {"type":"subscribe","topics":["orders:*"]}
5< {"type":"subscribe.ok","topics":["orders:*"]}
5 linesbash

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.

WebSocketWebhooks
LatencySub-second1-5 seconds typical
ReliabilityBest-effort (reconnect on drop)At-least-once delivery with retries
Best forActive takers, solvers, dashboardsBackend systems, accounting pipelines
Client requirementsPersistent connection, long-lived processPublic HTTPS endpoint
Setup complexityConnect + auth + subscribeRegister endpoint + verify signatures
Replay supportLast 100 events via cursorAll events stored 7 days, replayable
Backpressure handlingServer drops events on slow clientServer 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#

  1. Open WebSocket to wss://api.tetrafi.io/api/v1/ws
  2. Send authentication message with your API key
  3. Subscribe to one or more event channels
  4. Receive events as JSON messages
  5. Respond to server ping frames with pong every 30 seconds
TypeScript
1const ws = new WebSocket("wss://api.tetrafi.io/api/v1/ws?token=" + API_KEY);
2
3ws.onopen = () => {
4 // Subscribe to order events
5 ws.send(JSON.stringify({
6 type: "subscribe",
7 topics: ["orders:*"],
8 }));
9};
10
11ws.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};
19
20// Reconnect with exponential backoff
21let retryCount = 0;
22ws.onclose = () => {
23 const delay = Math.min(30000, 1000 * Math.pow(2, retryCount++));
24 setTimeout(connect, delay);
25};
25 linestypescript

Authentication#

Two equivalent methods - server responds with {"type":"auth.ok"} on success:

MethodPatternWhen to use
Query stringwss://.../ws?token=tfk_live_...Browser clients (can't set headers)
Auth messageConnect, then send {"type":"auth","token":"..."}Server clients (keeps keys out of URLs)

Event Channels#

Subscribe using topic-based patterns:

TopicMatches
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:

text
11. order.created Order submitted with winning quote
22. order.settled Both legs settled, funds delivered
33. compliance.event Compliance check outcome recorded
4
5Failure path:
61. order.created
72. order.failed Fill deadline expired, escrow refunded
83. compliance.event Failure recorded in audit trail
8 linestext

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

JSON
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}
14 linesjson

order.settled#

Emitted when DvP settlement succeeds - both legs complete.

JSON
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}
12 linesjson

compliance.event#

Emitted when a compliance check is recorded (approval, rejection, or audit entry).

JSON
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}
12 linesjson

Other Events#

EventCategoryDescription
credit.updatedCreditWorkspace credit balance changed
member.addedWorkspaceNew member joined workspace
member.removedWorkspaceMember 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#

Bash
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 }'
8 linesbash

Manage Webhooks#

Bash
1# List webhooks
2curl https://api.tetrafi.io/api/v1/webhooks \
3 -H "Authorization: Bearer tfk_live_..."
4
5# Delete
6curl -X DELETE https://api.tetrafi.io/api/v1/webhooks/{webhook_id} \
7 -H "Authorization: Bearer tfk_live_..."
8
9# Send test event
10curl -X POST https://api.tetrafi.io/api/v1/webhooks/{webhook_id}/test \
11 -H "Authorization: Bearer tfk_live_..."
11 linesbash

Webhook Signature Verification#

Every webhook POST includes signature headers for security verification. Always verify before processing:

TypeScript
1import crypto from "crypto";
2
3function verifyWebhook(
4 body: string,
5 signature: string,
6 timestamp: string,
7 secret: string
8): boolean {
9 // Prevent replay attacks: reject requests older than 5 minutes
10 const age = Date.now() / 1000 - parseInt(timestamp);
11 if (age > 300) return false;
12
13 // Compute expected signature
14 const expected = crypto
15 .createHmac("sha256", secret)
16 .update(`{timestamp}.{body}`)
17 .digest("hex");
18
19 // Timing-safe comparison
20 return crypto.timingSafeEqual(
21 Buffer.from(signature),
22 Buffer.from(expected)
23 );
24}
25
26// Express handler
27app.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();
31
32 if (!verifyWebhook(body, signature, timestamp, WEBHOOK_SECRET)) {
33 return res.status(401).json({ error: "Invalid signature" });
34 }
35
36 const event = JSON.parse(body);
37 handleEvent(event);
38 res.status(200).json({ received: true });
39});
39 linestypescript

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:

AttemptDelayCumulative Wait
1Immediate0s
25s5s
330s35s
45min5m 35s
51hr1h 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:

TypeScript
1// Store processed delivery IDs to prevent double-processing
2const processedIds = new Set<string>();
3
4app.post("/webhook/tetrafi", (req, res) => {
5 const deliveryId = req.headers["x-tetrafi-delivery-id"] as string;
6
7 if (processedIds.has(deliveryId)) {
8 return res.status(200).json({ received: true, duplicate: true });
9 }
10
11 processedIds.add(deliveryId);
12 // Process event...
13 res.status(200).json({ received: true });
14});
14 linestypescript

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

Language
TypeScript
1import { TetraFi } from '@tetrafi/sdk';
2
3const tetrafi = new TetraFi({
4 apiKey: process.env.TETRAFI_API_KEY!,
5 environment: 'sandbox',
6});
7
8// Subscribe to all settlement events
9tetrafi.events.on('settlement.*', (event) => {
10 console.log(`[{event.type}]`, event.settlement.id);
11});
12
13// Subscribe to quote events
14tetrafi.events.on('quote.received', (event) => {
15 console.log(`Quote: {event.quote.price} from {event.quote.solverId}`);
16});
16 linestypescript

See Also#

I'm in OpsStep 7 of 7
ErrorsPath complete - nice work.

Related topics