Watches and webhooks
A watch is a standing request: “send me scored events for this ICP above this probability”. Verdict scores each new event in the index against your ICP and posts the ones that clear min_p to your URL.
Creating, listing and deleting watches is free. Each delivered event bills one score_signal, $0.005. Retries are never billed. Scope watches:manage.
Create a watch
Section titled “Create a watch”curl -X POST 'https://api.askverdict.io/v1/watches' \ -H "Authorization: Bearer $VERDICT_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H 'Content-Type: application/json' \ -d '{ "icp_id": "icp_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "webhook_url": "https://agent.northwind-ops.io/hooks/verdict", "min_p": 0.7, "types": [ "filing", "fund_raise", "officer_change" ]}'{ "id": "wch_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "project_id": "prj_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "icp_id": "icp_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "webhook_url": "https://agent.northwind-ops.io/hooks/verdict", "min_p": 0.7, "types": [ "filing", "fund_raise", "officer_change" ], "status": "pending_verification", "created_at": "2026-11-07T14:10:00Z", "updated_at": "2026-11-07T14:10:00Z", "signing_secret": "whsec_ExampleExampleExampleExampleExample"}The response includes signing_secret (whsec_...). It is shown once, like an API key. Store it to verify deliveries.
| Field | Meaning |
|---|---|
icp_id |
The ICP to score events against. |
webhook_url |
Your HTTPS endpoint. Private and link-local addresses are refused. |
min_p |
Deliver events whose relevant is at least this. |
types |
Event types to watch. |
The challenge
Section titled “The challenge”Right after creation, Verdict sends a watchChallenge request to your URL. Answer 200 with {"challenge": <challenge>} within 10 seconds. The watch becomes active only then. The challenge is signed like every delivery.
Deliveries
Section titled “Deliveries”Each delivery is a POST of a scored event to your URL. See the scoredEvent webhook for the full body.
curl -X POST 'https://your-app.example.com/verdict/webhook' \ -H 'Verdict-Signature: t=1794060000,v1=5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e' \ -H 'Content-Type: application/json' \ -d '{ "id": "dlv_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "type": "event.scored", "created_at": "2026-11-07T15:00:03Z", "watch_id": "wch_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "event": { "id": "evt_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "type": "filing", "firm_id": "frm_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "occurred_at": "2026-11-03T21:15:00Z", "observed_at": "2026-11-04T02:05:00Z", "source": "sec_edgar", "source_url": "https://www.sec.gov/Archives/edgar/data/1601234/000160123426000004/xslFormDX01/primary_doc.xml", "confidence": 0.97, "attributes": { "filing_type": "sec_form_d", "accession_number": "0001601234-26-000004", "title": "Notice of Exempt Offering of Securities" }, "score": { "decision_id": "dec_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "icp_id": "icp_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "relevant": 0.78, "act_now": 0.64, "window_days": 21, "answer": "relevant", "reason_code": "event_relevant_and_timely" } }, "score": { "decision_id": "dec_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "icp_id": "icp_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "relevant": 0.78, "act_now": 0.64, "window_days": 21, "answer": "relevant", "reason_code": "event_relevant_and_timely" }}'HTTP/1.1 200Answer any 2xx within 10 seconds. Anything else is retried with exponential backoff for 24 hours. Dedupe on the delivery id. Check past deliveries and their status with GET /v1/watches/{id}/deliveries.
Verify the signature
Section titled “Verify the signature”Every request carries Verdict-Signature: t=<unix seconds>,v1=<hex>. The v1 value is HMAC-SHA256 of <t>.<raw body> keyed with the watch’s signing secret.
- Read the raw body before parsing JSON.
- Compute HMAC-SHA256 of
t + "." + bodywith your secret. - Accept if any
v1matches (several appear while a secret rotation overlaps) andtis within 5 minutes of your clock.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(header: string, rawBody: string, secret: string): boolean { const parts = header.split(","); const t = parts.find((p) => p.startsWith("t="))?.slice(2) ?? ""; if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest(); return parts .filter((p) => p.startsWith("v1=")) .some((p) => { const got = Buffer.from(p.slice(3), "hex"); return got.length === expected.length && timingSafeEqual(got, expected); });}Manage watches
Section titled “Manage watches”GET /v1/watches, GET /v1/watches/{id} and DELETE /v1/watches/{id}. All free.