emfasemfas
Webhooks

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"
}
FieldTypeDescription
idstringUnique delivery id. Use it as an idempotency key — see below.
eventstringThe topic that fired, e.g. product.updated.
identifierstringThe identifier of the affected entity — the same value you use in the REST API.
entity_typestringThe entity type: product, variant, sku, bundle, metaobject, category, or asset.
occurred_atstringRFC 3339 timestamp of when the event was recorded.

Headers

HeaderDescription
Content-TypeAlways application/json.
X-Emfas-Webhook-IdThe delivery id, mirrored from the body for convenience.
X-Emfas-SignatureHMAC signature for verifying authenticity. See Signature verification.
User-AgentEmfas-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 missed 2xx) carries the same id, 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 — and occurred_at reflects that single delivery.
  • A replay is a new id. Re-sending a delivery from Settings is intentional, so it gets a fresh id and 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.