Skip to Content
Browse the Docs▾

Outbound Webhooks

Revnu POSTs a signed JSON event to your URL when a LinkedIn invite is sent, accepted or replied to, so a CRM sync doesn't have to poll. Retried for about two days.

An outbound webhook is a URL of yours that Revnu POSTs to when something happens on LinkedIn: an invite goes out, someone accepts, or someone replies. Use it to keep a CRM in step with Revnu's LinkedIn outreach without polling. For other changes, such as email replies or new leads, poll the API.

(For the other direction, where your systems wake Revnu, see Inbound Webhooks.)

1. Add an endpoint

  1. In the dashboard, open Settings → Developers and find Outbound Webhooks.

  2. Click Add Endpoint, paste an https:// URL, and pick the events you want.

  3. Copy the signing secret it shows (whsec_...). You can read it again any time with Signing Secret.

  4. Click Send Test. A ping event arrives at your URL, and the result shows under Deliveries.

An account can have five endpoints. The URL must be public https; private addresses and redirects are refused.

Events

Event Sent when When it arrives
linkedin.invite_sent Revnu sends a LinkedIn connection request Within seconds
linkedin.invite_accepted Revnu sees that the person accepted Up to a day after they accept
linkedin.replied The person replies to a LinkedIn message for the first time in that conversation Within seconds

LinkedIn doesn't tell anyone when an invite is accepted. Revnu checks its pending invites about once a day, so linkedin.invite_accepted fires when that check finds the connection, and occurredAt is the time it was found. Later replies in the same conversation don't fire again. Events cover what Revnu sent, so invites you send yourself in LinkedIn aren't included.

Payload

Every event is a JSON POST like this:

{
  "id": "evt_6f0c2b9e1d4a47c8a1e35b2d90f7c164",
  "type": "linkedin.invite_accepted",
  "createdAt": 1792656000000,
  "data": {
    "occurredAt": 1792655990000,
    "linkedinProfile": "dana-smith-4b1a2",
    "lead": { "id": "k57c8w0y3m2...", "companyName": "Acme", "domain": "acme.com" },
    "contact": {
      "id": "jd7f2q9x1v8...",
      "name": "Dana Smith",
      "linkedinUrl": "https://www.linkedin.com/in/dana-smith-4b1a2"
    }
  }
}
Field Meaning
id The event's id. The same change always has the same id, including on retries.
type One of the events above, or ping for Send Test.
createdAt When Revnu queued the event, in Unix milliseconds.
data.occurredAt When the invite was sent, the acceptance was found or the reply arrived, in Unix milliseconds.
data.linkedinProfile The LinkedIn profile Revnu sent to, as Revnu addressed it.
data.lead The company, when the person is part of a sequence. Same id as GET /leads/{id}. Otherwise null.
data.contact The person, when part of a sequence. Otherwise null.

For the full LinkedIn history of a lead (invited, accepted, last message, seen, replied and the next step), read linkedin on GET /leads/{id}.

Verify the signature

Each request carries these headers:

Header Value
Revnu-Event-Id Same as id in the body
Revnu-Event-Type Same as type in the body
Revnu-Signature t=<unix seconds>,v1=<hex HMAC-SHA256>

v1 is the HMAC-SHA256 of <t>.<raw body> with your signing secret as the key. Compute it over the raw bytes you received, before parsing the JSON:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyRevnu(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
  const t = Number(parts.t);
  // Refuse anything older than five minutes so a captured request can't be replayed.
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const got = parts.v1 ?? "";
  return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}

Responding and retries

Answer with any 2xx within 15 seconds to confirm you received the event. Anything else, a redirect or a timeout counts as a failure, and Revnu tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 1 day: eight tries over about two days. After the last one the event shows as Failed under Deliveries, where Resend sends it again with the same id.

Revnu may deliver an event more than once, and events can arrive out of order. Store each id you've handled and skip repeats. To get the current state rather than the latest change, read GET /leads/{id}.

Do the slow work after you reply: return 200 first, then update your CRM.

Turning it off

Disable stops sending to the endpoint. Events that happen while it's off aren't sent later; read GET /leads to catch up. Delete removes the endpoint and its delivery history.

Frequently Asked Questions

Why did the acceptance webhook arrive a day late?

LinkedIn doesn't announce acceptances. Revnu checks pending invites about once a day, and the event fires when that check finds the connection.

Can I get a webhook when a lead replies by email?

Not yet. Outbound webhooks cover LinkedIn invites, acceptances and replies. For email replies, poll GET /leads?status=replied as described in Push replies into your CRM.

Do I get events for invites I send myself?

No. Events cover invites and messages Revnu sent. For a person you contacted yourself, use GET /linkedin/person.

How do I check my receiver works?

Click Send Test on the endpoint. A ping event is sent once, and Deliveries shows Delivered with the status code, or Failed with the error.