Webhooks

Webhooks

Voyager can push real-time events to your server via webhooks. Instead of polling for new messages or connection requests, configure a webhook URL and Voyager will POST events as they happen. Payloads are signed with HMAC-SHA256, and Slack-formatted delivery is built in.

Management endpoints return the { success, statusCode, message, data, errors } envelope. The JSON examples below show only the endpoint-specific payload — access res.data.<field> in your client. Webhook deliveries to your server use the raw event shape below, not the envelope.

Create a Webhook

curl -X POST "$BASE/api/webhooks" \
-H "Authorization: Bearer $KEY" \
-H "X-User-Id: $USER" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhook",
"events": ["message_received", "connection_received", "session_expired"],
"secret": "your-hmac-secret"
}'
{
"success": true,
"webhook": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://your-server.com/webhook",
"events": ["message_received", "connection_received", "session_expired"],
"secret": "***",
"active": true,
"createdAt": "2026-03-17T10:00:00Z",
"updatedAt": "2026-03-17T10:00:00Z"
}
}

Note: id is a UUID, and the secret you sent is masked to "***" in every response — Voyager never echoes it back. Save it on your side when you create the webhook.

Available Event Types

Inbound Signals

EventDescription
message_receivedNew message in your inbox
connection_receivedNew inbound connection request
post_likedSomeone liked your post
post_commentedSomeone commented on your post
comment_repliedSomeone replied to your comment

Action Results

EventDescription
action_send_completedMessage send succeeded
action_send_failedMessage send failed
action_connect_completedConnection request succeeded
action_connect_failedConnection request failed
action_like_completedLike action succeeded
action_comment_completedComment action succeeded
action_post_completedPost creation succeeded
action_visit_completedProfile visit succeeded

System Events

EventDescription
session_expiredLinkedIn session expired, needs re-authentication
job_completedAsync job finished successfully
job_failedAsync job failed

Payload Format

All webhook payloads follow this structure:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"event": "message_received",
"tenantId": "32e365dc-e8c4-4caa-8637-0e6b48dfeccd",
"tenantName": "Acme",
"timestamp": "2026-03-17T10:30:00Z",
"data": {
"userId": "charis",
"conversationId": "conv123",
"participantName": "Jane Smith",
"participantUrn": "urn:li:fsd_profile:ACoAA...",
"lastMessage": "Thanks for connecting! Would love to chat about..."
}
}
  • id is a UUID per delivery — useful for idempotency on your side
  • event is one of the types in the catalogue above
  • tenantName is the human-readable tenant label
  • data varies by event type; the userId of the LinkedIn account the event belongs to lives inside data (not at the top level)

HMAC Signature Verification

Voyager signs every webhook payload with HMAC-SHA256 using the secret you provided at creation. The signature is in the X-Webhook-Signature header.

Verification Example (Node.js)

const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler
app.post('/webhook', (req, res) => {
const sig = req.headers['x-webhook-signature'];
if (!verifyWebhook(req.body, sig, 'your-hmac-secret')) {
return res.status(401).send('Invalid signature');
}
// Process the event
console.log(req.body.event, req.body.data);
res.status(200).send('OK');
});

Always verify the HMAC signature before processing webhook payloads. Without verification, an attacker could send fake events to your endpoint.

Slack-Native Formatting

If your webhook URL points to a Slack incoming webhook (hooks.slack.com), Voyager automatically formats payloads as Slack messages with rich formatting — no middleware needed.

curl -X POST "$BASE/api/webhooks" \
-H "Authorization: Bearer $KEY" \
-H "X-User-Id: $USER" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.slack.com/services/T.../B.../xxx",
"events": ["message_received", "connection_received", "session_expired"]
}'

For Slack webhooks, you do not need to provide a secret — Slack handles authentication via the webhook URL itself. Voyager detects hooks.slack.com URLs and switches to Slack message format automatically.

For production setups, use two webhooks:

  1. Main channel — inbound signals: message_received, connection_received, post_liked, post_commented, comment_replied, session_expired
  2. Test/ops channel — action results: action_send_completed, action_send_failed, action_connect_completed, action_connect_failed, job_completed, job_failed

This prevents test runs and action confirmations from drowning out real inbound signals.

List Webhooks

curl "$BASE/api/webhooks" \
-H "Authorization: Bearer $KEY" \
-H "X-User-Id: $USER"

Delete a Webhook

curl -X DELETE "$BASE/api/webhooks/<webhookId>" \
-H "Authorization: Bearer $KEY" \
-H "X-User-Id: $USER"

Test a Webhook

Send a test payload to verify your endpoint is working:

curl -X POST "$BASE/api/webhooks/<webhookId>/test" \
-H "Authorization: Bearer $KEY" \
-H "X-User-Id: $USER"

Delivery Log

Check recent delivery attempts and their HTTP status codes:

curl "$BASE/api/webhooks/deliveries" \
-H "Authorization: Bearer $KEY" \
-H "X-User-Id: $USER"

How Events Are Generated

Voyager’s background PollService checks for new messages, connection requests, and notifications every 5 minutes per tenant. When it detects changes, it fires the corresponding webhook events.

The first poll after a session sync takes a state snapshot without firing events — this prevents a flood of historical events on first connect. Only subsequent polls emit deltas.

If all three poll sections (messages, invitations, notifications) fail simultaneously with session errors, Voyager emits a session_expired event and pauses polling for that user until the next cookie sync.