API webhooks
Work appears mid-day by design: second comments unlock six hours after the first, and scout finds land whenever a scan returns. A signed POST tells you the moment it does.
Last updated July 24, 2026Polling a fleet is either late or wasteful, because new work does not arrive on your schedule: the warmer’s mid-day comment unlock and the scout’s scan results both land whenever they land. The webhook is a push that says “this account has work now”, and nothing more; your script then pulls the details over the API it already uses.
01 · SetupSetup
On the API & Webhooks page (account menu), add one https:// endpoint for your account. Saving it mints a signing secret (nk_whsec_...) that you can reveal and copy there any time. From the same card you can pause and resume deliveries, rotate the secret, and send a webhook.test event to prove the wiring end to end. Every attempt, real or test, lands in the delivery log with its status, so “did it send?” is never a mystery.
02 · EventsThe events
Two real events exist, one per engine, both meaning “new tasks are ready to pull”:
warmup.tasks_ready: the morning batch for a warm-up, and again mid-day when a held second comment unlocks.scout.tasks_ready: a scan just added conversations to a scout’s inbox.
{
"event": "warmup.tasks_ready",
"warmup_id": "3f2b7c1e-9d5a-4f42-8f6e-2a91d0c47b13",
"task_count": 2,
"at": "2026-07-26T08:00:14.000Z"
}The scout event mirrors it with scout_id; webhook.test carries just event and at. On receipt, fetch the actual work: GET /v1/tasks?warmup_id=...&status=ready (see the API task loop).
03 · VerificationVerify the signature
Every delivery carries X-Nilkick-Signature, an HMAC-SHA256 of the exact body bytes using your signing secret, formatted sha256=<hex>. Verify it before trusting the event, or anyone who learns your endpoint can spoof “you have work” and wake your fleet:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyNilkick(rawBody, header, secret) {
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
return (
typeof header === 'string' &&
header.length === expected.length &&
timingSafeEqual(Buffer.from(header), Buffer.from(expected))
);
}Compute it over the raw request body, not a re-serialized copy of the parsed JSON: any framework that hands you parsed objects first must also expose the original bytes.
04 · ReliabilityDeliveries are sent once
A delivery is attempted once, with a ten second timeout, and never retried. If your endpoint happens to be down at that moment, the event is gone (the attempt is still visible in the delivery log). Treat webhooks as an accelerator, not as the source of truth.
The safety net is one reconcile poll per account per day: a plain GET /v1/tasks?status=ready at a fixed hour catches anything a missed event would have told you about, and turns a lost delivery into a delay instead of a silently missed day. On a 28-day warm-up curriculum, that one call a day is the difference between “late” and “gone”.