Webhooks#
curl https://api.nolvin.com/v1/domains/$DOMAIN/webhooks -H "Authorization: Bearer $NOLVIN_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/nolvin", "events": ["crawl.finished", "budget.hit"]}'
{"id": "…", "secret": "whsec_…", "events": ["crawl.finished","budget.hit"]}
Store the secret — it is shown once (rotate with POST …/webhooks/{id}/rotate-secret). Nolvin POSTs JSON:
{"id": "<delivery id>", "type": "crawl.finished", "domain_id": "…",
"data": {"runId": "…", "pages": 42, "unchanged": 31, "deterministic": 2, "agentic": 9, "failed": 0, "entities_added": 17}}
| Event | When | data |
|---|---|---|
crawl.finished | A crawl run completed. | runId, pages, unchanged, deterministic, agentic, failed, entities_added |
budget.hit | The monthly spend cap was reached and the domain paused. | spend, cap |
budget.hit can arrive more than once while a domain stays over its cap.
Verifying signatures#
Each delivery has an X-Nolvin-Signature header: the hex SHA-256 of the secret, a dot, and the raw request body. Compute it over the raw bytes before parsing, and compare in constant time.
import { createHash, timingSafeEqual } from "node:crypto";
export function verifyNolvin(rawBody, signature, secret) {
const expected = createHash("sha256").update(`${secret}.${rawBody}`).digest("hex");
return signature?.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Retries and idempotency#
Any 2xx response counts as delivered. Otherwise Nolvin retries twice more with a short backoff (10-second timeout per attempt); deliveries interrupted mid-flight are re-driven by the scheduler. The delivery id stays the same across retries — use it to ignore duplicates. GET …/webhooks/{id}/deliveries shows the last 50 attempts with status and error, and POST …/webhooks/{id}/test sends a test event.
Polling events#
GET /v1/domains/{domainId}/events/poll?since=0
{"events": [{"id": 9812, "type": "page.update", "payload": {"url": "…", "status": "extracted", "mode": "agentic"}, "created_at": "…"}],
"cursor": 9812}
Pass the returned cursor as since next time. Up to 200 events per call. Event types: run.update, page.update, crawl.finished, budget.hit. Events are retained for a limited window — poll at least daily if you rely on them.
Server-sent events#
curl -N https://api.nolvin.com/v1/domains/$DOMAIN/events -H "Authorization: Bearer $NOLVIN_KEY"
id: 9813
event: page.update
data: {"url":"https://…","page_id":"…","status":"extracted","mode":"unchanged","error":null}
The same events as a stream. The server closes it after about four minutes; reconnect with the Last-Event-ID header (browsers' EventSource does this automatically). A browser EventSource cannot send an Authorization header, so stream from your server. Agent activity across the project streams from GET /v1/agents/events.