Skillshub assemblyai-webhooks-events
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/assemblyai-webhooks-events" ~/.claude/skills/comeonoliver-skillshub-assemblyai-webhooks-events && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/assemblyai-webhooks-events/SKILL.mdsource content
AssemblyAI Webhooks & Events
Overview
Handle AssemblyAI webhooks for transcription completion. When you submit a transcript with
webhook_url, AssemblyAI sends a POST request to your URL when the transcript is completed or fails. One webhook per transcript — no complex event routing needed.
Prerequisites
- HTTPS endpoint accessible from the internet
package installedassemblyai- API key configured
How AssemblyAI Webhooks Work
- You submit a transcription with
parameterwebhook_url - AssemblyAI processes the audio asynchronously
- When done (completed or error), AssemblyAI sends a POST to your URL
- Your endpoint receives transcript ID and status, then fetches the full transcript
Key difference from other APIs: AssemblyAI webhooks are per-transcript (set at submission time), not a global webhook registration. There are no event types to subscribe to — you get one callback per transcript.
Instructions
Step 1: Submit Transcription with Webhook
import { AssemblyAI } from 'assemblyai'; const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY!, }); // submit() queues the job and returns immediately (doesn't poll) const transcript = await client.transcripts.submit({ audio: 'https://example.com/meeting-recording.mp3', webhook_url: 'https://your-app.com/webhooks/assemblyai', // Optional: auth header for webhook verification webhook_auth_header_name: 'X-Webhook-Secret', webhook_auth_header_value: process.env.ASSEMBLYAI_WEBHOOK_SECRET!, // Enable features — results will be available when webhook fires speaker_labels: true, sentiment_analysis: true, auto_highlights: true, }); console.log('Submitted:', transcript.id); // Returns immediately, webhook fires when processing completes
Step 2: Webhook Endpoint (Express.js)
import express from 'express'; import { AssemblyAI, type Transcript } from 'assemblyai'; const app = express(); const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY!, }); app.post('/webhooks/assemblyai', express.json(), async (req, res) => { // Step 1: Verify authenticity via custom auth header const secret = req.headers['x-webhook-secret']; if (secret !== process.env.ASSEMBLYAI_WEBHOOK_SECRET) { console.warn('Webhook auth failed'); return res.status(401).json({ error: 'Unauthorized' }); } // Step 2: Extract payload const { transcript_id, status } = req.body; console.log(`Webhook received: ${transcript_id} — ${status}`); // Step 3: Respond quickly (within 10 seconds) res.status(200).json({ received: true }); // Step 4: Process asynchronously try { if (status === 'completed') { const transcript = await client.transcripts.get(transcript_id); await processCompletedTranscript(transcript); } else if (status === 'error') { await handleFailedTranscript(transcript_id, req.body.error); } } catch (error) { console.error('Webhook processing error:', error); } }); async function processCompletedTranscript(transcript: Transcript) { console.log(`Processing transcript ${transcript.id}:`); console.log(` Text: ${transcript.text?.length} chars`); console.log(` Duration: ${transcript.audio_duration}s`); console.log(` Speakers: ${transcript.utterances?.length ?? 0} utterances`); // Store in database, notify user, trigger LeMUR analysis, etc. // Example: Run LeMUR summarization after transcription completes if (transcript.text && transcript.text.length > 100) { const { response } = await client.lemur.summary({ transcript_ids: [transcript.id], answer_format: 'bullet points', }); console.log('Auto-summary:', response); } } async function handleFailedTranscript(transcriptId: string, error?: string) { console.error(`Transcript ${transcriptId} failed: ${error}`); // Alert ops team, retry with different settings, etc. } app.listen(3000, () => console.log('Listening on :3000'));
Step 3: Webhook Endpoint (Next.js App Router)
// app/api/webhooks/assemblyai/route.ts import { AssemblyAI } from 'assemblyai'; import { NextRequest, NextResponse } from 'next/server'; const client = new AssemblyAI({ apiKey: process.env.ASSEMBLYAI_API_KEY!, }); export async function POST(req: NextRequest) { const secret = req.headers.get('x-webhook-secret'); if (secret !== process.env.ASSEMBLYAI_WEBHOOK_SECRET) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await req.json(); const { transcript_id, status } = body; if (status === 'completed') { const transcript = await client.transcripts.get(transcript_id); // Process transcript... console.log(`Completed: ${transcript_id}, ${transcript.text?.length} chars`); } return NextResponse.json({ received: true }); }
Step 4: Idempotent Processing
// Prevent duplicate processing if webhook is retried const processedTranscripts = new Set<string>(); // In production, use Redis or a database instead of in-memory Set async function idempotentProcess(transcriptId: string, handler: () => Promise<void>) { if (processedTranscripts.has(transcriptId)) { console.log(`Already processed: ${transcriptId}`); return; } await handler(); processedTranscripts.add(transcriptId); } // Usage in webhook handler: await idempotentProcess(transcript_id, async () => { const transcript = await client.transcripts.get(transcript_id); await processCompletedTranscript(transcript); });
Step 5: Testing Webhooks Locally
# Option 1: ngrok ngrok http 3000 # Use the HTTPS URL as your webhook_url # Option 2: Simulate webhook manually curl -X POST http://localhost:3000/webhooks/assemblyai \ -H "Content-Type: application/json" \ -H "X-Webhook-Secret: your-secret" \ -d '{ "transcript_id": "test-id-123", "status": "completed" }'
Webhook Payload Reference
AssemblyAI sends a POST with this JSON body:
{ "transcript_id": "6wij2z3g66-...", "status": "completed" }
For errors:
{ "transcript_id": "6wij2z3g66-...", "status": "error", "error": "Download error: unable to download audio from URL" }
If
redact_pii_audio was enabled, a second webhook fires when redacted audio is ready.
Output
- Webhook endpoint that receives transcription completion events
- Auth header verification for secure webhook handling
- Idempotent processing to handle retries
- LeMUR auto-analysis triggered on completion
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Webhook not received | URL not accessible from internet | Verify HTTPS URL, check firewall |
| 401 on webhook | Wrong auth header value | Match from submission |
| Duplicate processing | Webhook retried after timeout | Implement idempotency (check transcript_id) |
| Webhook timeout | Processing > 10 seconds | Return 200 immediately, process async |
| Missing transcript data | Fetching too early | Fetch with after webhook |
Resources
Next Steps
For performance optimization, see
assemblyai-performance-tuning.