Skip to main content

Webhooks

Webhooks let you receive HTTP notifications when shelf processing events occur, so you don’t need to poll the API.

Events

EventDescription
shelf.readyShelf processing completed successfully
shelf.failedShelf processing failed
shelf.reviewShelf is ready for review (review mode enabled)

Register a Webhook

curl -X POST https://api.shelv.dev/api/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"]
  }'
Response:
{
  "id": "w1b2c3d4-...",
  "url": "https://your-app.com/webhooks/shelv",
  "events": ["shelf.ready", "shelf.failed"],
  "secret": "whsec_abc123...",
  "createdAt": "2025-01-15T10:30:00.000Z"
}
The secret is only returned when the webhook is created. Store it securely — you’ll need it to verify signatures.

Webhook Payload

When an event fires, Shelv sends a POST request to your URL:
{
  "event": "shelf.ready",
  "timestamp": "2025-01-15T10:35:00.000Z",
  "data": {
    "shelfId": "a1b2c3d4-...",
    "userId": "user_...",
    "status": "ready",
    "errorMessage": null
  }
}
For shelf.failed events, errorMessage contains the failure reason.

Verify Signatures

Every webhook request includes signature headers. Verify them with your webhook secret:
  • webhook-id — unique event delivery ID
  • webhook-timestamp — Unix timestamp (seconds)
  • webhook-signature — signed value in the format v1,<base64-hmac>
The signed payload format is:
{webhook-id}.{webhook-timestamp}.{raw_request_body}
Example verifier:
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhookSignature(
  payload: string,
  webhookId: string,
  webhookTimestamp: string,
  webhookSignature: string,
  secret: string,
): boolean {
  const [version, received] = webhookSignature.split(",");
  if (version !== "v1" || !received) return false;

  const signed = `${webhookId}.${webhookTimestamp}.${payload}`;
  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);
}

// In your webhook handler
const payload = await request.text();
const webhookId = request.headers.get("webhook-id") ?? "";
const webhookTimestamp = request.headers.get("webhook-timestamp") ?? "";
const webhookSignature = request.headers.get("webhook-signature") ?? "";

const isValid = verifyWebhookSignature(
  payload,
  webhookId,
  webhookTimestamp,
  webhookSignature,
  WEBHOOK_SECRET,
);

Manage Webhooks

List Webhooks

curl https://api.shelv.dev/api/webhooks \
  -H "Authorization: Bearer sk_your_api_key"

Delete a Webhook

curl -X DELETE https://api.shelv.dev/api/webhooks/{id} \
  -H "Authorization: Bearer sk_your_api_key"

Rotate Signing Secret

If a secret is compromised, rotate it:
curl -X POST https://api.shelv.dev/api/webhooks/{id}/rotate-secret \
  -H "Authorization: Bearer sk_your_api_key"
The old secret is invalidated immediately. Update your webhook handler with the new secret.