Webhooks
Outbound webhooks for signals, catalysts, swap events, and agentic actions — with HMAC signing and replay.
What this is
TRUE pushes events to your endpoint instead of forcing you to poll. Webhooks are signed with HMAC-SHA256, retried with exponential backoff, and idempotent via stable dedup keys. Use them whenever you need real-time notification — signals, catalysts, swap events, agentic actions.
Registration
POST https://app.truefinance.ai/api/v1/webhooks curl -X POST https://app.truefinance.ai/api/v1/webhooks \
-H "Authorization: Bearer $TRUE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/true",
"events": ["signal.created", "catalyst.detected", "swap.confirmed"],
"secret": "whsec_...",
"filter": { "assets": ["SOL", "ETH"], "min_conviction": 0.6 }
}'
The secret is set by you and used to compute the HMAC signature on every delivery. Keep it server-side.
Event types
| Event | When it fires |
|---|---|
signal.created | The signal-agent emits a signal that clears thresholds |
signal.invalidated | A previously emitted signal hits its invalidation level |
catalyst.detected | Event Catalyst classifies a new event |
catalyst.updated | Sentiment or magnitude changes on a tracked event |
swap.confirmed | A user’s swap lands and is finalized |
swap.failed | A swap is rejected or fails post-submission |
agentic.intent | An agentic strategy submits an intent (informational) |
agentic.executed | An agentic intent is executed by SAE |
points.credited / points.reversed | Points balance changes |
Payload shape
{
"id": "wh_evt_01JX...",
"type": "signal.created",
"created_at": "2026-04-29T12:34:56Z",
"dedup_key": "signal-SOL-20260429-12-34",
"data": { /* event-specific shape */ }
}
Signature verification — mandatory
Every delivery includes two headers:
X-True-Signature: t=1745924712,v1=5d4..., v1=...
X-True-Delivery-Id: wh_evt_01JX...
The signature is HMAC-SHA256 over {timestamp}.{raw_body} using your registered secret. The header may carry multiple v1= values to support secret rotation.
// Node — verify a delivery
import crypto from 'node:crypto';
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(',').map(p => p.split('=', 2)) as [string, string][]
);
const ts = parts.t;
const sigs = header.split(',').filter(p => p.startsWith('v1=')).map(p => p.slice(3));
if (!ts || sigs.length === 0) return false;
// Reject if older than 5 minutes — replay protection.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
return sigs.some(s =>
crypto.timingSafeEqual(Buffer.from(s, 'hex'), Buffer.from(expected, 'hex'))
);
}
Never trust a webhook payload without verifying the signature. An unverified payload is no different from anonymous user input. On signature mismatch: log the delivery ID, return 400, and do not act on the body. Webhook secrets must be stored server-side; never embed them in client-side bundles.
Retry policy
Failed deliveries (any non-2xx response or timeout) are retried with exponential backoff:
Attempt: 1 2 3 4 5 6 7
Wait (s): 0 10 60 300 900 3600 14400
After 7 failed attempts the delivery is marked dead and the webhook is paused. Pausing fires a webhook.paused notification email to the registered contact. Replay individual deliveries from the dashboard or via the replay endpoint.
Replay endpoint
POST https://app.truefinance.ai/api/v1/webhooks/{id}/replay { "delivery_ids": ["wh_evt_01JX...", "wh_evt_01JY..."] }
Replay attempts re-fire the same payload with the original timestamp and a new X-True-Delivery-Id. Idempotency on your side is your responsibility — use the dedup_key in the payload.
Idempotency
dedup_key is stable across all deliveries (initial + retries + replays) for the same logical event. Persist seen dedup keys for 24h to drop duplicates.
Local development
For local testing, register a webhook against a tunneled URL (ngrok, localtunnel) and use the dashboard’s Trigger test event action. Test events carry livemode: false in the payload.
Safety, limits, failure modes
- Out-of-order delivery can happen during retries. Use
created_atrather than receipt order to reconstruct sequence. - Timeouts. Your handler should respond within 5 seconds. Long-running work goes onto a queue.
- Secret rotation. Provision the new secret first, accept both during the window, retire the old one.
- Paused webhooks stop receiving new events. Replay backlog after re-enabling.
See also
- Signals — the most subscribed event type.
- Event Catalyst — catalyst payload shape.
- Authentication — required scope:
subscribe:webhooks.