Skip to main content
Webhooks let you receive HTTP POST notifications when events occur in your Shelfforce account. Instead of polling for analysis results, register a webhook URL and Shelfforce will notify you as soon as results are ready.

Available events

EventDescription
analysis.completedAn image analysis finished successfully and products are available.
analysis.failedAn image analysis encountered an error and could not produce results.
task.createdA new task was created.
task.updatedA task’s status or details were modified.
task.completedA task moved to the completed status.
*Wildcard — receive all events.

Registering a webhook

Create a webhook by sending a POST request with an admin-role API key:
curl -X POST https://shelfforce.ai/api/v1/webhooks \
  -H "Authorization: Bearer sf_live_a1b2c3d4..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/shelfforce",
    "events": ["analysis.completed", "analysis.failed"],
    "description": "Production analysis handler"
  }'
Response:
{
  "data": {
    "id": "wh_abc123",
    "url": "https://your-app.com/webhooks/shelfforce",
    "events": ["analysis.completed", "analysis.failed"],
    "secret": "whsec_a1b2c3d4e5f6g7h8i9j0",
    "status": "active",
    "createdAt": "2026-02-23T10:00:00Z"
  }
}
The webhook secret is returned only once at creation. Store it securely — you will need it to verify webhook signatures.

Webhook delivery format

Every webhook delivery is an HTTP POST to your registered URL. Here is the full shape of what hits your endpoint.

HTTP headers

POST /webhooks/shelfforce HTTP/1.1
Host: your-app.com
Content-Type: application/json
User-Agent: Shelfforce-Webhooks/1.0
X-Shelfforce-Signature: t=1740312618,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-Shelfforce-Event: analysis.completed
X-Shelfforce-Delivery: evt_abc123
HeaderDescription
X-Shelfforce-SignatureHMAC-SHA256 signature for verification (see Signature verification)
X-Shelfforce-EventThe event type (e.g., analysis.completed)
X-Shelfforce-DeliveryUnique delivery ID for idempotency tracking
Content-TypeAlways application/json

JSON body structure

Every webhook body follows the same top-level structure:
{
  "event": "analysis.completed",
  "data": { ... },
  "timestamp": "2026-02-23T10:32:00Z",
  "webhookId": "wh_abc123"
}
FieldTypeDescription
eventstringThe event type that triggered this delivery
dataobjectEvent-specific payload (see examples below)
timestampstringISO 8601 timestamp of when the event occurred
webhookIdstringID of the webhook endpoint that received this delivery

Payload examples by event type

Fired when a shelf analysis finishes successfully.
{
  "event": "analysis.completed",
  "data": {
    "generationId": "an_g7h8j9k0",
    "status": "completed",
    "productCount": 12,
    "createdAt": "2026-02-23T10:30:00Z"
  },
  "timestamp": "2026-02-23T10:32:00Z",
  "webhookId": "wh_abc123"
}
Use the generationId to fetch the full analysis with products:
curl https://shelfforce.ai/api/v1/analyses/an_g7h8j9k0 \
  -H "Authorization: Bearer sf_live_a1b2c3d4..."

Webhook receiver examples

Copy-paste these handlers to get a working webhook receiver. They verify the signature and handle each event type.
import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } }));

const WEBHOOK_SECRET = process.env.SHELFFORCE_WEBHOOK_SECRET!;

function verifySignature(rawBody: Buffer, signatureHeader: string): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => {
      const [key, value] = part.split("=");
      return [key, value];
    })
  );

  const timestamp = parts["t"];
  const signature = parts["v1"];
  if (!timestamp || !signature) return false;

  // Reject requests older than 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) return false;

  const signedPayload = `${timestamp}.${rawBody.toString()}`;
  const expected = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(signedPayload)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

app.post("/webhooks/shelfforce", (req, res) => {
  const signatureHeader = req.headers["x-shelfforce-signature"] as string;
  if (!verifySignature((req as any).rawBody, signatureHeader)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  // Respond immediately — process asynchronously
  res.status(200).json({ received: true });

  const { event, data } = req.body;

  switch (event) {
    case "analysis.completed":
      console.log(`Analysis ${data.generationId} completed with ${data.productCount} products`);
      // Fetch full results: GET /api/v1/analyses/{data.generationId}
      break;

    case "analysis.failed":
      console.error(`Analysis ${data.generationId} failed: ${data.error}`);
      break;

    case "task.created":
      console.log(`Task ${data.taskId} created: ${data.title}`);
      break;

    case "task.updated":
      console.log(`Task ${data.taskId} updated: ${data.updatedFields.join(", ")}`);
      break;

    case "task.completed":
      console.log(`Task ${data.taskId} completed — compliance: ${data.complianceScore}%`);
      break;
  }
});

app.listen(3000, () => console.log("Webhook receiver running on port 3000"));
Your webhook endpoint should return a 200 response as quickly as possible. Perform heavy processing (fetching full analysis results, updating your database, etc.) asynchronously after acknowledging receipt.

Signature verification

Every webhook request includes an X-Shelfforce-Signature header that you should verify to ensure the request is authentic and has not been tampered with. The header format is:
X-Shelfforce-Signature: t=1740312618,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Where:
  • t — Unix timestamp of when the webhook was sent
  • v1 — HMAC-SHA256 signature

How to verify

  1. Extract the t (timestamp) and v1 (signature) values from the header.
  2. Construct the signed payload: {timestamp}.{raw_json_body}
  3. Compute HMAC-SHA256 of that payload using your webhook secret.
  4. Compare your computed signature with the v1 value using a constant-time comparison.
  5. Optionally, reject requests where the timestamp is more than 5 minutes old to prevent replay attacks.

Standalone verification functions

If you already have a web server and just need the verification logic:
import crypto from "crypto";

function verifyWebhookSignature(
  payload: string,
  signatureHeader: string,
  secret: string,
  toleranceSeconds = 300
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => {
      const [key, value] = part.split("=");
      return [key, value];
    })
  );

  const timestamp = parts["t"];
  const signature = parts["v1"];

  if (!timestamp || !signature) {
    return false;
  }

  // Check timestamp tolerance (prevent replay attacks)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > toleranceSeconds) {
    return false;
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Retry behavior

If your webhook endpoint returns a non-2xx status code or is unreachable, Shelfforce retries the delivery up to 3 times with exponential backoff:
AttemptDelay
1st retry500ms
2nd retry1 second
3rd retry2 seconds
After all retries are exhausted, the webhook delivery is marked as failed. You can view failed deliveries in the Webhooks settings page in the dashboard.

Managing webhooks

OperationEndpointKey role
List webhooksGET /api/v1/webhooksadmin
Create webhookPOST /api/v1/webhooksadmin
Delete webhookDELETE /api/v1/webhooks/{id}admin

List webhooks

curl https://shelfforce.ai/api/v1/webhooks \
  -H "Authorization: Bearer sf_live_a1b2c3d4..."

Delete a webhook

curl -X DELETE https://shelfforce.ai/api/v1/webhooks/wh_abc123 \
  -H "Authorization: Bearer sf_live_a1b2c3d4..."

Testing

Use a tool like webhook.site to get a temporary URL for testing:
  1. Go to webhook.site and copy the unique URL.
  2. Register it as a webhook endpoint with events: ["*"].
  3. Submit a test analysis via the API.
  4. Watch the event appear on webhook.site with the full payload and headers.
Remember to delete test webhooks when you are done testing to avoid unnecessary deliveries.