Skip to main content

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:
  1. Build the receiver. It must accept POST application/json and verify the Standard Webhooks headers.
  2. Register it. POST /v1/webhooks returns the signing secret once — store it before the response is discarded.
  3. 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 typeWhen it fires
alert.createdA 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 */ }
}
FieldNotes
typeThe event slug (matches the value you subscribed via enabled_events).
idPrefixed event ID — same value as the webhook-id header. For alert.created this is the alert’s prefixed ID (alt_*).
timestampISO 8601 UTC instant captured at signing — fresh on every retry.
dataEvent-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):
FieldTypeDescription
alert_idstring (alt_*)Pass to GET /v1/alerts/{id} for the full alert detail.
watch_idstring (wat_*)The watch that fired. Pass to GET /v1/watches/{id}.
trademark_record_idstring (tm_*)The matched trademark. Pass to GET /v1/trademarks/{id}.
event_type'trademark.created' | 'trademark.updated' | 'trademark.status_changed'Why the alert fired.
evaluation_epochnumberIncrements on /replay — useful for distinguishing post-replay alerts.
content_versionnumberSnapshot of the trademark’s version at evaluation time.
severity'normal' | 'high' | 'critical'Severity ranking — drives notification routing on your side.
must_act_bystring | nullISO 8601 deadline (e.g. opposition window close); null when no deadline applies.
opposition_window_status'open' | 'closing_soon' | 'critical' | 'closed' | nullCurrent state of the opposition window for this mark; null when no window applies.
source_data_hashstring | nullHash 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:
HeaderMeaning
webhook-idStable 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-timestampUnix seconds at delivery attempt time. Fresh on every retry. Reject deliveries older than 5 minutes (the SDK helper does this for you).
webhook-signaturev1,<base64-HMAC>. During rotation, two SPACE-separated entries: v1,<curr> v1,<prev>.
webhook-attemptDelivery 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:
AttemptDelay
1immediate
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:
StatusMeaning
pendingQueued or scheduled for retry; not yet a final outcome.
deliveredReceiver returned 2xx.
failedLast attempt failed but more retries remain.
exhaustedAll 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.