Webhooks
Overview
Section titled “Overview”Webhooks stream Vaam outreach activity to an HTTPS endpoint you control, as signed HTTP events. Use them to sync activity into your own systems — a CRM, a data warehouse, an internal alerting service — without polling the Vaam API.
Each event is a JSON payload identifying what happened, the prospect it happened to, and which sequence they’re in. Every request is signed so you can verify it came from Vaam, and every delivery’s outcome is logged so you can inspect or replay it.
Webhooks vs. the webhook step: the webhook step fires at one chosen point in a single sequence and sends data about the prospect reaching that step — it’s a sequence building block. Webhooks are account-wide: once configured, an endpoint receives every subscribed event across all of your sequences, is HMAC-signed, and comes with a delivery log and retries. If you need “call my endpoint when a prospect reaches this step,” use the webhook step. If you need “stream everything happening across my account to my system,” use webhooks.
Setting up an endpoint
Section titled “Setting up an endpoint”Setting up a webhook endpoint requires an admin or owner role on the team.
- Go to Settings → Webhooks.
- Click Add endpoint and enter your endpoint’s HTTPS URL and a description.
- Copy the signing secret (
whsec_...) shown on creation — it’s only displayed once. Store it securely; you’ll need it to verify signatures. - Choose which events to subscribe to: All events, or a specific list of event types.
- Optionally enable Include message bodies if you want email and LinkedIn message text, and form answer summaries, included in payloads (off by default — see Events).
- Click Send test event to deliver a
webhook.testevent through the real delivery pipeline and confirm your endpoint is reachable and your signature verification works.
Verifying signatures
Section titled “Verifying signatures”Every delivery includes these headers:
| Header | Description |
|---|---|
X-Vaam-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
X-Vaam-Event | The event type, e.g. linkedin.message.received |
X-Vaam-Delivery | <id>:<endpoint id>:<attempt number> |
To verify a delivery, compute an HMAC-SHA256 of ${t}.${rawBody} using your signing secret and compare it to v1 with a timing-safe comparison. Reject requests where the timestamp is more than 5 minutes old or in the future — this guards against replay.
const crypto = require("crypto");
function verifyVaamSignature(rawBody, signatureHeader, secret) { const parts = Object.fromEntries( signatureHeader.split(",").map((p) => p.split("=")), ); const t = parts.t; const v1 = parts.v1;
const expected = crypto .createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex");
const expectedBuf = Buffer.from(expected, "hex"); const receivedBuf = Buffer.from(v1, "hex"); if ( expectedBuf.length !== receivedBuf.length || !crypto.timingSafeEqual(expectedBuf, receivedBuf) ) { throw new Error("Invalid signature"); }
const ageSeconds = Math.abs(Date.now() / 1000 - Number(t)); if (ageSeconds > 5 * 60) { throw new Error("Timestamp outside tolerance"); }}Use the raw request body — not a re-serialized version of parsed JSON — since re-serializing can change byte-for-byte formatting and break the signature check.
Secret rotation: rotating an endpoint’s secret takes effect immediately. Every delivery sent after rotation is signed with the new secret, including retries of deliveries that were already queued, and the old secret stops working at once. Update your receiver with the new secret right away; until you do, it rejects Vaam’s deliveries, and any delivery that still fails after its retries is lost unless you redeliver it from the delivery log.
Events
Section titled “Events”Vaam supports 17 subscribable event types, plus a synthetic test event. Fields marked † are only included in data when Include message bodies is enabled on the endpoint; otherwise they’re omitted. Message bodies and text content — email body, LinkedIn message text, and form answer summaries — are truncated at 10,000 characters, with "truncated": true added to data when that happens. Other fields are not truncated. Form answer content additionally respects the form’s own answer-sharing setting: if the form isn’t configured to share full answers, answerSummary lists the answered fields with their values redacted, even on endpoints with Include message bodies enabled.
Sequence lifecycle
Section titled “Sequence lifecycle”| Event | Fires when | data fields |
|---|---|---|
sequence.started | A prospect starts a sequence | — |
sequence.completed | A prospect finishes every step of a sequence | — |
sequence.canceled | A prospect’s sequence is stopped before completion | reason (when available) |
| Event | Fires when | data fields |
|---|---|---|
email.sent | Vaam sends an email step | subject†, body† |
email.received | A prospect replies by email | — |
| Event | Fires when | data fields |
|---|---|---|
linkedin.message.sent | Vaam sends a LinkedIn message step | body† |
linkedin.message.received | A prospect replies on LinkedIn | messageText†, threadLink |
linkedin.profile.visited | Vaam visits a prospect’s LinkedIn profile as a step | — |
linkedin.connection.sent | A LinkedIn connection request is sent | — |
linkedin.connection.accepted | A prospect accepts the connection request | — |
linkedin.connection.expired | A sent connection request expires unaccepted | — |
linkedin.connection.accepted also fires when the prospect was already connected to you, or when you had already accepted their invitation. Vaam cannot tell these cases apart from a request that the prospect accepted.
Vaam sends one reply event (email.received or linkedin.message.received) each time a reply moves a prospect into the replied state. While a prospect stays in that state, further replies from them do not send another event. Resume the prospect in the sequence to receive events for later replies.
| Event | Fires when | data fields |
|---|---|---|
video.opened | A prospect opens a video for the first time | captureId |
video.playing | A prospect starts playing a video for the first time | captureId |
video.watched75 | A prospect watches at least 75% of a video | captureId, completionPercentage |
video.conversion | A prospect completes a video’s call-to-action. Vaam sends the first conversion of each type for each video. | captureId, conversionType |
Links & forms
Section titled “Links & forms”| Event | Fires when | data fields |
|---|---|---|
link.clicked | A prospect clicks a tracked link | linkUrl, linkId, stepId, clickedAt (click time as epoch milliseconds) |
form.submitted | A prospect submits a form | formTitle, answerSummary† |
| Event | Fires when | data fields |
|---|---|---|
webhook.test | You click Send test event in settings | message |
Payload
Section titled “Payload”Every event shares the same envelope. Here’s an example linkedin.message.received delivery:
{ "id": "5f1c6b1e-8a52-5c7e-9a3d-1b2c3d4e5f60", "event": "linkedin.message.received", "version": 1, "occurredAt": "2026-08-08T09:14:03.000Z", "prospect": { "id": "abc123", "firstname": "Jane", "lastname": "Doe", "email": "jane@example.com", "company": "Acme Inc", "jobtitle": "VP Sales", "phone": "+1234567890", "linkedinProfileUrl": "https://linkedin.com/in/janedoe", "tags": ["enterprise", "inbound"], "custom": "...", "custom2": "...", "custom3": "..." }, "enrollment": { "id": "enr456", "sequenceId": "seq789", "sequenceName": "Enterprise Outreach Q1" }, "data": { "threadLink": "https://www.linkedin.com/messaging/thread/...", "messageText": "Sounds interesting — can you send more info?" }}idis the event’s deduplication key — use it to detect and discard duplicate deliveries.prospectreflects prospect data as of when the event was prepared for delivery, not as of delivery time — the payload is built once and reused as-is for every retry or redelivery, so those all resend the exact same bytes rather than a freshly looked-up copy. If the prospect had been deleted by the time the payload was built,prospectdegrades to{ "id": "..." }.enrollmentcarries the sequence run this event belongs to: its id, the sequence’s id, and the sequence’s name — this is the prospect’s progress in a sequence, not a separate concept to configure.
Delivery
Section titled “Delivery”- Delivery is at-least-once. Use
idto deduplicate on your end — the same event can arrive more than once. - There’s no ordering guarantee across events. Order events using
occurredAt, and expect near-simultaneous events for the same prospect activity. - A 2xx response within 10 seconds counts as delivered. Anything else — a timeout, a non-2xx status, a connection error — is retried.
- Redirects are not followed — a 3xx response counts as a failed attempt. Point the endpoint at its final URL.
- Failed deliveries are retried up to 6 times over roughly 31 minutes (7 attempts total, including the first), with exponential backoff: 30s, 1m, 2m, 4m, 8m, 16m after the first attempt.
- Endpoints subscribed to All events automatically receive any new event types Vaam adds later. Endpoints with an explicit event list do not — you need to add new types yourself.
Failure handling
Section titled “Failure handling”The endpoint’s delivery log in settings records the final outcome of each delivery — delivered, or failed after all retries — not every intermediate attempt. Delivered rows show the status code and timing; failed rows show the error and, when available, the HTTP status. Delivered entries are kept for 7 days and failed entries for 30 days, and the settings page shows the 50 most recent. Failed deliveries can be redelivered from the log — a redelivery resends the exact original payload, not a freshly generated one.
Sustained failures escalate in two stages:
- Warning email — sent to team admins and owners once an endpoint has roughly 10 consecutive failures with no successful delivery in 6 hours. At most one warning email is sent per 24 hours.
- Automatic disable — an endpoint is disabled once it reaches roughly 100 consecutive failures with no successful delivery in 3 days. You’ll get an email when this happens. Re-enable the endpoint with a single toggle in settings after fixing the receiving end — this resets the failure counters.
Transient failures alone never disable an endpoint or change its configuration.
URL requirements
Section titled “URL requirements”- Must use HTTPS — HTTP URLs are not accepted. This, along with the hostname format and the check against Vaam domains, is validated when you save the endpoint.
- Must resolve to a public address. This is enforced at delivery time, not when you save the endpoint — if the hostname resolves to a private or internal network address when a delivery is attempted, that delivery fails rather than the endpoint being rejected at setup.
- Cannot point at a Vaam domain.
Example use cases
Section titled “Example use cases”- Sync activity to an internal CRM or data warehouse — keep your own system up to date with outreach activity without polling.
- Notify Slack through your own service that consumes the webhook and posts to a channel.
- Trigger custom automations — kick off internal workflows when a prospect replies, converts on a video, or submits a form.