This document describes the scope and payload for subscribers of the virtual-stage-event-ended
event through webhooks.
Overview
The Virtual Stage Event Ended webhook sends comprehensive engagement data for all attendees after a live event concludes. Unlike the Custom Integration which sends individual user records, this webhook aggregates all users and sends them in configurable batches.
Key Differences:
- Custom Integration: Sends one POST per user (e.g., 100 attendees = 100 requests)
- Virtual Stage Event Ended: Sends all users in configurable batches (e.g., 100 attendees = 1-10 requests depending on batch size, or just 1 request if configured to send all)
Batch Size Options:
- Configure batch size in webhook settings (10, 50, 100, 500, etc.)
- Or select "Send All" to receive all users in a single request
- Default is typically 50-100 users per batch
Scope
The event is subscribed at the company level.
- Subscription Coverage: All events owned by the company and its child companies
- Trigger: Event is invoked when the host clicks on the "End Event" or "End Event and Start Networking" button on the virtual stage
- Frequency: Once per event (when ended)
- Batch Configuration: Configure how many users to send per request, or send all users at once
Payload Structure
Request
A HTTP POST request is made to the endpoint registered as the webhook for the virtual stage event ended event.
Body
{
"eventType": "virtual-stage-event-ended",
"webHookInvocationId": "unique-web-hook-invocation-id",
"correlationId": "unique-correlation-id-for-this-event",
"timestamp": "2025-01-07T10:05:24.000Z",
"details": {
"companyId": "d5907d53-e996-4db5-9575-6b9c17b0df1e",
"companyName": "Acme Corporation",
"eventId": "8d6e1e40-9deb-430d-8c17-33bd26fe70a1",
"eventName": "Q1 2025 Product Launch Webinar",
"totalCount": 150,
"page": 1,
"size": 50,
"users": [
{
"_id": "65e6bfaddab97d4fc595cc08",
"uid": "380c2455-1a9c-49a5-b691-761563c495b3",
"userId": "user123|||8d6e1e40-9deb-430d-8c17-33bd26fe70a1",
"name": "John Doe",
"email": "[email protected]",
"eventId": "8d6e1e40-9deb-430d-8c17-33bd26fe70a1",
"companyId": "d5907d53-e996-4db5-9575-6b9c17b0df1e",
"attended": true,
"viewedReplay": false,
"registrationDate": "2025-01-05T06:27:51.414Z",
"createdOn": "2025-01-07T06:46:03.025Z",
"updatedOn": "2025-01-07T10:15:30.125Z",
"liveTime": 45,
"onDemandTime": 0,
"averageLiveViewTime": 90,
"averageOnDemandViewTime": 0,
"liveTimeSpentPercentage": 90,
"onDemandTimeSpentPercentage": 0,
"engagementScore": 85,
"leadScore": 42,
"rawLeadScore": 168,
"leadStatus": ["Attended"],
"leadScoreDetails": {
"attendedLiveSession": 10,
"askedQuestion": 5,
"answeredPoll": 3,
"clickedCTA": 8,
"viewedResource": 4
},
"questions": [
{
"id": "q1",
"question": "Does this integrate with Salesforce?",
"timestamp": "2025-01-07T10:08:15.000Z"
},
{
"id": "q2",
"question": "What's the pricing model?",
"timestamp": "2025-01-07T10:12:30.000Z"
}
],
"questionsNumber": 2,
"polls": [
{
"id": "poll1",
"question": "What feature interests you most?",
"answer": "API Integration",
"timestamp": "2025-01-07T10:05:00.000Z"
},
{
"id": "poll2",
"question": "Would you recommend this?",
"answer": "Yes",
"timestamp": "2025-01-07T10:20:00.000Z"
}
],
"pollsNumber": 2,
"ctaClicks": [
{
"id": "cta1",
"name": "Download Whitepaper",
"url": "https://example.com/whitepaper.pdf",
"timestamp": "2025-01-07T10:10:00.000Z"
},
{
"id": "cta2",
"name": "Schedule Demo",
"url": "https://example.com/demo",
"timestamp": "2025-01-07T10:25:00.000Z"
}
],
"ctaClicksNumber": 2,
"resourceClicks": [
{
"id": "res1",
"name": "Product Deck",
"url": "https://cdn.example.com/product-deck.pdf",
"timestamp": "2025-01-07T10:15:00.000Z"
}
],
"resourceClicksNumber": 1,
"circles": [],
"circlesNumber": 0,
"comments": 0,
"messagesReactionsCount": 0
},
{
"_id": "65e6c1b2dab97d4fc595cc09",
"uid": "490d3566-2b0d-5aa6-c7a2-872674d5a6c4",
"userId": "user456|||8d6e1e40-9deb-430d-8c17-33bd26fe70a1",
"name": "Jane Smith",
"email": "[email protected]",
"eventId": "8d6e1e40-9deb-430d-8c17-33bd26fe70a1",
"companyId": "d5907d53-e996-4db5-9575-6b9c17b0df1e",
"attended": false,
"viewedReplay": true,
"registrationDate": "2025-01-04T14:22:15.414Z",
"createdOn": "2025-01-08T09:30:00.000Z",
"updatedOn": "2025-01-08T10:00:00.000Z",
"liveTime": 0,
"onDemandTime": 30,
"averageLiveViewTime": 0,
"averageOnDemandViewTime": 60,
"liveTimeSpentPercentage": 0,
"onDemandTimeSpentPercentage": 60,
"engagementScore": 45,
"leadScore": 18,
"rawLeadScore": 72,
"leadStatus": ["Viewed On Demand"],
"leadScoreDetails": {
"viewedRecording": 8,
"clickedCTA": 5
},
"questions": [],
"questionsNumber": 0,
"polls": [],
"pollsNumber": 0,
"ctaClicks": [
{
"id": "cta1",
"name": "Download Whitepaper",
"url": "https://example.com/whitepaper.pdf",
"timestamp": "2025-01-08T09:45:00.000Z"
}
],
"ctaClicksNumber": 1,
"resourceClicks": [],
"resourceClicksNumber": 0,
"circles": [],
"circlesNumber": 0,
"comments": 0,
"messagesReactionsCount": 0
}
]
}
}
User Engagement Fields
Each user object in the users
array contains the following fields:
Identity Fields
Field | Type | Description |
---|---|---|
_id | String | MongoDB document ID |
uid | String | Unique user identifier |
userId | String | Composite user ID (email|||eventId) |
name | String | Full name of the user |
email | String | Email address of the user |
eventId | String | ID of the event |
companyId | String | ID of the company owning the event |
Attendance & Timing
Field | Type | Description |
---|---|---|
attended | Boolean | true if user attended the live event, false otherwise |
viewedReplay | Boolean | true if user watched the on-demand recording |
registrationDate | String (ISO 8601) | When the user registered |
createdOn | String (ISO 8601) | When the engagement record was created |
updatedOn | String (ISO 8601) | When the engagement record was last updated |
Viewing Time Metrics
Field | Type | Description |
---|---|---|
liveTime | Number | Minutes watched during live event |
onDemandTime | Number | Minutes watched on-demand/replay |
averageLiveViewTime | Number | Average percentage of live content watched |
averageOnDemandViewTime | Number | Average percentage of on-demand content watched |
liveTimeSpentPercentage | Number | Percentage of live event duration watched (0-100) |
onDemandTimeSpentPercentage | Number | Percentage of recording watched (0-100) |
Engagement & Scoring
Field | Type | Description |
---|---|---|
engagementScore | Number | Overall engagement score (0-100) |
leadScore | Number | Calculated lead score |
rawLeadScore | Number | Raw lead score before normalization |
leadStatus | Array[String] | Lead status values (e.g., ["Attended"], ["Viewed On Demand"]) |
leadScoreDetails | Object | Breakdown of lead score components |
Lead Score Details Object
{
"attendedLiveSession": 10,
"askedQuestion": 5,
"answeredPoll": 3,
"clickedCTA": 8,
"viewedResource": 4,
"viewedRecording": 8,
"timeSpent": 12
}
Questions
Field | Type | Description |
---|---|---|
questions | Array | Array of question objects asked by the user |
questionsNumber | Number | Total number of questions asked |
Question Object:
{
"id": "q1",
"question": "Question text",
"timestamp": "2025-01-07T10:08:15.000Z"
}
Polls
Field | Type | Description |
---|---|---|
polls | Array | Array of poll response objects |
pollsNumber | Number | Total number of polls answered |
Poll Object:
{
"id": "poll1",
"question": "Poll question text",
"answer": "User's answer",
"timestamp": "2025-01-07T10:05:00.000Z"
}
CTA Clicks
Field | Type | Description |
---|---|---|
ctaClicks | Array | Array of CTA click objects |
ctaClicksNumber | Number | Total number of CTAs clicked |
CTA Click Object:
{
"id": "cta1",
"name": "CTA name/title",
"url": "https://example.com/destination",
"timestamp": "2025-01-07T10:10:00.000Z"
}
Resource Clicks
Field | Type | Description |
---|---|---|
resourceClicks | Array | Array of resource click objects |
resourceClicksNumber | Number | Total number of resources clicked |
Resource Click Object:
{
"id": "res1",
"name": "Resource name",
"url": "https://cdn.example.com/resource.pdf",
"timestamp": "2025-01-07T10:15:00.000Z"
}
Social Engagement
Field | Type | Description |
---|---|---|
circles | Array | Array of circles (networking groups) the user joined |
circlesNumber | Number | Total number of circles joined |
comments | Number | Number of comments posted |
messagesReactionsCount | Number | Number of reactions to messages |
Headers
The following headers are included in the request:
Header Key | Value | Description |
---|---|---|
x-introvoke-web-hook-auth | Authentication key | The key to validate the authenticity of the request |
user-agent | introvoke-web-hook | Static value |
content-type | application/json | Static value |
Validating Requests
Always validate the x-introvoke-web-hook-auth
header to ensure the request is legitimate:
const expectedAuthKey = process.env.WEBHOOK_AUTH_KEY;
const receivedAuthKey = req.headers['x-introvoke-web-hook-auth'];
if (receivedAuthKey !== expectedAuthKey) {
return res.status(401).json({ error: 'Unauthorized' });
}
Responses
The expected responses from the webhook endpoint:
HTTP Response Code | Description | Retry? |
---|---|---|
200 | The message was accepted and request was successful | N/A |
400 | The message was malformed | ❌ Do not retry |
401 | The authentication failed and message could not be processed | ❌ Do not retry |
404 | The endpoint does not exist | ❌ Do not retry |
429 | Request quota was exceeded | ✅ Retry after some time |
500 | Internal server error | ✅ Retry after some time |
Best Practices for Responses
- Respond Quickly: Return a
200
response within 5 seconds - Process Asynchronously: Queue the data for processing after responding
- Handle Errors Gracefully: Log errors but still return appropriate status codes
- Implement Idempotency: Use
webHookInvocationId
to detect and handle duplicates
Example:
app.post('/webhook/event-ended', async (req, res) => {
// Validate auth
if (req.headers['x-introvoke-web-hook-auth'] !== expectedKey) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Respond immediately
res.status(200).json({ success: true });
// Process asynchronously
const payload = req.body;
processEventData(payload).catch(err => {
console.error('Error processing webhook:', err);
});
});
Pagination
When an event has many attendees, the data is paginated to keep payloads manageable.
Pagination Fields
Field | Type | Description |
---|---|---|
totalCount | Number | Total number of users across all pages |
page | Number | Current page number (1-indexed) |
size | Number | Number of users in this page |
Configuring Batch Size
Batch size is configurable! When setting up your webhook subscription, you can specify:
- Custom Batch Size: Set the number of users per request (e.g., 10, 50, 100, 500)
-
Send All at Once: Configure to send all users in a single request (no pagination)
Configuration Options:
Setting Example Use Case Small batches 10-25 users Real-time processing, rate-limited APIs Medium batches 50-100 users Balanced approach (default) Large batches 250-500 users Bulk imports, data warehouses No pagination All users Small events (<100), systems optimized for large payloads To Configure:
- Go to your webhook subscription settings in the Sequel Dashboard
- Find the "Batch Size" or "Users Per Request" setting
- Enter your preferred size or select "Send All"
-
Handling Pagination
You'll receive multiple webhook invocations for the same event if there are more users than fit in one configured page.
Identify pages for the same event:
- Same
eventId
indetails
- Same
correlationId
(groups all pages together) - Different
webHookInvocationId
for each page - Different
page
number