GuidesGuides

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:

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"
Response
{ "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

ToCallInput
Change address, events or limitsplatform.update_webhookwebhook_id, then any of name, target_url, events, timeout_ms, max_attempts
Send a test event, or replay a deliveryplatform.send_test_deliverywebhook_id, optional replay_of (a delivery id)
Replace the signing secretplatform.rotate_webhook_secretwebhook_id — the old secret keeps signing for 24 hours
Pause or resumeplatform.set_webhook_activewebhook_id, active
Removeplatform.delete_webhookwebhook_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

  1. Verify the signature before trusting the body — x-era-signature is an HMAC-SHA256 over <timestamp>.<raw body>; the code is on Outgoing webhook.
  2. Answer 2xx fast, then do the work. Anything else, or a timeout, is a failed attempt and is retried with growing waits.
  3. Deduplicate on the idempotency-key header (<endpoint id>:<event id>) — it stays the same across retries.
  4. Fetch the latest state when you need more than the payload: data carries ids, so read the record with GET /api/v1/<resource>/<id>.
  5. Watch failures. After many failures in a row the endpoint switches itself off and Era publishes platform.webhook_disabled; one success resets the count.
A minimal receiver (Node)
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.