Agent Skills: AssemblyAI Webhooks & Events

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/assemblyai-webhooks-events

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/assemblyai-pack/skills/assemblyai-webhooks-events

Skill Files

Browse the full folder contents for assemblyai-webhooks-events.

Download Skill

Loading file tree…

plugins/saas-packs/assemblyai-pack/skills/assemblyai-webhooks-events/SKILL.md

Skill Metadata

Name
assemblyai-webhooks-events
Description
|

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
  • assemblyai package installed
  • API key configured

How AssemblyAI Webhooks Work

  1. You submit a transcription with webhook_url parameter
  2. AssemblyAI processes the audio asynchronously
  3. When done (completed or error), AssemblyAI sends a POST to your URL
  4. 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 webhook_auth_header_value 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 client.transcripts.get() after webhook |

Resources

Next Steps

For performance optimization, see assemblyai-performance-tuning.