Mirra
BuildEvents

Voice Call Events

Voice call events fire during and after voice calls in your Mirra account. These events provide call metadata, real-time transcriptions, and AI-generated summaries, with support for ticker detection and sentiment analysis in transcripts.


Event Types

call.started

The call.started event fires when a voice call begins. This event captures call metadata and participant information.

Event Type:

type: 'call.started'
source: 'call'

Fields:

content.text (string)

Empty string for call start events.

content.contentType (ContentType)

Always 'call' for voice call events.

call.agoraCallId (string)

Agora RTC call identifier.

call.mirraCallId (string)

Mirra internal call identifier.

call.chatInstanceId (string)

Associated chat instance ID.

call.participants (CallParticipant[])

Array of participants in the call.

FieldTypeDescription
userIdstringUser's Mirra ID
usernamestringUser's display name
joinedAtDateWhen participant joined
leftAtDate | nullWhen participant left (null if still in call)
agoraUidnumber | nullAgora user ID
call.isRecording (boolean)

True if the call is being recorded.

Example - Call Logger:

export async function handler(event, context) {
  // Log call start
  await mirra.memory.create({
    type: 'call_log',
    content: `Call started with ${event.call.participants.length} participants`,
    metadata: {
      callId: event.call.agoraCallId,
      participants: event.call.participants.map(p => p.username),
      startTime: event.timestamp,
      isRecording: event.call.isRecording
    }
  });
  
  return { success: true };
}

Example - Call Notification:

export async function handler(event, context) {
  // Notify about call start
  const participantNames = event.call.participants
    .map(p => p.username)
    .join(', ');
  
  await mirra.telegram.sendMessage({
    chatId: 'your-chat-id',
    text: `📞 Call started\n\nParticipants: ${participantNames}\n${event.call.isRecording ? '🔴 Recording' : ''}`
  });
  
  return { success: true };
}

call.ended

The call.ended event fires when a voice call ends. This event includes complete call metadata and duration.

Event Type:

type: 'call.ended'
source: 'call'

Fields:

call.agoraCallId (string)

Agora RTC call identifier.

call.mirraCallId (string)

Mirra internal call identifier.

call.chatInstanceId (string)

Associated chat instance ID.

call.participants (CallParticipant[])

Array of all participants who were in the call.

call.durationSeconds (number)

Total call duration in seconds.

call.startedAt (Date)

When the call started.

call.endedAt (Date)

When the call ended.

call.wasRecorded (boolean)

True if the call was recorded.

call.recordingUrl (string | null)

URL to the call recording if available, null otherwise.

Example - Call Analytics:

export async function handler(event, context) {
  // Calculate call metrics
  const durationMinutes = Math.round(event.call.durationSeconds / 60);
  
  // Log to spreadsheet
  await mirra.google.sheets.appendRow({
    spreadsheetId: 'your-sheet-id',
    sheetName: 'Call Logs',
    values: [
      event.call.startedAt.toISOString(),
      event.call.participants.map(p => p.username).join(', '),
      event.call.participants.length,
      durationMinutes,
      event.call.wasRecorded ? 'Yes' : 'No',
      event.call.recordingUrl || 'N/A'
    ]
  });
  
  return { success: true };
}

Example - Call Summary:

export async function handler(event, context) {
  // Create summary memory
  const durationMinutes = Math.round(event.call.durationSeconds / 60);
  
  await mirra.memory.create({
    type: 'call_summary',
    content: `Call with ${event.call.participants.length} participants lasted ${durationMinutes} minutes`,
    metadata: {
      callId: event.call.mirraCallId,
      participants: event.call.participants.map(p => ({
        username: p.username,
        joinedAt: p.joinedAt,
        leftAt: p.leftAt
      })),
      duration: event.call.durationSeconds,
      recordingUrl: event.call.recordingUrl
    }
  });
  
  return { success: true };
}

transcript.chunk

The transcript.chunk event fires when a voice transcript chunk is processed in real-time. This event supports automatic enrichment including ticker detection and sentiment analysis.

Event Type:

type: 'transcript.chunk'
source: 'call'

Fields:

content.text (string)

Transcribed text for this chunk.

content.contentType (ContentType)

Always 'transcript' for transcript events.

call.agoraCallId (string)

Call identifier.

call.mirraCallId (string)

Mirra internal call identifier.

transcript.speakerUserId (string)

User ID of the speaker.

transcript.speakerName (string)

Display name of the speaker.

transcript.text (string)

Transcribed text (same as content.text).

transcript.startTimeMs (number)

Start time of this chunk in milliseconds from call start.

transcript.endTimeMs (number)

End time of this chunk in milliseconds from call start.

transcript.confidence (number)

Transcription confidence score from 0 to 1.

enrichment (EventEnrichment | null)

Enrichment data including detected tickers and sentiment. Null until processing completes.

Example - Ticker Tracker:

