Developers
Outgoing webhooks
Outgoing webhooks push a signed HTTP POST to a URL you control whenever a log is created, updated, or deleted in one of the channels you subscribe to. This page describes what your endpoint receives — webhooks themselves are set up from within Trailogs.
Overview
An outgoing webhook is created from within Trailogs and subscribes to one or more channels in a workspace. Every matching event is delivered asynchronously as its own HTTP request, signed with a per-webhook secret so your endpoint can verify it actually came from Trailogs.
Deliveries are sent directly from Trailogs' servers to the URL you configured — they aren't authenticated with an API token, only with the webhook's signing secret (see below).
Events
| Event | Fires when |
|---|---|
| log.created | A log is created in a subscribed channel. |
| log.updated | A log in a subscribed channel is updated. |
| log.deleted | A log in a subscribed channel is deleted. |
A webhook only receives events for the channels it's subscribed to, and only while it's enabled. Your endpoint should also expect a one-off url.verification request — see URL verification below.
Payload & signatures
Every delivery is a POST to your URL with a JSON body in the following shape. For log.deleted, data.log only contains the log's id; for the other two events it's the full log object.
{
"id": "outgoing_webhook_event_uuid",
"event": "log.created",
"workspace_id": "workspace_uuid",
"data": {
"log": {
"id": "log_uuid",
"title": "Deployed payments-service v2.3.1",
"occurred_at": 1690000000,
"category": { "id": "uuid", "name": "Deployment", "color": "blue" },
"channel": { "id": "uuid", "name": "deployments" }
/* ...same shape as a log returned by the main API */
}
}
}Headers
| Field | Type | Required | Description |
|---|---|---|---|
| X-Trailogs-Signature | string | Optional | sha256=<hex hmac>, see below. |
| X-Trailogs-Request-Timestamp | integer | Optional | Unix timestamp the request was signed at. |
| User-Agent | string | Optional | Always Trailogs-Webhook/1.0. |
| Content-Type | string | Optional | Always application/json. |
Verifying the signature
Compute an HMAC-SHA256 over {timestamp}.{raw request body} using your webhook's secret, and compare it to the signature header using a constant-time comparison. Sign the raw bytes of the body — re-serializing the parsed JSON before verifying can produce a different string and fail verification. Reject requests whose timestamp is more than a few minutes old to guard against replay.
const crypto = require('crypto');
function isValid(rawBody, timestamp, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}URL verification
Before a webhook starts receiving real events, Trailogs confirms you control the URL by sending a one-time challenge — the same pattern Slack uses for event subscriptions. This happens automatically when a webhook is created or its URL is changed in Trailogs; your endpoint just needs to be able to respond to it.
{
"event": "url.verification",
"challenge": "a1b2c3d4..."
}Respond within 5 seconds with a 2xx status and echo the challenge back exactly:
{
"challenge": "a1b2c3d4..."
}This request isn't signed the way event deliveries are, since no webhook secret exists until verification succeeds. If your endpoint doesn't respond correctly, the webhook can't be activated in Trailogs.
Retries & delivery
Deliveries are queued and sent asynchronously, so they may arrive slightly after the triggering action completes. A delivery is attempted up to 3 times, waiting 30s, then 60s, then 300s between attempts, and times out after 10 seconds. Any non-2xx response, timeout, or network error counts as a failure and triggers a retry.
Respond as soon as you've durably queued the event for your own processing — there's no need to finish handling it before returning a 2xx. Slow endpoints risk hitting the 10 second timeout and receiving duplicate deliveries on retry, so handlers should be idempotent (keying off the top-level id field is a safe way to de-duplicate).