cd /news/ai-tools/add-ai-meeting-summaries-to-any-app-… Β· home β€Ί topics β€Ί ai-tools β€Ί article
[ARTICLE Β· art-145733] src=dev.to β†— pub= topic=ai-tools verified=true sentiment=↑ positive

Add AI meeting summaries to any app with Zoom AI Services in 15 minutes

A developer demonstrated how to add AI meeting summaries and action items to any application using Zoom AI Services, Zoom's developer API platform, with roughly 60 lines of Python and two REST calls. The tutorial covers the Scribe transcription endpoint (Fast mode for files up to 5 minutes, Batch for up to 6 hours, Live over WebSocket), which supports 9 locales, speaker diarization, and word-level timestamps via query parameters, then feeds the speaker-labeled transcript to a Summarizer. The author stresses that API keys are server-to-server only and must never ship in client-side code.

by read7 min views1 publishedOct 5, 2026

If you have meeting recordings (standups, sales calls, support calls) and you want summaries + action items out of them without running your own speech models, two REST calls are all it takes:

Both are part of Zoom AI Services, Zoom's developer API platform. The developer portal lives at zoom.ai (verified October 2026 β€” that's Zoom's own portal, not a third party). No SDK needed β€” it's plain REST.

Total code: about 60 lines of Python.

requests (pip install requests). That's it for credentials. One important rule: API keys are server-to-server only. Never put the key in client-side code or ship it in a mobile/web app bundle. All calls in this tutorial run from a backend.

Not ready to create a key? The portal has an interactive playground with sample data β€” no setup required. I'd suggest poking it for two minutes so you know what the responses look like before wiring up the script.

