Skip to content

Webhooks

Webhooks push events to your server in near-real-time so you can react to billing without polling. Because the wallet model means renewals can fail (empty balance), webhooks are how you learn about past_due and recovery.

Terminal window
curl -s "$TRILE_API/v1/webhooks/endpoints" \
-H "x-api-key: $TRILE_KEY" -H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/trile",
"enabledEvents": ["subscription.created", "invoice.paid", "invoice.payment_failed"]
}'

The response includes the signing secret exactly once:

{ "id": "whk_01ARZ3...", "url": "...", "secret": "whsec_...", "enabledEvents": ["..."] }

Store the secret securely — you need it to verify every delivery. If you lose it, rotate with POST /v1/webhooks/endpoints/:id/rotate-secret.

Trile POSTs a JSON event to your URL with a signature header:

POST /webhooks/trile HTTP/1.1
Trile-Signature: t=1718900000,v1=5257a869e7 ... (hex HMAC)
Content-Type: application/json
{ "id": "evt_01ARZ3...", "type": "invoice.paid", "data": { "...": "the object" } }

The header is t=<unix-seconds>,v1=<hex>. Compute HMAC-SHA256( secret, "<t>.<raw-request-body>" ) over the raw bytes (not a re-serialized object) and compare in constant time. Reject if the timestamp is older than 5 minutes.

import crypto from "node:crypto";
function verifyTrile(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const t = parts["t"];
const sig = parts["v1"];
if (!t || !sig) return false;
// Reject stale deliveries (replay protection).
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(sig);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

After verifying, respond 2xx quickly and do slow work asynchronously. Trile retries non-2xx responses with exponential backoff.

Deliveries can repeat (retries, at-least-once delivery). Dedupe on the event id (evt_…): process it once, ignore repeats. Pair this with the event log — periodically sweep /v1/events to catch anything that never delivered.

ActionEndpoint
List endpointsGET /v1/webhooks/endpoints
Inspect delivery attemptsGET /v1/webhooks/endpoints/:id/deliveries
Send a test eventPOST /v1/webhooks/endpoints/:id/test
Replay failed deliveriesPOST /v1/webhooks/endpoints/:id/replay
Rotate the secretPOST /v1/webhooks/endpoints/:id/rotate-secret
Update events / URL / disablePATCH /v1/webhooks/endpoints/:id

Use test to wire up your handler, and deliveries to see status codes and bodies when a delivery fails.

The signature header can carry multiple v1= values during rotation, so deliveries stay valid while you roll the secret. Rotate, deploy the new secret, then retire the old one.