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", "enabled_events": ["subscription.created", "invoice.paid", "invoice.payment_failed"] }'import { Configuration, WebhooksApi } from '@trilehq/sdk'
const webhooks = new WebhooksApi(new Configuration({ apiKey: process.env.TRILE_KEY }))
const { data: endpoint } = await webhooks.createWebhookEndpoint({ createWebhookEndpoint: { url: 'https://yourapp.com/webhooks/trile', enabled_events: ['subscription.created', 'invoice.paid', 'invoice.payment_failed'], }, idempotencyKey: crypto.randomUUID(),})// endpoint.secret → shown exactly once, store it nowThe 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 | SDK method |
|---|---|---|
| List endpoints | GET /v1/webhooks/endpoints | listWebhookEndpoints |
| Inspect one endpoint | GET /v1/webhooks/endpoints/:id | getWebhookEndpoint |
| Inspect delivery attempts | GET /v1/webhooks/endpoints/:id/deliveries | listWebhookDeliveries |
| Send a test event | POST /v1/webhooks/endpoints/:id/test | sendTestWebhook |
| Replay failed deliveries | POST /v1/webhooks/endpoints/:id/replay | replayWebhookDeliveries |
| Rotate the secret | POST /v1/webhooks/endpoints/:id/rotate-secret | rotateWebhookSecret |
| Update events / URL / disable | PATCH /v1/webhooks/endpoints/:id | updateWebhookEndpoint |
| Delete an endpoint | DELETE /v1/webhooks/endpoints/:id | deleteWebhookEndpoint |
# List endpointscurl "$TRILE_API/v1/webhooks/endpoints" -H "x-api-key: $TRILE_KEY"
# Inspect delivery attemptscurl "$TRILE_API/v1/webhooks/endpoints/whk_01ARZ3.../deliveries" -H "x-api-key: $TRILE_KEY"
# Send a test eventcurl -X POST "$TRILE_API/v1/webhooks/endpoints/whk_01ARZ3.../test" \ -H "x-api-key: $TRILE_KEY" -H "Idempotency-Key: $(uuidgen)"
# Replay failed deliveriescurl -X POST "$TRILE_API/v1/webhooks/endpoints/whk_01ARZ3.../replay" \ -H "x-api-key: $TRILE_KEY" -H "Idempotency-Key: $(uuidgen)"
# Rotate the secret (new secret returned once)curl -X POST "$TRILE_API/v1/webhooks/endpoints/whk_01ARZ3.../rotate-secret" \ -H "x-api-key: $TRILE_KEY" -H "Idempotency-Key: $(uuidgen)"
# Update events / URL / disablecurl -X PATCH "$TRILE_API/v1/webhooks/endpoints/whk_01ARZ3..." \ -H "x-api-key: $TRILE_KEY" -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "active": false }'
# Delete an endpointcurl -X DELETE "$TRILE_API/v1/webhooks/endpoints/whk_01ARZ3..." \ -H "x-api-key: $TRILE_KEY" -H "Idempotency-Key: $(uuidgen)"// List endpointsconst { data: endpoints } = await webhooks.listWebhookEndpoints({})
// Inspect one endpoint / its delivery attemptsconst { data: ep } = await webhooks.getWebhookEndpoint({ id: 'whk_01ARZ3...' })const { data: deliveries } = await webhooks.listWebhookDeliveries({ id: 'whk_01ARZ3...' })
// Send a test eventawait webhooks.sendTestWebhook({ id: 'whk_01ARZ3...', idempotencyKey: crypto.randomUUID() })
// Replay failed deliveriesawait webhooks.replayWebhookDeliveries({ id: 'whk_01ARZ3...', idempotencyKey: crypto.randomUUID() })
// Rotate the secret (new secret returned once)const { data: rotated } = await webhooks.rotateWebhookSecret({ id: 'whk_01ARZ3...', idempotencyKey: crypto.randomUUID(),})
// Update events / URL / disableawait webhooks.updateWebhookEndpoint({ id: 'whk_01ARZ3...', updateWebhookEndpoint: { active: false }, idempotencyKey: crypto.randomUUID(),})
// Delete an endpointawait webhooks.deleteWebhookEndpoint({ id: 'whk_01ARZ3...', idempotencyKey: crypto.randomUUID() })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.