Teel sends real-time updates whenever a payout is created or changes status. Two delivery modes:
Mode
When to use
WebSocket
Your backend can hold a long-lived connection (Node.js server, Go service, Python worker). Lowest latency, no public URL needed.
HTTP webhooks
Your backend is serverless or runs behind a CDN (Lambda, Cloudflare Workers, Vercel Edge, Next.js API routes). Each event is delivered as a POST to a URL you control.
You can use either, or both — they carry the same events.
Add a subscription via the Webhooks page in the dashboard. You’ll provide:
URL — must be https://. Loopback, RFC1918 / private IPv4 and IPv6 ranges, link-local addresses, and known cloud metadata hosts are rejected at create time and at delivery time (DNS is re-resolved before each POST).
Events — pick from the allow list (payout.created, payout.status.updated).
Label — optional, helps you identify the subscription later (we never send it to your endpoint).
When the subscription is created, Teel shows you a whsec_… signing secret exactly once. Copy it into your verifier’s secret store immediately — there is no way to retrieve it again. Use the Rotate secret action in the dashboard if it’s ever lost or you suspect it’s been exposed; the rotation invalidates the old secret immediately (no overlap window).
Defensive bound. Each subscription multiplies storage on every delivery; the cap prevents accidental fan-out explosion.
Maximum payload size
256 KB (JSON-encoded, after envelope wrapping)
Today’s payout events are low single-digit KB. Anything larger is dropped. If you ever need bigger payloads, fetch the resource by ID from the REST API instead.
Replays per minute per account
5
Soft DoS-via-self guard on POST /api/webhooks/deliveries/{id}/replay. Burst of 5; refills at 5/min. 429 responses include Retry-After.
Any 2xx response within 10 seconds counts as success. We don’t read the body — return whatever shape you like, including an empty 200. Slow responses are aborted at 10s and treated as a transport failure.
A non-2xx response, a connection error, or a timeout triggers a retry. The schedule is exponential:
Attempt
Delay after the previous one
1
(initial)
2
~30 seconds
3
~2 minutes
4
~8 minutes
5
~32 minutes
6 (won’t happen)
—
After 5 attempts the delivery is marked permanently_failed. It stays visible in the dashboard’s Delivery history indefinitely (until pruned, no sooner than 30 days). Use Replay to re-enqueue a fresh attempt at any time.
Webhook delivery is at-least-once. The same Teel-Delivery-Id may arrive more than once if a 2xx response was lost on the wire. Idempotency on your end (e.g. an INSERT … ON CONFLICT (delivery_id) DO NOTHING) is the safest pattern.
Every delivery is HMAC-SHA256-signed over t=<unix>.<raw body> using your subscription’s whsec_… secret. Recompute the digest on your side and constant-time-compare. Reject anything older than 5 minutes — that’s the replay-protection window.
import crypto from "crypto";import express from "express";const WEBHOOK_SECRET = process.env.TEEL_WEBHOOK_SECRET; // whsec_...const TOLERANCE_SECONDS = 5 * 60;const app = express();// IMPORTANT: use express.raw — we need the EXACT bytes Teel signed.app.post( "/teel-webhook", express.raw({ type: "application/json" }), (req, res) => { const header = req.get("Teel-Signature"); if (!header) return res.status(400).send("missing signature"); // Parse `t=<unix>,v1=<hex>` (additional v2/v3 schemes may be added later). const parts = Object.fromEntries( header.split(",").map((p) => p.split("=", 2)) ); const ts = Number.parseInt(parts.t, 10); if (!Number.isFinite(ts)) return res.status(400).send("bad timestamp"); if (Math.abs(Date.now() / 1000 - ts) > TOLERANCE_SECONDS) { return res.status(400).send("stale or future timestamp"); } const expected = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(`${ts}.`) .update(req.body) // raw Buffer .digest("hex"); const got = Buffer.from(parts.v1 ?? "", "hex"); const exp = Buffer.from(expected, "hex"); if (got.length !== exp.length || !crypto.timingSafeEqual(got, exp)) { return res.status(400).send("invalid signature"); } // Signature verified — safe to parse + handle. const event = JSON.parse(req.body.toString("utf8")); const deliveryId = req.get("Teel-Delivery-Id"); // Dedupe on Teel-Delivery-Id at your storage layer here. console.log(`[${event.type}]`, event.data, `delivery=${deliveryId}`); res.status(200).end(); });app.listen(3000);
Frameworks that auto-parse JSON (e.g. Express’s default express.json(), FastAPI body parsing) rewrite the body before your handler runs — the bytes you’d then re-serialize won’t match what we signed. Always read the raw body for signature verification, then parse. The samples above use express.raw() and request.get_data() for exactly this reason.
Fires every time a payout’s status or step changes. Multiple events are emitted per payout as it progresses (pending → processing → completed is at least three updates, plus per-step transitions like compliance_cleared, instructions_sent, collected, converting, settling, delivered).
If you can hold an open connection, the WebSocket delivers the same events with sub-100ms latency. Authenticate in-band with your Auth0 access token:
const ws = new WebSocket("wss://api.teel.finance/ws");ws.onopen = () => { // Send auth as the first message — we close the socket if we don't see it // within 10 seconds. ws.send(JSON.stringify({ type: "auth", token: "YOUR_AUTH0_ACCESS_TOKEN" }));};ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === "payout.status.updated") { // ...same shape as the HTTP webhook payload's `data` field }};
The WebSocket scopes events to your account — you only see events for payouts your business owns.
Within processing, the payout progresses through a fixed sequence of steps. The step field on payout.status.updated reflects the current position:initiated → compliance_cleared → instructions_sent → collected → converting → settling → deliveredSteps are monotonic — they only ever move forward.
A fiat-to-fiat payout from your dashboard generates roughly this sequence of events (some steps fire multiple times depending on the providers involved):
Your verifier dedupes on Teel-Delivery-Id so transient retries (e.g. one of these events 2xx-acknowledged but the response was lost on the wire) don’t double-process.