Receive webhooks
Subscribe an HTTPS endpoint to OmniLead events, verify every delivery's signature, and handle retries and duplicates.
Webhooks push events to your server the moment they happen: a new lead, a reply, a bounce, an unsubscribe. Each delivery is an HTTPS POST with a JSON event, signed with a secret only you and OmniLead know. Webhooks are part of the Agency plan.
Events
The API reference shows every field of every event, with a full example.
The event envelope
Every delivery has the same shape:
{
"id": "evt_emailrepli01",
"object": "event",
"type": "email.replied",
"api_version": "2026-09-28",
"created": "2026-09-30T11:02:01Z",
"workspace_id": "ws_1Aa2Bb3Cc4",
"livemode": true,
"data": {
"object": {
"id": "msg_7Yu2iO5pAs",
"lead_id": "lead_7Hq2mR9xKd",
"sequence_id": "seq_5Kd9wP2xLm",
"mailbox_id": "mbx_8Tq3rN6yHs",
"direction": "inbound",
"from": "maya@northwind.example",
"to": "sam@yourcompany.example",
"subject": "Re: Northwind's Q4 shipping volume",
"snippet": "Thanks Sam, happy to talk next week. Tuesday works.",
"sent_at": "2026-09-30T11:01:48Z"
}
}
}data.object is rendered with the api_version shown, which is the version pinned on the endpoint when you created it.
Add an endpoint
Owners and admins can add endpoints in the app or through the API.
Open webhook settings
Go to Settings → Webhooks.
Add the endpoint
Click Add endpoint and enter your HTTPS URL, for example
https://hooks.example.com/omnilead.Choose events
Tick the events you want, or All events. Subscribe only to what you use; it keeps your server quiet.
Create and copy the secret
Click Create endpoint. Copy the Signing secret, which starts with
whsec_. It's shown once.
Through the API, send POST /webhook_endpoints with url and events. The response includes secret once. See the API reference.
Verify signatures
Anyone can send a POST to your URL, so check every delivery's signature before you trust it. Each request carries an OmniLead-Signature header:
OmniLead-Signature: t=1759230121,v1=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39tis the Unix time, in seconds, when OmniLead signed the delivery.v1is the hex-encoded HMAC-SHA256 of the stringt.body: the timestamp, a full stop, then the raw request body exactly as received. The key is your whole signing secret, including thewhsec_prefix.
To verify:
- Split the header on commas and read
tand everyv1value. - Reject the delivery if
tis more than 5 minutes (300 seconds) from your server's clock. This stops replayed requests. - Compute the HMAC-SHA256 of
t + "." + rawBodywith your secret. - Compare it with each
v1using a constant-time comparison. Accept if any matches.
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.OMNILEAD_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 300;
export function verifyOmniLeadSignature(rawBody, header, secret, now = Math.floor(Date.now() / 1000)) {
const parts = header.split(",").map((p) => p.trim().split("="));
const t = Number(parts.find(([k]) => k === "t")?.[1]);
const signatures = parts.filter(([k]) => k === "v1").map(([, v]) => v);
if (!Number.isInteger(t) || signatures.length === 0) return false;
if (Math.abs(now - t) > TOLERANCE_SECONDS) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return signatures.some((sig) => {
const received = Buffer.from(sig, "hex");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
// express.raw keeps the body as bytes so the signature can be checked.
app.post("/omnilead", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
if (!verifyOmniLeadSignature(rawBody, req.get("OmniLead-Signature") ?? "", SECRET)) {
return res.status(400).send("Invalid signature");
}
const event = JSON.parse(rawBody);
// Acknowledge fast, then process. Skip event.id values you've already handled.
res.sendStatus(200);
handleEvent(event);
});import hashlib
import hmac
import os
import time
from flask import Flask, abort, request
SECRET = os.environ["OMNILEAD_WEBHOOK_SECRET"] # whsec_…
TOLERANCE_SECONDS = 300
def verify_omnilead_signature(raw_body: bytes, header: str, secret: str, now: int | None = None) -> bool:
items = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in items if k == "t"), None)
signatures = [v for k, v in items if k == "v1"]
if t is None or not t.isdigit() or not signatures:
return False
now = int(time.time()) if now is None else now
if abs(now - int(t)) > TOLERANCE_SECONDS:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, sig) for sig in signatures)
app = Flask(__name__)
@app.post("/omnilead")
def omnilead_webhook():
raw_body = request.get_data() # bytes, before any parsing
if not verify_omnilead_signature(raw_body, request.headers.get("OmniLead-Signature", ""), SECRET):
abort(400)
event = request.get_json()
# Skip event["id"] values you've already handled, then process.
handle_event(event)
return "", 200In a Next.js route handler, read the raw body with await request.text() before calling the verification function, and pass the same string to JSON.parse afterwards.
Respond quickly
Return any 2xx status within 10 seconds to acknowledge a delivery. Do the real work afterwards, in a queue or background job. Anything else counts as a failure: a 3xx, 4xx or 5xx status, a timeout, or a connection error. Redirects aren't followed.
Retries
When a delivery fails, OmniLead retries with exponential backoff. Retries happen about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 15 hours after the previous attempt, so the last retry comes roughly 24 hours after the event. After that the delivery is marked failed.
If an endpoint fails 100 deliveries in a row, OmniLead disables it and stops sending to it. Settings → Webhooks shows the endpoint as Disabled with the reason. Fix the endpoint, then switch it back on; the failure count starts over.
Because of retries:
- You may receive an event more than once. Store the
idof events you've processed and skip repeats. - Events can arrive out of order. Use the
createdtimestamp, or fetch the current object from the API, rather than assuming order. For example, afterlead.updated, callGET /leads/{id}if you need the latest state.
Test your endpoint
Point an endpoint at your development server through a tunnel, then use a test API key in the API reference console to create or update a lead. Events caused by test-key calls arrive with livemode: false, so your handler can tell them apart.
Remove an endpoint
In Settings → Webhooks, open the endpoint's menu and click Delete endpoint, or call DELETE /webhook_endpoints/{id}. Deliveries stop immediately, including retries in progress.
Which IP addresses do webhooks come from?
Deliveries come from our hosting provider's shared network, so the addresses change. Verify the signature instead of allow-listing IPs.
Can I get the secret again?
No. Delete the endpoint and create a new one to get a new secret, then update your server.
Do webhooks cost credits?
No. Deliveries are free and don't count toward your API rate limit.