Webhooks
Have Era call your server when something happens — an order is placed, a lead comes in, a payment lands — and manage those endpoints from code.
Every Era action publishes events (retail.order_placed, crm.lead_captured, finance.payment_recorded …). A webhook sends the events you choose to an address of yours, signed, with retries. This guide covers choosing events and managing endpoints through the API; the request itself — body, headers, signature check, retries — is on Outgoing webhook, and receiving calls from other systems on Incoming webhook.
Which events exist
Each module's page in the reference ends with its Events table: the name, what happened and the fields its payload carries. For example:
- Shop —
retailevents:retail.order_placed,retail.fulfilment_created,retail.order_canceled,retail.order_charge_status_changed… - Sales —
crmevents:crm.lead_captured,crm.lead_updated… - Finance —
financeevents - Every module: Reference by module
An action's section also lists the events it publishes, and every API call returns them in events, so you can see exactly what a webhook would receive.
In Events you write names separated by commas. retail.* means every event of a module; an empty list means every event.
Create an endpoint from code
Managing webhooks needs platform:manage — create the key or sign in as someone who can manage the workspace's platform settings. Webhooks have no resource of their own (an endpoint's address is the receiver's credential); they are managed through the platform actions:
curl -X POST https://app.getera.io/api/v1/actions/platform.create_webhook \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"direction": "outbound", "name": "Orders to the warehouse", "target_url": "https://wms.example.com/era", "events": "retail.order_placed,retail.order_canceled"}'const res = await era.actions.run("platform.create_webhook", {
direction: "outbound",
name: "Orders to the warehouse",
target_url: "https://wms.example.com/era",
events: "retail.order_placed,retail.order_canceled",
})
const { id, signing_secret } = res.result as { id: string; signing_secret: string }era run platform.create_webhook --direction outbound --name "Orders to the warehouse" \
--target-url https://wms.example.com/era --events "retail.order_placed,retail.order_canceled"{ "ok": true, "result": { "id": "…", "signing_secret": "whsec_…" }, "events": ["platform.webhook_created"] }The secret is shown once
signing_secret is returned only in this answer. Store it with the receiver right away; Era keeps it encrypted and never shows it again. Lost it? Rotate it (below).
The endpoint is active at once. The address must be public https; it is refused with insecure_url, invalid_url or name_taken otherwise. Optional: timeout_ms (default 15000) and max_attempts (default 5).
Manage it
| To | Call | Input |
|---|---|---|
| Change address, events or limits | platform.update_webhook | webhook_id, then any of name, target_url, events, timeout_ms, max_attempts |
| Send a test event, or replay a delivery | platform.send_test_delivery | webhook_id, optional replay_of (a delivery id) |
| Replace the signing secret | platform.rotate_webhook_secret | webhook_id — the old secret keeps signing for 24 hours |
| Pause or resume | platform.set_webhook_active | webhook_id, active |
| Remove | platform.delete_webhook | webhook_id (needs platform:delete) |
All of them are also on screen in Settings → Advanced → Webhooks (/settings/webhooks), with each endpoint's delivery history.
Receive it well
- Verify the signature before trusting the body —
x-era-signatureis an HMAC-SHA256 over<timestamp>.<raw body>; the code is on Outgoing webhook. - Answer
2xxfast, then do the work. Anything else, or a timeout, is a failed attempt and is retried with growing waits. - Deduplicate on the
idempotency-keyheader (<endpoint id>:<event id>) — it stays the same across retries. - Fetch the latest state when you need more than the payload:
datacarries ids, so read the record withGET /api/v1/<resource>/<id>. - Watch failures. After many failures in a row the endpoint switches itself off and Era publishes
platform.webhook_disabled; one success resets the count.
import { createServer } from "node:http"
import { verifyEraSignature } from "./verify" // from the Outgoing webhook page
createServer(async (req, res) => {
const chunks: Buffer[] = []
for await (const c of req) chunks.push(c as Buffer)
const raw = Buffer.concat(chunks).toString("utf8")
if (!verifyEraSignature(raw, String(req.headers["x-era-signature"] ?? ""), process.env.ERA_WEBHOOK_SECRET!)) {
res.writeHead(401).end()
return
}
res.writeHead(204).end() // answer first
const event = JSON.parse(raw) as { event: string; data: Record<string, unknown> }
if (event.event === "retail.order_placed") queueForPicking(event.data.id as string)
}).listen(8080)Workflows instead of a server
If what you want to do on an event is inside Era — send a message, create a task, update a record — use a workflow with that event as its trigger instead: no server to run. See Workflows.