Webhooks: lead events, signatures and retries
Get a signed POST at your own https address when a lead is held or pushed, a run finishes or a trail pauses: the events, payloads, signature check and retries.
By Operelio team · Updated October 30, 2026
On this page9
What webhooks do
Operelio webhooks send a signed POST to your https address when a lead is held or pushed, a run finishes or a trail pauses, so your code doesn't have to keep asking the API. You give Operelio an https address and choose the events it should get. Each time one of those events happens, Operelio sends a POST with a small JSON body to that address, signed with a secret only you and Operelio know.
Webhooks come with API access, on Pro and Agency. A workspace can have up to 3 webhook endpoints on Pro and 10 on Agency. Deliveries don't count toward your monthly API calls.
A delivery carries ids, statuses and times, never a lead's name, email or other cells. That keeps your customers' data out of the logs of whatever receives it. To get the details, call the API with your key using the ids in the delivery.
Adding an endpoint
Open Integrations in the rail, choose the API keys tab, and click Add endpoint on the Webhooks card. Paste the Endpoint URL, add a Name if you like (the list leads with it, so two endpoints at one host are easy to tell apart), uncheck any Events to send you don't want (Lead held, Lead pushed, Run finished and Trail paused all start checked), then click Add endpoint. The next window shows the endpoint's signing secret once. Click Copy secret and store it the way you store an API key; it starts with whsec_.
Only workspace admins can add or change webhooks. Members see the endpoints and their logs, read only.
Operelio checks the address before it saves it. It must start with https://, it can't carry a user name or password, and its host must be a public address: an address on a private or local network is refused, both when you add it and each time Operelio sends. Redirects are never followed. Each address can be added once per workspace.
Events
An endpoint gets only the events it chose. Every delivery's data block holds the fields in the last column, and every time is an ISO 8601 time in UTC.
Run and trail events go out within a minute. Lead events go out about 2 to 3 minutes after they happen, because Operelio reads them a little behind the clock so none are missed.
| Event | Sent when | Data fields |
|---|---|---|
| lead.held | A lead on a trail is held for a person to look at, for example for review, because the CRM refused it, or because a field it needs is empty. | leadId, trailId, folderId, reason, at |
| lead.pushed | A lead on a trail reached your CRM. crm is hubspot, salesforce or pipedrive, or null when it isn't known. | leadId, trailId, folderId, crm, at |
| run.completed | A run of a switched-on trail ends (Run finished on the Webhooks card), with status completed, parked, failed or cancelled. Test runs send nothing. A parked run that later finishes sends again with its new status. | runId, trailId, status, at |
| trail.paused | Operelio stops a switched-on trail: a run needs a change to the trail (needs_attention), the plan no longer runs live trails (plan), or what the trail formats for changed (review_cleared). Switching a trail off yourself sends nothing. | trailId, reason, at |
| webhook.test | An admin clicks Send test event on the endpoint. It goes to that endpoint, whatever events it chose. | workspaceId, at |
A lead.held reason is a short code such as review, crm_rejected, crm_unreachable, missing_field or low_score. Anything that isn't a plain code is sent as other. trailId and folderId are null when the lead has none.
What a delivery looks like
Each delivery is a POST with a JSON body: the delivery's id, the event type, when the event was queued for this endpoint, your workspace id, and the event's data. Operelio-Event and Operelio-Delivery repeat the type and the id as headers, and Operelio-Timestamp and Operelio-Signature are for the signature check below.
The id stays the same on every try of the same delivery, so store the ids you have handled and skip one you have seen before. Deliveries can arrive out of order; sort by the time in data if order matters to you.
POST /hooks/operelio HTTP/1.1
Content-Type: application/json
User-Agent: Operelio-Webhooks/1.0 (+https://operelio.com/developers)
Operelio-Event: lead.pushed
Operelio-Delivery: 5f0c1d2e-8a4b-4c7d-9e21-3b6a7f9d0c48
Operelio-Timestamp: 1793350502
Operelio-Signature: v1=8d3e0c5b1f...
{
"id": "5f0c1d2e-8a4b-4c7d-9e21-3b6a7f9d0c48",
"type": "lead.pushed",
"createdAt": "2026-10-30T09:15:02.481Z",
"workspaceId": "0b8e4f6a-2c1d-4e3f-9a5b-7c6d8e9f0a1b",
"data": {
"leadId": "c41a7e2b-9d3f-4b6a-8e1c-2f5d7a9b0e3c",
"trailId": "7e2d9c1b-4a6f-4d8e-b3c5-1a9f0e7d2c6b",
"folderId": "a3b5c7d9-1e2f-4a6b-8c0d-9e1f2a3b4c5d",
"at": "2026-10-30T09:14:31.112Z",
"crm": "hubspot"
}
}The real body is sent on one line with no spaces. It's shown spread out here only to read.
Verify the signature
Check the signature on every delivery before you trust it, so you know it came from Operelio and wasn't changed or replayed. Operelio-Timestamp is the send time in Unix seconds. Operelio-Signature is v1= followed by the hex HMAC-SHA256 of the timestamp, a period, and the raw request body, keyed with your endpoint's whole signing secret, whsec_ included.
To check it, compute the same value from the raw body (before you parse the JSON), compare the two in constant time, and refuse a timestamp more than 5 minutes from your clock. A retry is signed again with a new timestamp. Here is the check in Node; the Python version follows.
const crypto = require("crypto");
// rawBody: the request body exactly as received, before JSON.parse.
function verifyOperelio(rawBody, headers, secret) {
const timestamp = headers["operelio-timestamp"];
const signature = headers["operelio-signature"] || "";
if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = "v1=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Most web frameworks parse JSON before your handler runs. Read the raw body instead (express.raw() in Express, request.get_data() in Flask), or the signature won't match.
Verify the signature in Python
The same check in Python, with only the standard library. Pass the body as the bytes you received.
import hashlib, hmac, time
# raw_body: the request body exactly as received, as bytes.
def verify_operelio(raw_body: bytes, headers, secret: str) -> bool:
timestamp = headers.get("Operelio-Timestamp", "")
signature = headers.get("Operelio-Signature", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
signed = timestamp.encode() + b"." + raw_body
expected = "v1=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)Retries and pausing
Answer with any 2xx status within 10 seconds and the delivery is done. Do the slow work after you answer. Anything else counts as a failure: another status (a redirect included), no answer in time, or a connection or TLS error.
A failed delivery is tried again after 1, 5 and 15 minutes, then after 1, 3, 6, 12 and 24 hours: 9 tries over about 2 days. If the last one fails, the delivery is marked failed and stays in the log. A test event is tried once.
When an endpoint's deliveries have kept failing for 3 days with none getting through, Operelio pauses it, and the workspace's admins get a notification and an email. Fix the receiver, then click the endpoint on the Webhooks card and choose Resume. Deliveries already waiting go out once it's resumed. Events that happen while it is paused are not sent. An admin can also pause and resume an endpoint by hand, and the endpoint's window says who paused it and when.
The delivery log
Click an endpoint on the Webhooks card to open it. Its Deliveries log keeps the last 30 days: the time, the event, the result your server gave, and how many attempts it took. Resend on a finished delivery sends it again with a fresh set of retries.
Send test event, in the same window, sends a webhook.test delivery so you can check your receiver before real events reach it, one at a time. Edit changes the endpoint's name, address or events: the signing secret stays the same, and deliveries not sent yet go to the new address. Delete endpoint removes the endpoint and its log.
Frequently asked questions
How do I verify an Operelio webhook?
Compute v1= plus the hex HMAC-SHA256 of the Operelio-Timestamp header, a period and the raw body, keyed with your endpoint's whsec_ secret, and compare it with Operelio-Signature in constant time. Refuse a timestamp more than 5 minutes old.
How often does Operelio retry a webhook?
A delivery that doesn't get a 2xx within 10 seconds is tried again after 1, 5 and 15 minutes, then after 1, 3, 6, 12 and 24 hours. That is 9 tries over about 2 days.
What happens when a webhook endpoint keeps failing?
After 3 days of failures with none getting through, Operelio pauses the endpoint and tells the workspace's admins by notification and email. Deliveries already waiting go out when an admin resumes it.
Connect Operelio to your code or your assistant
Start free and add Operelio to Claude or ChatGPT today. The API and webhooks come with Pro.