Webhooks
Webhooks let you receive HTTP notifications when shelf processing events occur, so you don’t need to poll the API.
Events
| Event | Description |
|---|
shelf.ready | Shelf processing completed successfully |
shelf.failed | Shelf processing failed |
shelf.review | Shelf 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.