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.
Register an endpoint
Section titled “Register an endpoint”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.
The delivery
Section titled “The delivery”Trile POSTs a JSON event to your URL with a signature header:
POST /webhooks/trile HTTP/1.1Trile-Signature: t=1718900000,v1=5257a869e7 ... (hex HMAC)Content-Type: application/json
{ "id": "evt_01ARZ3...", "type": "invoice.paid", "data": { "...": "the object" } }Verify the signature
Section titled “Verify the signature”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.
Make handlers idempotent
Section titled “Make handlers idempotent”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.
Manage & debug endpoints
Section titled “Manage & debug endpoints”| Action | Endpoint |
|---|---|
| List endpoints | GET /v1/webhooks/endpoints |
| Inspect delivery attempts | GET /v1/webhooks/endpoints/:id/deliveries |
| Send a test event | POST /v1/webhooks/endpoints/:id/test |
| Replay failed deliveries | POST /v1/webhooks/endpoints/:id/replay |
| Rotate the secret | POST /v1/webhooks/endpoints/:id/rotate-secret |
| Update events / URL / disable | PATCH /v1/webhooks/endpoints/:id |
Use test to wire up your handler, and deliveries to see status codes and bodies when a delivery fails.
Secret rotation without downtime
Section titled “Secret rotation without downtime”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.