Scribe has three modes: Live (real-time transcription over a secure WebSocket at wss://api.zoom.us/v2/aiservices/scribe/live), Fast (synchronous β€” POST your audio bytes, the response returns when transcription completes), and Batch (async jobs API for long files, with webhook callbacks). For this tutorial we use Fast mode, the simplest. Fast mode handles files up to 5 minutes; longer recordings go through Batch (up to 6 hours per file).

Input is the raw audio bytes: WAV, Opus, and Β΅-law encodings (audio/wav, etc. via Content-Type). 9 locales: en-US, de-DE, es, fr-FR, it-IT, ja-JP, ko-KR, pt-BR, zh-CN.

Endpoint: POST https://api.zoom.us/v2/aiservices/scribe/transcribe

Auth: one header β€” x-api-key: <your API key>. That's the whole auth story per the portal docs.

Options go as query parameters (model, language, diarization, text_polishing, profanity_filter, word_time_offsets, channel_separation, custom_terms):

curl -X POST "https://api.zoom.us/v2/aiservices/scribe/transcribe?model=zoom-scribe-en&language=en-US&diarization=true&text_polishing=true" \
  -H "x-api-key: $ZOOM_API_KEY" \
  -H "Content-Type: audio/wav" \
  --data-binary @meeting.wav

Setting diarization=true adds a speaker label (Speaker 1, Speaker 2, …) to each segment β€” worth it for meetings. Set Content-Type to match your file.

Response (200) β€” synchronous; it returns after transcription completes:

{
  "request_id": "req_123",
  "duration_sec": 181.04,
  "result": {
    "text_display": "So, let's have a good conversation, my friend. Do you think...",
    "segments": [
      {
        "id": "seg_1",
        "start": 0.0,
        "end": 5.2,
        "channel": 1,
        "speaker": "Speaker 1",
        "text": "So, let's have a good conversation, my friend.",
        "words": []
      }
    ]
  },
  "model": "zoom-scribe-en",
  "usage": { "input_units": 181.04, "unit_type": "seconds" }
}

With word_time_offsets=true the words arrays carry per-word timestamps; channel_separation=true splits stereo channels.

In Python:

import os, requests

API_BASE = "https://api.zoom.us/v2/aiservices"
API_KEY = os.environ["ZOOM_API_KEY"]

def transcribe(audio_path: str, language: str = "en-US") -> dict:
    with open(audio_path, "rb") as f:
        audio_bytes = f.read()
    resp = requests.post(
        f"{API_BASE}/scribe/transcribe",
        headers={"x-api-key": API_KEY, "Content-Type": "audio/wav"},
        params={"model": "zoom-scribe-en", "language": language,
                "diarization": "true", "text_polishing": "true"},
        data=audio_bytes,
        timeout=300,
    )
    resp.raise_for_status()
    return resp.json()["result"]

Tip: feed the Summarizer a speaker-labeled transcript β€” it's much better at action-item ownership. Build it from the segments:

def segments_to_transcript(result: dict) -> str:
    lines = []
    for seg in result.get("segments", []):
        speaker = seg.get("speaker") or "Speaker"
        lines.append(f"{speaker}: {seg.get('text', '').strip()}")
    text = "\n".join(lines)
    return text or result.get("text_display", "")

The Summarizer API takes transcript text (from any system β€” it doesn't have to come from Scribe) and returns rendered, display-ready text in result.text. Fast mode accepts one inline transcript (up to 96 KB) and returns synchronously.

Endpoint: POST https://api.zoom.us/v2/aiservices/summarizer/summarize

The request is flat JSON β€” no config wrapper. transcript is a JSON-encoded string of speaker/text turns:

curl -X POST https://api.zoom.us/v2/aiservices/summarizer/summarize \
  -H "x-api-key: $ZOOM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "summary_type": "CONVERSATION",
    "task": "full_summary",
    "transcript": "[{\"speaker\": \"Speaker 1\", \"text\": \"Let us finalize the timeline.\"}, {\"speaker\": \"Speaker 2\", \"text\": \"We need one more week for validation.\"}]",
    "language": "en-US"
  }'

Four tasks to choose from, set in task:

task What result.text contains
recap A concise Recap: paragraph
summary A detailed Summary: section
action_items An Action Items: section grouped by owner
full_summary All three β€” recap + summary + action items

summary_type is "CONVERSATION" (default; also "GENERIC", "MEETING_NOTES"), language is a BCP-47 output locale (14 supported, en-US default).

Response (200) β€” the exact shape, from the official docs:

{
  "request_id": "req_123",
  "task": "full_summary",
  "result": {
    "text": "Recap:\n\nThe team agreed to delay the launch by one week.\n\nSummary:\n\n..."
  }
}

For full_summary, result.text is rendered text, ready to display β€” recap, summary, and action items grouped by owner.

import json

def summarize(transcript: str, task: str = "full_summary") -> str:
    turns = []
    for line in transcript.splitlines():
        if ":" in line:
            speaker, text = line.split(":", 1)
            turns.append({"speaker": speaker.strip(), "text": text.strip()})
    resp = requests.post(
        f"{API_BASE}/summarizer/summarize",
        headers={"x-api-key": API_KEY, "Content-Type": "application/json"},
        json={"summary_type": "CONVERSATION", "task": task,
              "transcript": json.dumps(turns), "language": "en-US"},
        timeout=120,
    )
    resp.raise_for_status()
    data = resp.json()
    result = data.get("result") or {}
    return result.get("text") or data.get("text") or data.get("summary") or ""

One script: audio file in, meeting summary out. Save as summarize_meeting.py:

#!/usr/bin/env python3
"""Audio file -> transcript -> meeting summary, via Zoom AI Services (Scribe + Summarizer).

Usage:
    ZOOM_API_KEY=<your key> python summarize_meeting.py meeting.wav

The key is server-side only -- never expose it to clients.
"""
import json, os, sys, requests

API_BASE = "https://api.zoom.us/v2/aiservices"
API_KEY = os.environ["ZOOM_API_KEY"]

def transcribe(audio_path: str, language: str = "en-US") -> dict:
    with open(audio_path, "rb") as f:
        audio_bytes = f.read()
    resp = requests.post(
        f"{API_BASE}/scribe/transcribe",
        headers={"x-api-key": API_KEY, "Content-Type": "audio/wav"},
        params={"model": "zoom-scribe-en", "language": language,
                "diarization": "true", "text_polishing": "true"},
        data=audio_bytes,
        timeout=300,
    )
    resp.raise_for_status()
    return resp.json()["result"]

def segments_to_transcript(result: dict) -> str:
    lines = [
        f"{seg.get('speaker') or 'Speaker'}: {seg.get('text', '').strip()}"
        for seg in result.get("segments", [])
    ]
    return "\n".join(lines) or result.get("text_display", "")

def summarize(transcript: str, task: str = "full_summary") -> str:
    turns = [{"speaker": s.strip(), "text": t.strip()}
             for s, _, t in (line.partition(":") for line in transcript.splitlines()) if t.strip()]
    resp = requests.post(
        f"{API_BASE}/summarizer/summarize",
        headers={"x-api-key": API_KEY, "Content-Type": "application/json"},
        json={"summary_type": "CONVERSATION", "task": task,
              "transcript": json.dumps(turns), "language": "en-US"},
        timeout=120,
    )
    resp.raise_for_status()
    data = resp.json()
    result = data.get("result") or {}
    return result.get("text") or data.get("text") or data.get("summary") or ""

def main() -> None:
    audio_path = sys.argv[1]
    print("Transcribing...", flush=True)
    result = transcribe(audio_path)
    transcript = segments_to_transcript(result)
    print(f"Transcript ready ({len(transcript)} chars). Summarizing...\n", flush=True)
    print(summarize(transcript))

if __name__ == "__main__":
    main()

Run it:

ZOOM_API_KEY=your_key_here python summarize_meeting.py meeting.wav

That's the whole pipeline: audio bytes β†’ diarized transcript β†’ recap + summary + action items. (Fast mode caps at 5 minutes of audio; longer files go through the Batch jobs API.)

On pricing: Zoom AI Services are usage-based via prepaid credits. I don't have verified per-minute rates, so check the current numbers at zoom.us/pricing/developer before you estimate costs.

401 Unauthorized. Most common causes: the key is wrong or expired (remember, it's shown only once at creation β€” regenerate it in the portal if you lost it), or the wrong header name. Per the portal docs, auth is the x-api-key header only.

Unsupported format / fetch errors. Scribe Fast mode takes the raw audio bytes (--data-binary), not a URL β€” set Content-Type to match (audio/wav, audio/mpeg, …). Keep Fast-mode files under 5 minutes; longer audio goes through Batch.

429 rate limits. Back off and retry with exponential backoff. Exact rate limits and quotas depend on your plan β€” check the portal rather than guessing.

Diarization quality. Heavy background noise, overlapping speech, and very short clips all degrade speaker separation. If labels matter to you (they should β€” the Summarizer groups action items by owner), a quick pass with a noise-reduction tool before upload pays off.

Transcript too long for Summarizer Fast mode. Inline input caps at 96 KB. For longer meetings, split the transcript into chunks and summarize each, or move to Batch mode.

GET /aiservices/summarizer/jobs/{jobId} or .../scribe/jobs/{jobId}, or get webhook callbacks. Webhooks are signed β€” verify with the x-zm-signature (HMAC-SHA256, sha256= prefix) and x-zm-request-timestamp headers against your secret.wss://api.zoom.us/v2/aiservices/scribe/live β€” one completed segment per detected speech turn. Disclosure: this article was drafted with AI assistance; all API calls and code were checked against the official Zoom AI Services portal docs (October 2026), and the Scribe flow was run live in the portal playground.

── more in #ai-tools 4 stories Β· sorted by recency
── more on @zoom 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/add-ai-meeting-summa…] indexed:0 read:7min 2026-10-05 Β· β€”