Operelio
Docs6 min read

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
  1. 1.What webhooks do
  2. 2.Adding an endpoint
  3. 3.Events
  4. 4.What a delivery looks like
  5. 5.Verify the signature
  6. 6.Verify the signature in Python
  7. 7.Retries and pausing
  8. 8.The delivery log
  9. 9.Frequently asked questions

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.

EventSent whenData fields
lead.heldA 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.pushedA 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.completedA 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.pausedOperelio 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.testAn 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.

A lead.pushed delivery
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.

Node
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.

Python
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.