Skip to content

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.

POST /v1/watches.

Request
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"
]
}'
Response 201
{
"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.

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.

Each delivery is a POST of a scored event to your URL. See the scoredEvent webhook for the full body.

Request
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"
}
}'
Response
HTTP/1.1 200

Answer 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.

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.

  1. Read the raw body before parsing JSON.
  2. Compute HMAC-SHA256 of t + "." + body with your secret.
  3. Accept if any v1 matches (several appear while a secret rotation overlaps) and t is 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);
});
}

GET /v1/watches, GET /v1/watches/{id} and DELETE /v1/watches/{id}. All free.