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

Canonical: https://revnu.com/docs/outbound-webhooks
Last updated: 2026-10-21

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](https://revnu.com/docs/webhooks.md#what-outbound-webhooks-dont-cover).

(For the other direction, where your systems wake Revnu, see [Inbound Webhooks](https://revnu.com/docs/webhooks.md).)

## 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:

```json
{
  "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}`](https://revnu.com/docs/lead.md). 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}`](https://revnu.com/docs/lead.md).

## 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:

```ts
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}`](https://revnu.com/docs/lead.md).

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`](https://revnu.com/docs/leads.md) as described in [Push replies into your CRM](https://revnu.com/docs/webhooks.md#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`](https://revnu.com/docs/linkedin-person.md).

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