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.

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"
> }'
1{
2 "success": true,
3 "webhook": {
4 "id": "wh_abc123",
5 "url": "https://your-server.com/webhook",
6 "events": ["message_received", "connection_received", "session_expired"],
7 "createdAt": "2026-03-17T10:00:00Z"
8 }
9}

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:

1{
2 "event": "message_received",
3 "timestamp": "2026-03-17T10:30:00Z",
4 "tenantId": "32e365dc-e8c4-4caa-8637-0e6b48dfeccd",
5 "userId": "charis",
6 "payload": {
7 "conversationId": "conv123",
8 "senderName": "Jane Smith",
9 "lastMessage": "Thanks for connecting! Would love to chat about...",
10 "senderProfileUrl": "https://www.linkedin.com/in/janesmith/"
11 }
12}

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)

1const crypto = require('crypto');
2
3function verifyWebhook(payload, signature, secret) {
4 const expected = crypto
5 .createHmac('sha256', secret)
6 .update(JSON.stringify(payload))
7 .digest('hex');
8 return crypto.timingSafeEqual(
9 Buffer.from(signature),
10 Buffer.from(expected)
11 );
12}
13
14// In your webhook handler
15app.post('/webhook', (req, res) => {
16 const sig = req.headers['x-webhook-signature'];
17 if (!verifyWebhook(req.body, sig, 'your-hmac-secret')) {
18 return res.status(401).send('Invalid signature');
19 }
20 // Process the event
21 console.log(req.body.event, req.body.payload);
22 res.status(200).send('OK');
23});

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/wh_abc123" \
> -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/wh_abc123/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 (configurable via POLL_INTERVAL_MS). 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.