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

EventWhen it fires
signal.createdThe signal-agent emits a signal that clears thresholds
signal.invalidatedA previously emitted signal hits its invalidation level
catalyst.detectedEvent Catalyst classifies a new event
catalyst.updatedSentiment or magnitude changes on a tracked event
swap.confirmedA user’s swap lands and is finalized
swap.failedA swap is rejected or fails post-submission
agentic.intentAn agentic strategy submits an intent (informational)
agentic.executedAn agentic intent is executed by SAE
points.credited / points.reversedPoints 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'))
  );
}
Treat unverified payloads as forged

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.

For Developers

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_at rather 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

Last updated: