Feed APIFeed API

REST API / Webhook events

Webhook events

Feed API delivers real-time event notifications to your HTTPS endpoint. Every delivery is signed with HMAC-SHA256 so you can verify it genuinely came from Feed API.

Payload shape

Every event, regardless of type, is wrapped in the same envelope. The top-level event field tells you which kind of event this is; everything specific to it lives under data.

POST your-endpoint.com/webhooks
{
  "id": "wh_a1b2c3d4e5f6a1b2",
  "event": "order.created",
  "timestamp": 1750672200,
  "data": {
    "order_id": "c3d4e5f6-a1b2-4645-8d5e-ff6de080127c",
    "order_number": 1042,
    "reseller_id": "5f25b76c-f8b4-4645-8d5e-ff6de080127c",
    "supplier_id": "5f50e19c-09a0-4b45-8a96-4a9b53a97641",
    "status": "pending",
    "currency": "GBP",
    "reseller_total": 125.99,
    "amount_minor": 12599,
    "items_count": 3,
    "external_ref": null,
    "created_at": "2025-06-23T10:30:00Z"
  }
}

Money amounts appear twice: as a decimal (reseller_total, in pounds) and as an integer in the smallest currency unit (amount_minor, in pence). Prefer amount_minor if your system does exact money arithmetic - it avoids floating-point rounding issues entirely.

Delivery headers

Every request carries these headers. X-Webhook-Attempt is only present on retries.

Headers
Content-Type:        application/json
X-Webhook-ID:        wh_a1b2c3d4e5f6a1b2
X-Webhook-Event:     order.created
X-Webhook-Timestamp: 1750672200
X-Webhook-Signature: sha256=3b5a2c1d...
X-Webhook-Attempt:   2
User-Agent:          FeedAPI-Webhook/1.0

X-Webhook-ID stays the same across every retry of the same event, so you can use it to deduplicate deliveries on your side. Only the timestamp and signature change between attempts.

Verifying signatures

Each delivery includes an X-Webhook-Signature header containing sha256=<hex-digest>. The signed value is the timestamp and the raw request body joined with a period - {timestamp}.{raw_body} - not the body alone. Binding the timestamp into the signature means a captured request can't be replayed later with a new timestamp without also invalidating the signature, so check that the timestamp is recent (e.g. within 5 minutes) before trusting a delivery.

Node.js / TypeScript
import crypto from "node:crypto";

export function verifyWebhookSignature(
  rawBody: string,
  timestamp: string,
  signature: string,
  secret: string
): boolean {
  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  // Reject deliveries that are too old to guard against replay - not
  // part of the signature check itself, but should run alongside it.
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (ageSeconds > 300) return false;

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

app.post("/webhooks", express.raw({ type: "*/*" }), (req, res) => {
  const signature = req.headers["x-webhook-signature"] as string;
  const timestamp = req.headers["x-webhook-timestamp"] as string;
  const rawBody = req.body.toString();

  if (!verifyWebhookSignature(rawBody, timestamp, signature, process.env.FEEDAPI_WEBHOOK_SECRET!)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(rawBody);
  // event.id is stable across retries of the same delivery - use it to
  // deduplicate if you process the same webhook more than once.
  res.status(200).send("ok");
});

Event types

Orders
order.createdNew order placed by the reseller.
order.status_updatedOrder transitions to confirmed or processing.
order.items_cancelledOne or more items on an order are cancelled without cancelling the whole order.
order.shippedOrder marked as shipped with tracking.
order.deliveredOrder marked as delivered.
order.cancelledOrder cancelled.
order.payment_releasedEscrowed payment released to the supplier.
order.refundedA refund to the reseller's card completed successfully.
order.refund_failedAn attempted refund failed and needs manual review.
order.dispute_openedThe cardholder's bank opened a dispute (chargeback).
order.dispute_resolvedA dispute reached a final outcome (won, lost, or withdrawn).
Returns
return.createdReturn request submitted.
return.status_updatedReturn status updated by supplier.
Products
product.updatedA product you stock changed, or was republished after a spell as draft or archived. Includes an updated_fields array, with "status" present on a republish.
product.archivedProduct archived.
catalog.import_completedA bulk catalogue import finishes - one aggregate event, not one per row.
Stock
stock.lowVariant stock falls below its low_stock_threshold.
stock.outVariant stock reaches zero.
Account
account.status_changedAn admin changes your account's status (e.g. suspended, reactivated).

Retry behaviour

A delivery is retried up to 4 attempts total if your endpoint doesn't respond with a 2xx status within 10 seconds. After the 4th attempt fails, the delivery is marked permanently failed and is not retried again.

1stImmediate
2nd1 minute later
3rd5 minutes later
4th (final)30 minutes later

Your endpoint must respond within 10 seconds and with a 2xx status for a delivery to count as successful. Anything else - a timeout, a non-2xx status, or a redirect (redirects are never followed) - is treated as a failure and queued for retry.