Skip to content
OmniLeadDocs
Sign in

Receive webhooks

Subscribe an HTTPS endpoint to OmniLead events, verify every delivery's signature, and handle retries and duplicates.

Available on Agency4 min readLast updated

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

EventSent whendata.object
lead.createdA lead is added by a reveal, an import, the app or the APILead
lead.updatedLead fields change; data.previous_attributes holds the old valuesLead
lead.stage_changedA lead moves to another pipeline stageLead stage
reveal.completedA reveal finishes, including refunded ones with status: "no_contact_found"Reveal
email.sentA sequence step or an inbox reply leaves a mailboxEmail message
email.repliedA reply is detected; the sequence stops for that lead. Out-of-office replies don't countEmail message
email.bouncedA hard bounce; the address is marked invalid and suppressedEmail message
sequence.completedA lead reaches the end of a sequence without replyingEnrollment
unsubscribe.createdSomeone unsubscribes, or opts out through the public portalSuppression
credits.lowThe total balance drops below 10% of the plan's monthly credits (once per period)Credit balance

The API reference shows every field of every event, with a full example.

The event envelope

Every delivery has the same shape:

POST to your endpoint
{
  "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.

  1. Open webhook settings

    Go to Settings → Webhooks.

  2. Add the endpoint

    Click Add endpoint and enter your HTTPS URL, for example https://hooks.example.com/omnilead.

  3. Choose events

    Tick the events you want, or All events. Subscribe only to what you use; it keeps your server quiet.

  4. 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:

Signature header
OmniLead-Signature: t=1759230121,v1=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
  • t is the Unix time, in seconds, when OmniLead signed the delivery.
  • v1 is the hex-encoded HMAC-SHA256 of the string t.body: the timestamp, a full stop, then the raw request body exactly as received. The key is your whole signing secret, including the whsec_ prefix.

To verify:

  1. Split the header on commas and read t and every v1 value.
  2. Reject the delivery if t is more than 5 minutes (300 seconds) from your server's clock. This stops replayed requests.
  3. Compute the HMAC-SHA256 of t + "." + rawBody with your secret.
  4. Compare it with each v1 using 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);
});

In 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 id of events you've processed and skip repeats.
  • Events can arrive out of order. Use the created timestamp, or fetch the current object from the API, rather than assuming order. For example, after lead.updated, call GET /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.

What's next