From fireflies-pack
Implements Fireflies.ai webhook receiver with HMAC-SHA256 verification to process transcription completed events and notifications.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin fireflies-packThis skill is limited to using the following tools:
Handle Fireflies.ai webhook events for real-time transcript notifications. Fireflies fires a webhook when a transcript finishes processing. The payload is signed with HMAC-SHA256 for verification.
Provides reference architecture for Fireflies.ai meeting intelligence: GraphQL API client, webhook transcript processing, action items, analytics, CRM sync.
Handles TwinMind webhooks for meeting events: transcription (Ear-3), action items, summaries (GPT-4/Claude/Gemini), and calendar sync. Verifies API integration.
Sets up Fathom webhooks via API or settings to receive meeting summaries, transcripts, action items for automated processing and workflows.
Share bugs, ideas, or general feedback.
Handle Fireflies.ai webhook events for real-time transcript notifications. Fireflies fires a webhook when a transcript finishes processing. The payload is signed with HMAC-SHA256 for verification.
FIREFLIES_API_KEY and FIREFLIES_WEBHOOK_SECRET in environmentFireflies currently fires one event type:
| Event | eventType Value | Trigger |
|---|---|---|
| Transcription completed | "Transcription completed" | Transcript is fully processed and ready |
{
"meetingId": "ASxwZxCstx",
"eventType": "Transcription completed",
"clientReferenceId": "be582c46-4ac9-4565-9ba6-6ab4264496a8"
}
| Field | Type | Description |
|---|---|---|
meetingId | String | Transcript ID -- use in transcript(id:) query |
eventType | String | Always "Transcription completed" currently |
clientReferenceId | ID | Your custom ID from uploadAudio (null if bot-recorded) |
import express from "express";
import crypto from "crypto";
const app = express();
// IMPORTANT: Use raw body for HMAC verification
app.post("/webhooks/fireflies",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.headers["x-hub-signature"] as string;
const rawBody = req.body.toString();
// Verify HMAC-SHA256 signature
if (!signature || !verifySignature(rawBody, signature)) {
console.warn("Rejected webhook: invalid signature");
return res.status(401).json({ error: "Invalid signature" });
}
// Acknowledge immediately -- process async
res.status(200).json({ received: true });
const event = JSON.parse(rawBody);
console.log(`Webhook: ${event.eventType} for meeting ${event.meetingId}`);
// Process in background
processTranscriptReady(event.meetingId, event.clientReferenceId)
.catch(err => console.error("Webhook processing failed:", err));
}
);
function verifySignature(payload: string, signature: string): boolean {
const secret = process.env.FIREFLIES_WEBHOOK_SECRET!;
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
const FIREFLIES_API = "https://api.fireflies.ai/graphql";
async function processTranscriptReady(meetingId: string, clientRefId?: string) {
const res = await fetch(FIREFLIES_API, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.FIREFLIES_API_KEY}`,
},
body: JSON.stringify({
query: `
query GetTranscript($id: String!) {
transcript(id: $id) {
id title date duration
organizer_email
speakers { name }
sentences { speaker_name text start_time end_time }
summary {
overview
action_items
keywords
short_summary
}
meeting_attendees { displayName email }
}
}
`,
variables: { id: meetingId },
}),
});
const json = await res.json();
if (json.errors) throw new Error(json.errors[0].message);
const transcript = json.data.transcript;
console.log(`Processing: "${transcript.title}" (${transcript.duration}min)`);
console.log(`Speakers: ${transcript.speakers.map((s: any) => s.name).join(", ")}`);
console.log(`Action items: ${transcript.summary?.action_items?.length || 0}`);
// Route to downstream systems
await Promise.all([
storeTranscript(transcript),
createTasksFromActionItems(transcript),
notifyTeam(transcript),
]);
}
async function storeTranscript(transcript: any) {
// Store in your database
console.log(`Stored transcript: ${transcript.id}`);
}
async function createTasksFromActionItems(transcript: any) {
const items = transcript.summary?.action_items || [];
for (const item of items) {
console.log(`Task created: ${item}`);
// await taskManager.create({ title: item, source: transcript.title });
}
}
async function notifyTeam(transcript: any) {
// Send Slack/email notification
const summary = transcript.summary?.short_summary || transcript.summary?.overview;
console.log(`Notification: "${transcript.title}" -- ${summary}`);
}
Instead of dashboard-level webhook, include a webhook URL in uploadAudio:
await fetch(FIREFLIES_API, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.FIREFLIES_API_KEY}`,
},
body: JSON.stringify({
query: `
mutation($input: AudioUploadInput) {
uploadAudio(input: $input) { success title message }
}
`,
variables: {
input: {
url: "https://storage.example.com/recording.mp3",
title: "Client Call 2026-03-22",
webhook: "https://api.yourapp.com/webhooks/fireflies",
client_reference_id: "order-12345",
},
},
}),
});
set -euo pipefail
# Test by uploading a short audio file
curl -s -X POST https://api.fireflies.ai/graphql \
-H "Authorization: Bearer $FIREFLIES_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation($input: AudioUploadInput) { uploadAudio(input: $input) { success message } }",
"variables": { "input": { "url": "https://example.com/test-audio.mp3", "title": "Webhook Test" } }
}' | jq .
# The webhook will fire when transcription completes (usually 2-5 minutes)
| Issue | Cause | Solution |
|---|---|---|
| Webhook not firing | URL not saved in dashboard | Re-register at app.fireflies.ai/settings |
| Invalid signature | Secret mismatch | Verify secret matches dashboard value |
Missing meetingId | Malformed payload | Log raw body, check Fireflies status |
| Webhook only fires for some meetings | Owner-only constraint | Webhooks fire only for your meetings |
clientReferenceId is null | Bot-recorded meeting | Only set on uploadAudio calls |
For deployment setup, see fireflies-deploy-integration.