Outgoing webhook
Era posting its events to your address, signed: body, headers, signature checks and retries.
https://app.getera.iohttps://your-server.example/era-webhookWith an outgoing webhook the direction is reversed: when the events you chose happen, Era sends a POST request to your address. For example, it tells your accounting system or your own server whenever a new lead comes in.
Creating an endpoint
New endpoint
On Settings → Advanced → Webhooks (/settings/webhooks) click New endpoint and choose We call them as the Direction.
Address and events
Type a Name and the URL. In Events, list the event names you want, separated by commas, e.g. crm.lead_captured. Use crm.* for every event of a module. Leave it empty and every event is sent.
Turn on signing
In the endpoint's detail, under Signing, click Generate secret. The secret (it starts with whsec_) is shown once; store it on the receiving side. Endpoints without one are marked Unsigned in the list.
Try it
Send test under Try it sends a signed test event through the same queue, timeout and history as a real delivery.
The request Era sends
Body:
{
"id": "1842",
"event": "crm.lead_captured",
"recordId": "8b1d0c4e-0000-4000-8000-000000000010",
"occurredAt": "2026-10-14T09:30:00.000Z",
"workspaceId": "WORKSPACE_ID",
"data": { "id": "8b1d0c4e-0000-4000-8000-000000000010", "customerId": null, "affiliateId": null }
}data is the event's own payload; it differs per event.
Headers:
| Header | Value |
|---|---|
content-type | application/json |
user-agent | Era-Webhook/1 |
idempotency-key | <endpoint id>:<event id> — stays the same across retries; use it to avoid processing twice. |
x-era-event | The event name, e.g. crm.lead_captured. |
x-era-attempt | Which attempt this is (starting at 1). |
x-era-timestamp | When it was signed (Unix seconds). |
x-era-signature | t=<time>,v1=<signature>. While a secret is being rotated, there are two v1 values. |
x-era-signed | Only on unsigned endpoints: none. |
Verifying the signature
Each v1 is an HMAC-SHA256 computed with the secret over <time>.<raw body>. Use the body exactly as received — do not parse and re-serialise it. Reject timestamps older than five minutes. The request is valid if any value matches.
import crypto from 'node:crypto'
export function verifyEraSignature(rawBody, header, secret) {
const parts = header.split(',')
const t = Number(parts.find((p) => p.startsWith('t='))?.slice(2))
const signatures = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3))
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return signatures.some(
(s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))
)
}When you click Rotate secret, the old secret keeps signing for another 24 hours; move the receiver to the new one in that time.
Your response and retries
- A
2xxstatus counts as delivered. Any other status, a timeout or a connection error is a failed attempt. - Failed deliveries are retried; the wait grows a little each time (from a few seconds up to ten minutes).
- Endpoint settings: Timeout (ms) defaults to 15000; Attempts before giving up defaults to 5 (1–10).
- After many consecutive failures the endpoint switches itself off (a workspace threshold, 100 by default). One successful delivery resets the count.
- Redirects are not followed, and only public addresses are called.
The delivery history shows each attempt's status and duration; Replay this delivery sends the same body again.