agentsclimarketplace

Assemblyai webhooks events

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/assemblyai-pack/skills/assemblyai-webhooks-events

'Implement AssemblyAI webhook handling for transcription completion events.From its SKILL.md

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill assemblyai-webhooks-events

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its file declares

Copied from the file, not written here

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

7.9 KB, ~1.8k tokens by cl100k_base, as published. Nobody here has run it

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

IssueCauseSolution
Webhook not receivedURL not accessible from internetVerify HTTPS URL, check firewall
401 on webhookWrong auth header valueMatch webhook_auth_header_value from submission
Duplicate processingWebhook retried after timeoutImplement idempotency (check transcript_id)
Webhook timeoutProcessing > 10 secondsReturn 200 immediately, process async
Missing transcript dataFetching too earlyFetch with client.transcripts.get() after webhook

Resources

Next Steps

For performance optimization, see assemblyai-performance-tuning.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,144. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.