Skip to main contentSkip to FAQSkip to contact

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/v1/ws
2> {"type":"auth","token":"sk_test_..."}
3< {"type":"auth.ok","environment":"sandbox"}
4> {"type":"subscribe","channels":["rfq.*","settlement.*"]}
5< {"type":"subscribe.ok","channels":["rfq.*","settlement.*"]}
5 linesbash

Once subscribed, every matching event will print to stdout in real time.

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

Connect to the real-time event stream for sub-second quote and settlement updates:

  1. Open WebSocket to wss://api.tetrafi.io/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/v1/ws");
2
3ws.onopen = () => {
4 // Step 1: Authenticate
5 ws.send(JSON.stringify({
6 type: "auth",
7 token: process.env.TETRAFI_API_KEY,
8 }));
9
10 // Step 2: Subscribe to channels
11 ws.send(JSON.stringify({
12 type: "subscribe",
13 channels: ["rfq.*", "settlement.*"],
14 // Optional: filter by corridor
15 corridors: ["ethereum-optimism"],
16 }));
17};
18
19ws.onmessage = (e) => {
20 const event = JSON.parse(e.data);
21 switch (event.type) {
22 case "rfq.created": handleRFQ(event.rfq); break;
23 case "quote.received": handleQuote(event.quote); break;
24 case "settlement.complete": handleSettlement(event.settlement); break;
25 case "ping": ws.send(JSON.stringify({ type: "pong" })); break;
26 }
27};
28
29// Reconnect with exponential backoff
30let retryCount = 0;
31ws.onclose = () => {
32 const delay = Math.min(30000, 1000 * Math.pow(2, retryCount++));
33 setTimeout(connect, delay);
34};
34 linestypescript

Subscribe using exact event types or glob patterns:

PatternMatches
rfq.createdNew RFQ submitted
quote.receivedSolver submitted quote
rfq.*All RFQ events
settlement.*All settlement events
audit.recordedWORM entry created

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.

TetraFi emits 7 event types covering the complete settlement lifecycle. The typical event sequence for a successful trade follows this order:

text
11. rfq.created Taker submits intent
22. quote.received N) Solvers respond (typically 3-7 quotes)
33. quote.accepted Taker selects winning quote
44. settlement.pending Escrow locked, settlement in flight
55. settlement.complete Both legs settled atomically
66. audit.recorded WORM entry written
7
8Failure path:
93. quote.accepted
104. settlement.pending
115. settlement.failed Refund issued, both legs reverted
126. audit.recorded Failure recorded in WORM trail
12 linestext

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.

Emitted when a taker submits a new Request for Quote.

JSON
1{
2 "type": "rfq.created",
3 "id": "evt_abc123",
4 "timestamp": "2024-03-10T12:00:00.000Z",
5 "rfq": {
6 "id": "rfq_xyz789",
7 "pair": "USDC/USDT",
8 "side": "buy",
9 "amount": "1000000.00",
10 "corridor": "ethereum-optimism",
11 "deadline": 1710000005,
12 "complianceAttestation": "0x..."
13 }
14}
14 linesjson

Emitted when a solver submits a quote (visible to the taker only).

JSON
1{
2 "type": "quote.received",
3 "id": "evt_def456",
4 "timestamp": "2024-03-10T12:00:01.500Z",
5 "quote": {
6 "id": "qt_ghi012",
7 "rfqId": "rfq_xyz789",
8 "price": "0.9995",
9 "solverId": "solver_alpha",
10 "ttl": 30,
11 "confidence": 0.98
12 }
13}
13 linesjson

Emitted when the taker accepts a quote, triggering escrow creation.

JSON
1{
2 "type": "quote.accepted",
3 "quote": { "id": "qt_ghi012", "price": "0.9995" },
4 "settlement": { "id": "stl_jkl345", "status": "pending" }
5}
5 linesjson

Emitted when funds are locked in the origin chain escrow.

JSON
1{
2 "type": "settlement.pending",
3 "settlement": {
4 "id": "stl_jkl345",
5 "escrowTxHash": "0x7f3a...",
6 "originChainId": 1,
7 "destinationChainId": 10,
8 "fillDeadline": 1710000035
9 }
10}
10 linesjson

Emitted when DvP settlement succeeds - both legs complete atomically.

JSON
1{
2 "type": "settlement.complete",
3 "settlement": {
4 "id": "stl_jkl345",
5 "originTxHash": "0x7f3a...",
6 "destinationTxHash": "0x9b2d...",
7 "amount": "1000000.00",
8 "settledAt": "2024-03-10T12:00:42.000Z"
9 }
10}
10 linesjson

Emitted when settlement times out or fails. Escrow auto-refunds the taker.

JSON
1{
2 "type": "settlement.failed",
3 "settlement": {
4 "id": "stl_jkl345",
5 "error": "fill_timeout",
6 "refundTxHash": "0xc4f1...",
7 "refundedAt": "2024-03-10T12:01:15.000Z"
8 }
9}
9 linesjson

Emitted when a WORM evidence entry is written to the ledger.

JSON
1{
2 "type": "audit.recorded",
3 "audit": {
4 "tradeId": "stl_jkl345",
5 "type": "settlement_complete",
6 "timestamp": "2024-03-10T12:00:42.100Z",
7 "hash": "0xab12...",
8 "prevHash": "0xcd34..."
9 }
10}
10 linesjson

Webhooks deliver events to your server endpoint via HTTPS POST. Use webhooks for reliable, persistent notifications that don't require an active connection.

TypeScript
1const webhook = await tetrafi.webhooks.create({
2 url: "https://your-server.com/webhook/tetrafi",
3 events: ["settlement.*", "audit.recorded"],
4 secret: generateSecureSecret(), // Store this securely
5});
6
7console.log("Webhook ID:", webhook.id);
7 linestypescript
TypeScript
1// List webhooks
2const webhooks = await tetrafi.webhooks.list();
3
4// Update
5await tetrafi.webhooks.update(webhook.id, {
6 events: ["settlement.complete"],
7});
8
9// Delete
10await tetrafi.webhooks.delete(webhook.id);
11
12// Send test event
13await tetrafi.webhooks.test(webhook.id);
13 linestypescript

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
HeaderDescription
X-TetraFi-SignatureHMAC-SHA256 signature of timestamp.body
X-TetraFi-TimestampUnix timestamp when the webhook was sent
X-TetraFi-EventEvent type (e.g. settlement.complete)
X-TetraFi-WebhookIdYour webhook endpoint ID
X-TetraFi-DeliveryIdUnique delivery attempt ID

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.

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.