Skip to content

Webhooks

Webhooks send each entry of the admin audit trail (the Audit Log page) to a URL you control the moment it is recorded: client created, updated or deleted, secret rotated, registration approved or rejected, change set applied, and so on.

Manage them under Settings > Integrations > Webhooks. That page is for instance admins and organization owners and admins. Webhooks belong to the organization they were created in.

Give the URL, optionally a description, and choose All events or particular ones. The signing secret is shown once, when you add the webhook: copy it then. Send test posts a webhook.test event and shows the outcome. You can switch a webhook off, or delete it, at any time. Up to 10 per organization.

The URL must use https and may not contain credentials. By default Taco refuses to send to private, loopback or link-local addresses (checked again when connecting, so a host can’t change its answer in between) and never follows redirects.

A POST with a JSON body:

{
"id": "0d4f…",
"type": "client.secret.rotate",
"created_at": "2026-10-02T10:00:00.000Z",
"organization_id": "org_…",
"status": "success",
"actor": { "id": "usr_…", "email": "admin@example.com" },
"target": { "type": "client", "id": "abc123" },
"details": {}
}

type is the audit action (client.create, client.secret.rotate, registration.approve…; the Audit Log filter lists them), status is success or failure. The admin’s IP address and user agent are not sent.

Headers: X-Taco-Event, X-Taco-Delivery (unique per delivery), X-Taco-Timestamp (Unix seconds) and X-Taco-Signature.

The signature is v1= followed by the hex HMAC-SHA256, keyed with your signing secret, of <timestamp>.<raw body>. Recompute it on the raw body, compare in constant time, and reject old timestamps to stop replays.

import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(secret, headers, rawBody) {
const expected = `v1=${createHmac('sha256', secret)
.update(`${headers['x-taco-timestamp']}.${rawBody}`)
.digest('hex')}`
const given = Buffer.from(headers['x-taco-signature'] ?? '')
return (
given.length === expected.length &&
timingSafeEqual(given, Buffer.from(expected))
)
}

Any 2xx response counts as delivered. Otherwise (or on a timeout of 10 seconds) Taco retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then gives up after 6 attempts. Retries run from a scheduled task every minute (a cron trigger on Cloudflare). A delivery can arrive more than once: use X-Taco-Delivery to ignore repeats. Deliveries are kept 30 days; the settings page shows the recent ones.