export async function handler(event, context) {
  // Wait for enrichment
  if (!event.enrichment?.tickers) {
    return { success: true, message: 'No tickers detected' };
  }
  
  // Track ticker mentions in calls
  for (const ticker of event.enrichment.tickers) {
    await mirra.memory.create({
      type: 'call_ticker_mention',
      content: `${ticker.symbol} mentioned by ${event.transcript.speakerName}: ${event.content.text}`,
      metadata: {
        ticker: ticker.symbol,
        confidence: ticker.confidence,
        speaker: event.transcript.speakerName,
        callId: event.call.mirraCallId,
        timestamp: event.transcript.startTimeMs
      }
    });
  }
  
  return { 
    success: true, 
    tickersProcessed: event.enrichment.tickers.length 
  };
}

Example - Sentiment Monitoring:

export async function handler(event, context) {
  if (!event.enrichment?.sentiment) {
    return { success: true };
  }
  
  const sentiment = event.enrichment.sentiment;
  
  // Alert on negative sentiment
  if (sentiment.label === 'negative' && sentiment.score < -0.5) {
    await mirra.telegram.sendMessage({
      chatId: 'your-alerts-channel',
      text: `⚠️ Negative sentiment in call\n\nSpeaker: ${event.transcript.speakerName}\nScore: ${sentiment.score}\n\n"${event.content.text}"`
    });
  }
  
  return { success: true };
}

Subscription Examples:

// All transcript chunks
{
  "eventType": "transcript.chunk"
}
 
// Only chunks with tickers
{
  "eventType": "transcript.chunk",
  "conditions": [
    {
      "field": "enrichment.tickers",
      "operator": "exists"
    }
  ]
}
 
// Specific speaker
{
  "eventType": "transcript.chunk",
  "conditions": [
    {
      "field": "transcript.speakerName",
      "operator": "equals",
      "value": "John Doe"
    }
  ]
}

call.summary_available

The call.summary_available event fires when an AI-generated call summary is ready.

Event Type:

type: 'call.summary_available'
source: 'call'

Fields:

content.text (string)

AI-generated summary text.

call.agoraCallId (string)

Call identifier.

call.mirraCallId (string)

Mirra internal call identifier.

call.summary (string)

AI-generated call summary (same as content.text).

Example - Summary Distribution:

export async function handler(event, context) {
  // Send summary to Telegram
  await mirra.telegram.sendMessage({
    chatId: 'your-team-channel',
    text: `📞 Call Summary\n\n${event.call.summary}`
  });
  
  // Save to memory
  await mirra.memory.create({
    type: 'call_summary',
    content: event.call.summary,
    metadata: {
      callId: event.call.mirraCallId,
      generatedAt: event.timestamp
    }
  });
  
  return { success: true };
}

Common Use Cases

Call Time Tracking

export async function handler(event, context) {
  // Only track ended calls
  if (event.type !== 'call.ended') {
    return { success: true, skipped: true };
  }
  
  // Calculate billable time
  const durationHours = event.call.durationSeconds / 3600;
  const roundedHours = Math.ceil(durationHours * 4) / 4; // Round to nearest 15 min
  
  // Log to time tracking
  await mirra.google.sheets.appendRow({
    spreadsheetId: 'time-tracking-sheet',
    sheetName: 'Calls',
    values: [
      event.call.startedAt.toISOString().split('T')[0],
      'Call',
      event.call.participants.filter(p => p.userId !== event.userId).map(p => p.username).join(', '),
      roundedHours,
      'Billable'
    ]
  });
  
  return { success: true };
}

Meeting Minutes Generator

export async function handler(event, context) {
  // Wait for summary
  if (event.type !== 'call.summary_available') {
    return { success: true, skipped: true };
  }
  
  // Create Google Doc with meeting minutes
  const doc = await mirra.google.docs.create({
    title: `Meeting Minutes - ${new Date().toLocaleDateString()}`,
    content: `# Meeting Minutes\n\nDate: ${new Date().toLocaleDateString()}\nDuration: ${Math.round(event.call.durationSeconds / 60)} minutes\n\n## Summary\n\n${event.call.summary}\n\n## Participants\n\n${event.call.participants.map(p => `- ${p.username}`).join('\n')}`
  });
  
  // Share link via Telegram
  await mirra.telegram.sendMessage({
    chatId: 'your-team-channel',
    text: `📝 Meeting minutes available: ${doc.url}`
  });
  
  return { success: true };
}

Action Items Extractor

export async function handler(event, context) {
  // Check transcript for action items
  const text = event.content.text.toLowerCase();
  
  // Look for action-oriented phrases
  const hasActionWords = ['todo', 'action item', 'follow up', 'will do', 'need to'].some(
    phrase => text.includes(phrase)
  );
  
  if (!hasActionWords) {
    return { success: true, skipped: true };
  }
  
  // Create assignment
  await mirra.assignments.create({
    title: `Action from call: ${event.content.text.substring(0, 50)}...`,
    description: `From ${event.transcript.speakerName} in call:\n\n"${event.content.text}"`,
    metadata: {
      callId: event.call.mirraCallId,
      speaker: event.transcript.speakerName,
      timestamp: event.transcript.startTimeMs
    }
  });
  
  return { success: true };
}

See Also

On this page