.dev

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

EventTrigger
orderCreatedA customer places an order
orderUpdatedOrder details, status, or items change — payload includes previous_data
orderDeletedAn order is deleted

Products

EventTrigger
productCreatedA product is created (dashboard, API, or ingest)
productUpdatedProduct details, price, stock, or status change — payload includes previous_data
productDeletedA 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.

MethodPathPurpose
GET…/webhooksList (secrets masked, hasSecret flag)
POST…/webhooksCreate — 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}/testSend a sample delivery using saved config
POST…/webhooks/testTest an ad-hoc url before saving
POST…/webhooks/sendManually dispatch an event
GET…/webhooks/{id}/historyDelivery 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:

HeaderValue
X-Feeef-EventCanonical event name
X-Feeef-DeliveryUnique UUID — use as your idempotency key
X-Feeef-Webhook-IdThe webhook's id
X-Feeef-Signature-256sha256=<hmac-hex>
X-Feeef-SignatureRaw HMAC hex digest
X-Feeef-Testtrue on test deliveries only
User-AgentFeeef-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

PropertyValue
Timeout30 s (10 s for tests)
RetriesNone — design your handler to be idempotent and monitor the history
SuccessAny 2xx or 3xx response
HistoryEvery 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)

WebhooksRealtime SSE
DirectionFeeef → your serverYour client subscribes
Needs public endpointYesNo
SignatureHMAC-SHA256Bearer-authenticated subscription
Best forServer integrations, automationsLive dashboards, in-app updates

On this page