Webhooks notify your system when shelf state changes.
How webhooks work
Register a webhook endpoint once, then submit as many documents as you need.
Every shelf lifecycle event is automatically delivered to all matching endpoints — you don’t create a new webhook per document.
1. Setup (once) POST /v1/webhooks → register your endpoint
2. Submit documents POST /v1/shelves → upload as many as you need
3. Receive events Your endpoint receives shelf.ready / shelf.failed
for every shelf — use the shelfPublicId in the
payload to correlate events to specific documents
Do not create a webhook on every document submission. This accumulates
endpoints, complicates secret management, and provides no benefit — a single
endpoint already receives events for all your shelves.
Subscribable events
| Event | Description |
|---|
shelf.ready | Shelf processing completed |
shelf.failed | Shelf processing failed |
shelf.review | Shelf paused and awaiting approval |
webhook.ping is a test event triggered only by POST /v1/webhooks/{webhookPublicId}/ping.
Register an endpoint
curl -X POST https://api.shelv.dev/v1/webhooks \
-H "Authorization: Bearer sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/shelv",
"events": ["shelf.ready", "shelf.failed", "shelf.review"]
}'
The signing secret is returned on create and rotate.
Endpoint URL requirements
- Must be an absolute URL without username/password credentials
- In production, must use
https://
- In production, if a port is provided, it must be
443
- Host must resolve
- Resolved IPs cannot be private, loopback, link-local, or special-purpose
localhost, *.localhost, and .local are rejected in production
- In non-production,
http://localhost and http://*.localhost are allowed
Invalid URL checks return 400 with code=VALIDATION_ERROR and details:
{
"code": "VALIDATION_ERROR",
"message": "Webhook URL must use https://",
"details": {
"field": "url",
"reason": "https_required"
}
}
Delivery contract
-
At-least-once delivery
-
Use
webhook-id as idempotency key
-
Verify signature before processing
-
webhook-id
-
webhook-timestamp
-
webhook-signature (v1,<base64-hmac>)
-
webhook-attempt
-
webhook-endpoint-id
Signed payload format:
{webhook-id}.{webhook-timestamp}.{raw_request_body}
Signature verification example
import { createHmac, timingSafeEqual } from "node:crypto";
const MAX_AGE_SECONDS = 300;
function verify(
body: string,
id: string,
timestamp: string,
signature: string,
secret: string,
): boolean {
const [version, received] = signature.split(",");
if (version !== "v1" || !received) return false;
const ts = Number(timestamp);
if (!Number.isFinite(ts)) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - ts);
if (age > MAX_AGE_SECONDS) return false;
const signed = `${id}.${timestamp}.${body}`;
const expected = createHmac("sha256", secret).update(signed).digest("base64");
const expectedBuf = Buffer.from(expected);
const receivedBuf = Buffer.from(received);
if (expectedBuf.length !== receivedBuf.length) return false;
return timingSafeEqual(expectedBuf, receivedBuf);
}
Handler checklist:
- Read raw request body
- Verify signature and enforce timestamp freshness window
- Deduplicate by
webhook-id
- Process event
Minimal operations
List endpoints
curl https://api.shelv.dev/v1/webhooks \
-H "Authorization: Bearer sk_your_api_key"
Rotate secret
curl -X POST https://api.shelv.dev/v1/webhooks/{webhookPublicId}/rotate-secret \
-H "Authorization: Bearer sk_your_api_key"
Send test event
curl -X POST https://api.shelv.dev/v1/webhooks/{webhookPublicId}/ping \
-H "Authorization: Bearer sk_your_api_key"
For delivery history and redelivery endpoints, use the API Reference tab.