Webhooks
HMAC-signed order and product events pushed to your endpoints, with a management API and delivery history.
Webhooks push order and product events from a store to your HTTPS endpoint as they happen.
They are scoped per store and managed through a dedicated API — from the dashboard or
programmatically with a token carrying the store.integrations scope.
Events
Orders
| Event | Trigger |
|---|---|
orderCreated | A customer places an order |
orderUpdated | Order details, status, or items change — payload includes previous_data |
orderDeleted | An order is deleted |
Products
| Event | Trigger |
|---|---|
productCreated | A product is created (dashboard, API, or ingest) |
productUpdated | Product details, price, stock, or status change — payload includes previous_data |
productDeleted | A product is deleted |
Legacy spellings (order.created, product_updated, any case) are accepted on input and
normalized to the canonical camelCase values.
Inbound connector writes (metadata.connector.integrationSource === "inbound") do not
fire webhooks, to avoid echo loops with the source platform.
Management API
Base: /stores/{storeId}/integrations/webhooks. Writes need an editor/admin/owner role
(or an OAuth token with store.integrations). Max 10 webhooks per store.
| Method | Path | Purpose |
|---|---|---|
GET | …/webhooks | List (secrets masked, hasSecret flag) |
POST | …/webhooks | Create — returns plaintext secret once |
GET | …/webhooks/{id} | Show one |
PATCH | …/webhooks/{id} | Update; rotateSecret: true mints a fresh secret |
DELETE | …/webhooks/{id} | Delete webhook + its delivery history |
POST | …/webhooks/{id}/test | Send a sample delivery using saved config |
POST | …/webhooks/test | Test an ad-hoc url before saving |
POST | …/webhooks/send | Manually dispatch an event |
GET | …/webhooks/{id}/history | Delivery history, newest first (last 50 kept) |
curl -X POST "https://api.feeef.org/v1/stores/{storeId}/integrations/webhooks" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Catalog sync",
"url": "https://your-app.com/webhooks/feeef",
"events": ["productCreated", "productUpdated", "productDeleted"]
}'Omitting events subscribes to all events (orders + products); an explicit []
subscribes to nothing. Optional fields: headers (custom HTTP headers sent with each
delivery — transport and X-Feeef-* headers can't be overridden), metadata, active.
Payload
Order events put the entity on data.order; product events put it on data.product.
*Updated events also include data.previous_data (may be null).
{
"event": "orderUpdated",
"timestamp": "2025-06-29T01:07:59.787Z",
"data": {
"store_id": "your-store-id",
"order": { "id": "...", "customerName": "...", "items": [], "status": "pending" },
"previous_data": { "status": "draft" }
},
"webhook": { "id": "wh_AbC123xYz9", "name": "Order sync" }
}{
"event": "productCreated",
"timestamp": "2025-06-29T01:07:59.787Z",
"data": {
"store_id": "your-store-id",
"product": {
"id": "...",
"name": "Seasonal shirt",
"slug": "seasonal-shirt",
"price": 2000,
"stock": 40,
"status": "published"
}
},
"webhook": { "id": "wh_AbC123xYz9", "name": "Catalog sync" }
}Request headers on every delivery:
| Header | Value |
|---|---|
X-Feeef-Event | Canonical event name |
X-Feeef-Delivery | Unique UUID — use as your idempotency key |
X-Feeef-Webhook-Id | The webhook's id |
X-Feeef-Signature-256 | sha256=<hmac-hex> |
X-Feeef-Signature | Raw HMAC hex digest |
X-Feeef-Test | true on test deliveries only |
User-Agent | Feeef-Webhooks/2.0 |
Verify signatures
Secrets are server-generated (whsec_ prefix, 192-bit) and returned exactly once. The HMAC
covers the exact raw request body — verify against raw bytes before JSON parsing:
import crypto from 'node:crypto'
export function verifyFeeefWebhook(rawBody: Buffer, signatureHeader: string, secret: string) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const received = signatureHeader.replace('sha256=', '')
return (
expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
)
}
// Express: capture the raw body
app.post('/webhooks/feeef', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyFeeefWebhook(req.body, req.headers['x-feeef-signature-256'] as string, SECRET)) {
return res.status(401).send('Invalid signature')
}
const payload = JSON.parse(req.body.toString('utf8'))
res.status(200).send('OK') // respond fast, process async
})Delivery behavior
| Property | Value |
|---|---|
| Timeout | 30 s (10 s for tests) |
| Retries | None — design your handler to be idempotent and monitor the history |
| Success | Any 2xx or 3xx response |
| History | Every attempt persisted; last 50 per webhook, includes status, response time, truncated body |
Respond 200 immediately and process asynchronously. Dedupe by X-Feeef-Delivery. Since
there are no automatic retries, poll …/history or reconcile periodically via
GET /orders / GET /products if your integration must never miss an event.
Webhooks vs realtime (SSE)
| Webhooks | Realtime SSE | |
|---|---|---|
| Direction | Feeef → your server | Your client subscribes |
| Needs public endpoint | Yes | No |
| Signature | HMAC-SHA256 | Bearer-authenticated subscription |
| Best for | Server integrations, automations | Live dashboards, in-app updates |