Webhooks
Webhooks allow your application to receive real-time notifications when events occur in the Curate-Me platform. Instead of polling the API, you register a URL and the platform sends HTTP POST requests to it when relevant events fire.
Webhook Events
| Event | Description |
|---|---|
analysis.complete | An analysis pipeline has finished (success or failure) |
subscription.created | A new subscription was created |
subscription.updated | A subscription plan was changed |
subscription.cancelled | A subscription was cancelled |
price.alert | A tracked product price dropped below the alert threshold |
Setting Up Webhooks
Register a webhook endpoint through the B2B API.
POST /api/v1/admin/webhooksHeaders:
Authorization: Bearer {token}
X-Org-ID: {organization_id}
Content-Type: application/jsonRequest:
{
"url": "https://your-app.com/webhooks/curate-me",
"events": ["analysis.complete", "price.alert"],
"secret": "whsec_your_signing_secret"
}Response (201):
{
"id": "wh_abc123",
"url": "https://your-app.com/webhooks/curate-me",
"events": ["analysis.complete", "price.alert"],
"status": "active",
"created_at": "2026-02-08T15:00:00Z"
}Webhook Payload
Each webhook delivery is an HTTP POST request with a JSON body and signature headers.
Headers
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the request body |
X-Webhook-Event | The event type (e.g., analysis.complete) |
X-Webhook-Timestamp | Unix timestamp of when the event was generated |
Content-Type | Always application/json |
Example Payload
{
"id": "evt_xyz789",
"event": "analysis.complete",
"timestamp": "2026-02-08T14:23:00Z",
"data": {
"analysis_id": "pipe_abc123",
"status": "completed",
"total_cost": 0.0142,
"total_latency_ms": 4500,
"result_url": "https://api.curate-me.ai/api/v1/analyze/pipe_abc123"
}
}Signature Verification
Every webhook request is signed using HMAC-SHA256 with the secret you provided when creating the webhook. Always verify the signature before processing the payload.
The signature is computed over the raw request body using the webhook secret as the HMAC key.
Python
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
"""Verify the webhook signature.
Args:
payload: Raw request body bytes.
signature: Value of the X-Webhook-Signature header.
secret: The webhook signing secret (whsec_...).
Returns:
True if the signature is valid.
"""
expected = hmac.new(
secret.encode("utf-8"),
payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
# Usage in a FastAPI route:
from fastapi import Request, HTTPException
@app.post("/webhooks/curate-me")
async def handle_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Webhook-Signature", "")
if not verify_webhook(body, signature, WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
event = request.headers.get("X-Webhook-Event")
data = await request.json()
match event:
case "analysis.complete":
await handle_analysis_complete(data["data"])
case "price.alert":
await handle_price_alert(data["data"])
return {"status": "ok"}JavaScript / Node.js
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
const expectedSignature = `sha256=${expected}`;
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}
// Usage in an Express route:
const express = require('express');
const app = express();
app.post('/webhooks/curate-me', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const event = req.headers['x-webhook-event'];
if (!verifyWebhook(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const data = JSON.parse(req.body);
switch (event) {
case 'analysis.complete':
handleAnalysisComplete(data.data);
break;
case 'price.alert':
handlePriceAlert(data.data);
break;
}
res.json({ status: 'ok' });
});Retry Policy
If your endpoint returns a non-2xx status code or the request times out (30 seconds), the platform retries the delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 24 hours |
After 5 failed retries, the delivery is marked as failed. Failed deliveries can be viewed and manually retried through the dashboard or API.
Managing Webhooks
List Webhooks
GET /api/v1/admin/webhooksUpdate a Webhook
PATCH /api/v1/admin/webhooks/{id}{
"events": ["analysis.complete", "price.alert", "subscription.created"],
"url": "https://your-app.com/webhooks/v2/curate-me"
}Delete a Webhook
DELETE /api/v1/admin/webhooks/{id}View Delivery History
GET /api/v1/admin/webhooks/{id}/deliveries?offset=0&limit=20Returns recent delivery attempts including status codes, response times, and retry counts.