| name | twinmind-webhooks-events |
| description | Handle TwinMind webhooks and events for real-time meeting notifications.
Use when implementing webhook handlers, processing meeting events,
or building real-time integrations.
Trigger with phrases like "twinmind webhooks", "twinmind events",
"twinmind notifications", "meeting webhook handler".
|
| allowed-tools | Read, Write, Edit, Bash(npm:*), Grep |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
TwinMind Webhooks & Events
Overview
Implement webhook handlers for real-time TwinMind meeting events and notifications.
Prerequisites
- TwinMind Pro/Enterprise account
- Public HTTPS endpoint for webhooks
- Webhook secret configured
- Understanding of event-driven architecture
Instructions
Step 1: Define Event Types
export enum TwinMindEventType {
TRANSCRIPTION_STARTED = 'transcription.started',
TRANSCRIPTION_COMPLETED = 'transcription.completed',
TRANSCRIPTION_FAILED = 'transcription.failed',
MEETING_STARTED = 'meeting.started',
MEETING_ENDED = 'meeting.ended',
MEETING_PARTICIPANT_JOINED = 'meeting.participant.joined',
MEETING_PARTICIPANT_LEFT = 'meeting.participant.left',
SUMMARY_GENERATED = 'summary.generated',
ACTION_ITEMS_EXTRACTED = 'action_items.extracted',
CALENDAR_SYNCED = 'calendar.synced',
CALENDAR_EVENT_REMINDER = 'calendar.event.reminder',
USAGE_LIMIT_WARNING = 'usage.limit.warning',
USAGE_LIMIT_EXCEEDED = 'usage.limit.exceeded',
}
export interface TwinMindEvent<T = any> {
id: string;
type: TwinMindEventType;
created_at: string;
data: T;
}
export interface TranscriptionCompletedData {
transcript_id: string;
duration_seconds: number;
language: string;
word_count: number;
speaker_count: number;
model: string;
}
export interface MeetingEndedData {
meeting_id: string;
transcript_id: string;
title: string;
duration_seconds: number;
participants: string[];
summary_available: boolean;
}
export interface SummaryGeneratedData {
summary_id: string;
transcript_id: string;
action_item_count: number;
key_point_count: number;
}
export interface ActionItemsExtractedData {
transcript_id: string;
action_items: Array<{
text: string;
assignee?: string;
due_date?: string;
}>;
}
Step 2: Implement Webhook Handler
import crypto from 'crypto';
import express, { Request, Response, NextFunction } from 'express';
import { TwinMindEvent, TwinMindEventType } from '../events/types';
export function verifySignature(
req: Request,
res: Response,
next: NextFunction
): void {
const signature = req.headers['x-twinmind-signature'] as string;
const timestamp = req.headers['x-twinmind-timestamp'] as string;
const secret = process.env.TWINMIND_WEBHOOK_SECRET!;
if (!signature || !timestamp) {
res.status(401).json({ error: 'Missing signature or timestamp' });
return;
}
const timestampMs = parseInt(timestamp) * ;
now = .();
(.(now - timestampMs) > * * ) {
res.().({ : });
;
}
payload = ;
expectedSignature = crypto
.(, secret)
.(payload)
.();
(!crypto.(
.(signature),
.()
)) {
res.().({ : });
;
}
();
}
<T = > = <>;
handlers = <, []>();
registerHandler<T>(
: ,
: <T>
): {
existing = handlers.(eventType) || [];
existing.(handler);
handlers.(eventType, existing);
}
(): <> {
event = req. ;
.();
res.().({ : , : event. });
{
eventHandlers = handlers.(event. );
(eventHandlers && eventHandlers. > ) {
.(
eventHandlers.( (event))
);
} {
.();
}
} (error) {
.(, error);
}
}
Step 3: Implement Event Handlers
import {
TwinMindEvent,
TwinMindEventType,
TranscriptionCompletedData,
MeetingEndedData,
SummaryGeneratedData,
ActionItemsExtractedData,
} from '../events/types';
import { registerHandler } from './handler';
import { notifySlack, sendEmail } from '../notifications';
import { createTasksInLinear } from '../integrations/linear';
registerHandler<TranscriptionCompletedData>(
TwinMindEventType.TRANSCRIPTION_COMPLETED,
async (event) => {
const { transcript_id, duration_seconds, word_count, speaker_count } = event.data;
console.log(`Transcription completed: ${transcript_id}`);
console.log(` Duration: ${duration_seconds}s`);
console.log(` Words: ${word_count}`);
console.log(` Speakers: ${speaker_count}`);
const client = getTwinMindClient();
client.(, { transcript_id });
}
);
registerHandler<>(
.,
(event) => {
{ meeting_id, title, duration_seconds, participants, summary_available } = event.;
.();
({
: ,
: ,
participants,
});
(summary_available) {
client = ();
summary = client.();
({
: participants,
: ,
: summary..,
});
}
}
);
registerHandler<>(
.,
(event) => {
{ summary_id, transcript_id, action_item_count } = event.;
.();
.();
db..({
: summary_id,
transcript_id,
: (event.),
});
}
);
registerHandler<>(
.,
(event) => {
{ transcript_id, action_items } = event.;
.();
(action_items. > ) {
(action_items);
}
}
);
(
.,
(event) => {
.(, event.);
({
: ,
: ,
});
}
);
Step 4: Set Up Webhook Endpoint
import express from 'express';
import { verifySignature, handleWebhook } from '../../twinmind/webhooks/handler';
const router = express.Router();
router.use(express.json({
verify: (req: any, res, buf) => {
req.rawBody = buf;
}
}));
router.post(
'/twinmind',
verifySignature,
handleWebhook
);
export default router;
Step 5: Register Webhooks
import { getTwinMindClient } from '../src/twinmind/client';
import { TwinMindEventType } from '../src/twinmind/events/types';
async function registerWebhooks() {
const client = getTwinMindClient();
const webhookUrl = process.env.WEBHOOK_BASE_URL + '/webhooks/twinmind';
const response = await client.post('/webhooks', {
url: webhookUrl,
events: [
TwinMindEventType.TRANSCRIPTION_COMPLETED,
TwinMindEventType.MEETING_ENDED,
TwinMindEventType.SUMMARY_GENERATED,
TwinMindEventType.ACTION_ITEMS_EXTRACTED,
TwinMindEventType.USAGE_LIMIT_WARNING,
],
enabled: true,
});
console.log('Webhook registered:', response.data);
console.log('Webhook Secret:', response.data.secret);
.( + response..);
}
();
Step 6: Implement Retry Logic for Failed Events
import { TwinMindEvent } from '../events/types';
interface FailedEvent {
event: TwinMindEvent;
attempts: number;
lastError: string;
nextRetry: Date;
}
class WebhookRetryQueue {
private queue: FailedEvent[] = [];
private maxRetries = 5;
private baseDelayMs = 60000;
async add(event: TwinMindEvent, error: Error): Promise<void> {
const existing = this.queue.find(f => f.event.id === event.id);
if (existing) {
existing.attempts += 1;
existing.lastError = error.message;
existing.nextRetry = new Date(
Date.now() + . * .(, existing.)
);
(existing. >= .) {
.(existing);
. = ..( f.. !== event.);
}
} {
..({
event,
: ,
: error.,
: (.() + .),
});
}
}
(): <> {
now = ();
readyEvents = ..( f. <= now);
( failedEvent readyEvents) {
{
.(failedEvent.);
. = ..( f.. !== failedEvent..);
.();
} (: ) {
.(failedEvent., error);
}
}
}
(: ): <> {
handlers = (event.);
.(handlers.( (event)));
}
(: ): <> {
.();
db..({
: failedEvent..,
: failedEvent..,
: failedEvent.,
: failedEvent.,
: failedEvent.,
});
({
: ,
: ,
});
}
}
retryQueue = ();
( retryQueue.(), );
Output
- Event type definitions
- Webhook handler with signature verification
- Event processing logic
- Webhook registration script
- Retry queue for failed events
Webhook Events Reference
| Event | Description | Data |
|---|
transcription.started | Transcription job started | transcript_id, audio_url |
transcription.completed | Transcription finished | transcript_id, duration, word_count |
transcription.failed | Transcription failed | transcript_id, error |
meeting.started | Live meeting capture started | meeting_id, title |
meeting.ended | Meeting finished | meeting_id, transcript_id, participants |
summary.generated | AI summary ready | summary_id, action_item_count |
action_items.extracted | Action items available | transcript_id, action_items[] |
usage.limit.warning | Usage approaching limit | percent_used, limit |
Error Handling
| Issue | Cause | Solution |
|---|
| Invalid signature | Wrong secret | Verify webhook secret |
| Event missed | Endpoint down | Implement retry queue |
| Processing slow | Heavy handler | Use async queue |
| Duplicate events | Retries | Implement idempotency |
Resources
Next Steps
For performance optimization, see twinmind-performance-tuning.