Skip to main content
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

EventDescription
shelf.readyShelf processing completed
shelf.failedShelf processing failed
shelf.reviewShelf 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:
  1. Read raw request body
  2. Verify signature and enforce timestamp freshness window
  3. Deduplicate by webhook-id
  4. 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.