Payload
The structure of the JSON body Emfas POSTs to your webhook endpoint.
Every delivery is a POST with a JSON body and a small set of headers.
Body
{
"id": "9f1c8a2e-3b4d-4e5f-8a1b-2c3d4e5f6a7b",
"event": "product.updated",
"identifier": "SKU-12345",
"entity_type": "product",
"occurred_at": "2026-06-03T09:21:44Z"
}| Field | Type | Description |
|---|---|---|
id | string | Unique delivery id. Use it as an idempotency key — see below. |
event | string | The topic that fired, e.g. product.updated. |
identifier | string | The identifier of the affected entity — the same value you use in the REST API. |
entity_type | string | The entity type: product, variant, sku, bundle, metaobject, category, or asset. |
occurred_at | string | RFC 3339 timestamp of when the event was recorded. |
Headers
| Header | Description |
|---|---|
Content-Type | Always application/json. |
X-Emfas-Webhook-Id | The delivery id, mirrored from the body for convenience. |
X-Emfas-Signature | HMAC signature for verifying authenticity. See Signature verification. |
User-Agent | Emfas-Webhooks/1.0. |
Idempotency
Webhook delivery is at-least-once: a delivery is retried on failure, and a network hiccup can cause a successful delivery to be re-sent (we never saw your 2xx). So the same delivery may arrive more than once.
Each delivery carries a unique id (also in the X-Emfas-Webhook-Id header). Make your processing idempotent: record the ids you've handled and skip any you've already seen.
if (await alreadyProcessed(payload.id)) return res.sendStatus(200)
await handle(payload)
await markProcessed(payload.id)What the id identifies
id is the id of a delivery — one payload sent to your endpoint — not the underlying change. That distinction is deliberate and gives the behaviour you want:
- Retries reuse the same
id. Every retry of a delivery (after a failure or a missed2xx) carries the sameid, so your dedupe suppresses the duplicate. - Bursts collapse to one
id. Rapid repeated changes to the same entity are coalesced into a single delivery, so you won't get a flood of near-identical deliveries — andoccurred_atreflects that single delivery. - A replay is a new
id. Re-sending a delivery from Settings is intentional, so it gets a freshidand is not skipped by your dedupe — you reprocess it on purpose. (Keying on the underlying change instead would silently swallow replays.)
Because payloads are thin and you re-fetch current state via REST, processing a duplicate is harmless — you'd just re-fetch the same latest data. The id is there to save you redundant work and protect non-idempotent side effects, not to prevent corruption.
Fetching the full entity
The payload deliberately does not include the entity's full data. Use the identifier to fetch the current record from the REST API — for example, on product.updated:
curl "https://api.emfas.ai/v1/products/SKU-12345" \
-H "Authorization: Bearer YOUR_API_KEY"Re-fetching guarantees you always act on the latest state. Because several rapid changes to the same entity can be collapsed into a single delivery, the payload reflects that a change happened, not a specific diff.
For deleted events the entity no longer exists, so a follow-up fetch will return 404 — the identifier in the payload is the record you should remove on your side.