Documentation Index
Fetch the complete documentation index at: https://docs.signa.so/llms.txt
Use this file to discover all available pages before exploring further.
Setup
A webhook endpoint is a URL you control that receives signed POSTs from
Signa whenever a subscribed event fires. To set one up:
- Build the receiver. It must accept
POST application/json and
verify the Standard Webhooks headers.
- Register it.
POST /v1/webhooks
returns the signing secret once — store it before the response is
discarded.
- Verify. Use the SDK helper
verifyWebhookSignature (see the
signature verification guide)
or any Standard Webhooks-compatible
library.
Event types
In v1, alert.created is the only event type a customer can subscribe to via
enabled_events on POST /v1/webhooks
or PATCH /v1/webhooks/{id}. Any
other slug is rejected with a 400.
| Event type | When it fires |
|---|
alert.created | A watch matched a trademark; one delivery per Alert row. |
webhook.test (not subscribable)
webhook.test is not something you subscribe to — it is delivered
automatically (and only) when you invoke
POST /v1/webhooks/{id}/test. The
envelope shape matches alert.created (type, id, timestamp, data)
but data is a fixed { "type": "ping" } payload rather than a real alert.
Test deliveries are never retried and never count toward the auto-disable
counters (VAL-WEBHOOK-007), so probing a dead endpoint with /test is safe.
Payload shape
Every delivery has the same outer envelope (WebhookEvent in the SDK):
{
"type": "alert.created",
"id": "alt_018f9b2e-9b6c-7c9c-b4f1-1234567890ab",
"timestamp": "2026-05-08T14:32:11.428Z",
"data": { /* event-specific — see below */ }
}
| Field | Notes |
|---|
type | The event slug (matches the value you subscribed via enabled_events). |
id | Prefixed event ID — same value as the webhook-id header. For alert.created this is the alert’s prefixed ID (alt_*). |
timestamp | ISO 8601 UTC instant captured at signing — fresh on every retry. |
data | Event-specific payload object. |
alert.created data fields
data for alert.created matches the AlertCreatedPayload SDK type
(import type { AlertCreatedEvent } from '@signa-so/sdk'). All IDs are
prefixed and feed straight into the corresponding REST resources (no
conversion needed):
| Field | Type | Description |
|---|
alert_id | string (alt_*) | Pass to GET /v1/alerts/{id} for the full alert detail. |
watch_id | string (wat_*) | The watch that fired. Pass to GET /v1/watches/{id}. |
trademark_record_id | string (tm_*) | The matched trademark. Pass to GET /v1/trademarks/{id}. |
event_type | 'trademark.created' | 'trademark.updated' | 'trademark.status_changed' | Why the alert fired. |
evaluation_epoch | number | Increments on /replay — useful for distinguishing post-replay alerts. |
content_version | number | Snapshot of the trademark’s version at evaluation time. |
severity | 'normal' | 'high' | 'critical' | Severity ranking — drives notification routing on your side. |
must_act_by | string | null | ISO 8601 deadline (e.g. opposition window close); null when no deadline applies. |
opposition_window_status | 'open' | 'closing_soon' | 'critical' | 'closed' | null | Current state of the opposition window for this mark; null when no window applies. |
source_data_hash | string | null | Hash of the source payload that triggered the alert (best-effort audit aid). |
The webhook payload deliberately does not include any tenant
identifier — your endpoint URL is per-tenant by construction, so the
delivery’s tenant is implied by which endpoint received the POST.
Signing
The dispatcher signs every delivery using HMAC-SHA256 per the
Standard Webhooks spec. Three headers:
| Header | Meaning |
|---|
webhook-id | Stable event identifier — same value across retries and redeliveries. For alert.created deliveries this is the alert’s prefixed ID (alt_*). Use it as your application-level idempotency key so retries don’t double-process the underlying business event. |
webhook-timestamp | Unix seconds at delivery attempt time. Fresh on every retry. Reject deliveries older than 5 minutes (the SDK helper does this for you). |
webhook-signature | v1,<base64-HMAC>. During rotation, two SPACE-separated entries: v1,<curr> v1,<prev>. |
webhook-attempt | Delivery attempt number (1 = first delivery, 2 = first retry, …, up to 7). Use alongside webhook-id if you need infrastructure-layer dedup that distinguishes attempts. Not signed — see “Idempotency” below. |
The body is canonicalised JSON (sorted keys, UTF-8, no trailing newline)
before signing — see workers/webhook-dispatcher/src/signing.ts for the
exact bytes-on-the-wire contract.
Retry policy
Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|
| 1 | immediate |
| 2 | +5s |
| 3 | +25s |
| 4 | +2 min |
| 5 | +15 min |
| 6 | +1 h |
| 7 | +6 h (terminal) |
Each delay carries ±20% jitter to spread retry bursts across receivers.
A delivery is “failed” if the receiver returns 4xx/5xx, times out (5s
connect, 10s read), or refuses TLS. After attempt 7 the delivery row’s
status is set to exhausted and the dispatcher gives up.
The terminal webhook_deliveries.status values are:
| Status | Meaning |
|---|
pending | Queued or scheduled for retry; not yet a final outcome. |
delivered | Receiver returned 2xx. |
failed | Last attempt failed but more retries remain. |
exhausted | All 7 attempts failed. The dispatcher will not retry this row again — replay manually via POST /v1/webhooks/{id}/deliveries/{did}/redeliver. |
Auto-disable
The dispatcher disables a webhook endpoint when either of two
disjunctive triggers fires. Either one flips status to disabled and
the dispatcher stops attempting deliveries to it.
- Threshold A — consecutive failures. When
consecutive_failures
reaches 100, the endpoint is disabled.
- Threshold B — rolling failure rate. Over the rolling window of the
last 50 attempts, if the failure rate exceeds 50%, the endpoint
is disabled. (Trigger B only evaluates once the window is full, so a
single failure on a brand-new endpoint won’t disable it.)
A long-tail flaky endpoint can hit B without ever hitting A; a short
hard-down outage can hit A first. Both triggers run on every attempt.
Re-enable with
PATCH /v1/webhooks/{id}
once you’ve fixed the receiver — the consecutive-failure counter resets
to 0 on the next successful delivery.
webhook.test deliveries do not increment either counter
(VAL-WEBHOOK-007), so probing a dead endpoint with the test endpoint is
safe.
Rotation overlap
POST /v1/webhooks/{id}/rotate-secret
returns a new secret and bumps secret_version. For 24 hours the
dispatcher signs every delivery with both secrets — webhook-signature: v1,<new> v1,<old> — so customers can roll the key without a maintenance window.
The reference Standard Webhooks library iterates space-separated entries
and accepts any that verifies, so your verifier needs no changes during
the overlap.
Calling rotate-secret again while the previous-secret window is still
active returns 409. This protects against rapid-double-rotate
incidents where an admin’s “did the first rotation apply?” reflex
silently breaks every receiver still on the previous key.
Emergency force rotation
For a suspected secret leak mid-overlap, pass force=true (in the
request body or as ?force=true on the URL). Force-rotation:
- Skips the 24h overlap window check (no 409).
- Immediately invalidates the previous secret — any receiver still
using it will fail signature verification on the next delivery.
- Writes a
webhook.secret.force_rotated audit log entry, capturing the
optional reason you supply.
Use force rotation only when the previous secret is known or suspected
to be compromised. For routine rotations, wait for the overlap window
to close.
Redelivery
If your receiver is down for a stretch and the deliveries land in
status: "exhausted", manually replay them from
POST /v1/webhooks/{id}/deliveries/{did}/redeliver.
The redelivery carries a fresh webhook-timestamp (passes freshness
checks) but the same webhook-id — so your dedup logic continues
to work.
SSRF protection
The dispatcher blocks deliveries to non-routable IPs (RFC 1918, link-local,
169.254.169.254, etc.). Localhost and private CIDRs are rejected by the
producer-side validator on
POST /v1/webhooks in
production, and by the dispatcher at delivery time as defense-in-depth.
Cost model
Webhooks are billed per successful delivery and per redelivery. Test
deliveries are free.