Webhooks
Plug-N-Meet can notify your application about various events by sending webhook requests to a specified URL. You can configure this URL in the server settings or provide it when creating a room.
Receiving Webhooks
Webhook requests are sent as HTTP POST requests to the URLs you have configured, either in the server's config.yml file or during room creation. Each webhook event is encoded as JSON and included in the request body.
The request will have the Content-Type header set to application/webhook+json. Make sure your web server is configured to accept payloads with this content type.
Plug-N-Meet uses the same security pattern as LiveKit. To verify that webhook requests originate from Plug-N-Meet, each request includes Authorization and Hash-Token headers containing a signed JWT token. The token includes a SHA256 hash of the payload. For a PHP example, see webhook.php.
Events
You can review the event definitions here.
All webhook events include the following fields:
id: A UUID identifying the eventcreatedAt: UNIX timestamp in seconds
Room Created
interface CommonNotifyEvent {
event: 'room_created'
room: Room
}
Room Started
interface CommonNotifyEvent {
event: 'room_started'
room: Room
}
Room Finished
interface CommonNotifyEvent {
event: 'room_finished'
room: Room
}
Participant Joined
interface CommonNotifyEvent {
event: 'participant_joined'
room: Room
participant: ParticipantInfo
}
Participant Left
interface CommonNotifyEvent {
event: 'participant_left'
room: Room
participant: ParticipantInfo
}
Track Published
Only sid, identity, and name are included in the Room and Participant objects.
interface CommonNotifyEvent {
event: 'track_published'
room: Room
participant: ParticipantInfo
track: TrackInfo
}
Track Unpublished
Only sid, identity, and name are included in the Room and Participant objects.
interface CommonNotifyEvent {
event: 'track_unpublished'
room: Room
participant: ParticipantInfo
track: TrackInfo
}
Recording Started
interface CommonNotifyEvent {
event: 'start_recording'
room: Room
recording_info: RecordingInfoEvent
}
Recording Ended
interface CommonNotifyEvent {
event: 'end_recording'
room: Room
recording_info: RecordingInfoEvent
}
Recording Proceeded
interface CommonNotifyEvent {
event: 'recording_proceeded'
room: Room
recording_info: RecordingInfoEvent
}
RTMP Started
interface CommonNotifyEvent {
event: 'start_rtmp'
room: Room
recording_info: RecordingInfoEvent
}
RTMP Ended
interface CommonNotifyEvent {
event: 'end_rtmp'
room: Room
recording_info: RecordingInfoEvent
}
Artifact Created
This single, unified event is triggered whenever a new artifact is generated by the system, such as a transcription or a meeting summary. It replaces multiple older, individual webhooks with a more flexible and scalable model.
For details on the payload structure and possible artifact types, please refer to the protobuf definition.
interface CommonNotifyEvent {
event: 'artifact_created'
room: Room
room_artifact: RoomArtifactWebhookEvent
}
Analytics Proceeded
interface CommonNotifyEvent {
event: 'analytics_proceeded'
room: Room
analytics: AnalyticsEvent
}
Example Webhook Handler
The following pseudo-code demonstrates how to handle incoming webhooks. Your application should inspect the event property to determine what action to take. The artifact_created event is special, as it requires checking a nested type field to understand the payload.
// A mapping from the enum number to a human-readable string.
// See the protobuf definition for the complete, up-to-date list.
const ArtifactType = {
0: 'UNKNOWN_ARTIFACT',
2: 'MEETING_SUMMARY',
4: 'SPEECH_TRANSCRIPTION',
5: 'SPEECH_TRANSCRIPTION_USAGE',
};
function handleWebhook(payload) {
console.log(`Received event: ${payload.event}`);
const roomInfo = payload.room;
switch (payload.event) {
case 'room_started':
console.log(`Room '${roomInfo.name}' has started.`);
break;
case 'participant_joined':
console.log(`User '${payload.participant.name}' joined room '${roomInfo.name}'.`);
break;
case 'artifact_created':
handleArtifactCreated(payload);
break;
case 'room_finished':
console.log(`Room '${roomInfo.name}' has finished.`);
break;
default:
console.log(`Received an unhandled event: ${payload.event}`);
}
}
// A dedicated handler for the 'artifact_created' event
function handleArtifactCreated(payload) {
const artifact = payload.room_artifact;
const artifactType = ArtifactType[artifact.type] || 'UNKNOWN_ARTIFACT';
console.log(`A new artifact of type '${artifactType}' was created.`);
switch (artifactType) {
case 'MEETING_SUMMARY':
case 'SPEECH_TRANSCRIPTION':
// These are file-based artifacts.
if (artifact.metadata && artifact.metadata.file_info) {
const fileInfo = artifact.metadata.file_info;
console.log(`File available at: ${fileInfo.file_path}`);
// Your logic here: e.g., download the file, link it to a recording.
}
break;
case 'SPEECH_TRANSCRIPTION_USAGE':
// This is a usage-based artifact.
if (artifact.metadata && artifact.metadata.duration_usage) {
const usage = artifact.metadata.duration_usage;
console.log(`Transcription usage logged: ${usage.duration_sec} seconds.`);
// Your logic here: e.g., update billing records.
}
break;
default:
console.log(`Received an unhandled artifact type: ${artifactType}`);
}
}
// Example incoming payload for 'artifact_created' (a meeting summary)
const samplePayload = {
"event": "artifact_created",
"id": "EV_abc123",
"createdAt": 1761815000,
"room": {
"sid": "RM_zG9e4bY2pD-1761814595173",
"room_id": "room01",
"name": "Test Room"
},
"room_artifact": {
"type": 2, // Corresponds to MEETING_SUMMARY
"artifact_id": "ART_xyz789",
"metadata": {
"file_info": {
"file_path": "/path/to/summary.txt",
"file_size": 1024,
"mime_type": "text/plain"
}
}
}
};
// You would call your handler with the body of the POST request.
handleWebhook(samplePayload);