会議botは「作らない」のが正解になった
Zoom・Google Meet・Microsoft Teams の会議に自動参加して録画と文字起こしを取る—— この機能を自前で作ろうとすると、想像の何倍も削られます。
Zoom には Meeting SDK があり、Google Meet には(つい最近まで)公式の参加手段がなく、 Teams はテナント設定で挙動が変わる。結局ヘッドレスブラウザで会議UIを操作する実装に行き着き、プラットフォーム側のUI変更で毎月壊れるという保守地獄が始まります。 さらに音声・映像を実際に受け取るには、常時起動のメディアサーバとストレージが必要です。
Recall.ai はこの層をまるごと引き受けるAPIです。 「この会議URLにこの名前で入って録れ」というPOSTを1本投げるだけで、 botが参加し、録画・文字起こし・参加者イベントが取得できる状態になります。
本記事は「これから実際にRecallで開発する人」向けの実装ガイドです。 概念紹介ではなく、最初の1週間で必ず引っかかるところを先に潰す構成にしています。
まず決めること — 3つの録音方式
Recallには録音の入り口が3つあります。ここを間違えると設計をやり直すことになるので、最初に決めます。
| 方式 | 仕組み | 向いているケース | 注意点 |
|---|---|---|---|
| Meeting Bot API | Recallが管理するbotが会議に参加者として入る | 汎用的な議事録・商談解析・AI面接。プラットフォーム横断で最も広くカバーできる | 参加者一覧にbotが見える。会議側の設定で入室拒否されうる |
| Desktop Recording SDK | ユーザーのPC上でローカル録音(Electronアプリに組み込む) | botを見せたくない・入室許可が取れない環境。ネイティブアプリを持っているプロダクト | Windows と Apple Silicon Mac のみ。ユーザーがアプリを入れる必要がある |
| Meeting Direct Connect | Zoom RTMS / Google Meet Media API といった公式配信APIに直結 | botを一切出さずプラットフォーム公式経路で取りたい | 対応プラットフォームが限定的。会議主催側のアプリ承認が前提 |
迷ったら Meeting Bot API です。カバー範囲が最も広く、 サーバサイドだけで完結するため実装量も最小です。 以降は特記しない限り Meeting Bot API の話をします。
対応プラットフォーム
Zoom / Google Meet / Microsoft Teams / Webex が本命で、 Go-To Meeting と Slack Huddles も扱えます(前者はBeta)。 ただし機能の対応状況はプラットフォームごとに異なります。 特に効いてくるのが次の2点です。
- 参加者別の音声ストリーム(=完璧な話者分離)は Zoom / Teams / Google Meet で利用可。Webexは非対応
- Output Media(botから映像・音声を出す)は Zoom / Meet / Teams / Webex で利用可。Slack Huddlesは非対応
リージョン選択は後戻りできない
地味ですが最重要の初期決定です。Recallは4リージョンで完全に独立したデプロイとして動いています。
| リージョン | ベースURL |
|---|---|
| US West | https://us-west-2.recall.ai |
| US East | https://us-east-1.recall.ai |
| EU | https://eu-central-1.recall.ai |
| Asia(東京) | https://ap-northeast-1.recall.ai |
「独立」の意味は文字通りです。ログイン情報もAPIキーもリージョン間で共有されません。 US Westで作ったbotをTokyoのエンドポイントから参照することはできません。 Pay-as-you-goプランでリージョンを移りたくなったら、移行先で別アカウントを作り直す必要があります。
最小構成で動かす
認証はシンプルなトークンヘッダです。Token プレフィックスは付けても付けなくても通ります。
# 環境変数(以降のサンプルで使う)
export RECALL_REGION="ap-northeast-1"
export RECALLAI_API_KEY="your_api_key_here"
# 認証ヘッダはどちらでも通る
# Authorization: Token <key>
# Authorization: <key>botを作る
最小の Create Bot リクエストがこれです。recording_config に何を書くかで、何を録るかが決まります。何も指定しなければ録れません。
curl -X POST "https://${RECALL_REGION}.recall.ai/api/v1/bot" \
-H "Authorization: Token ${RECALLAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"meeting_url": "https://meet.google.com/abc-defg-hij",
"bot_name": "議事録アシスタント",
"recording_config": {
"transcript": {
"provider": { "recallai_streaming": {} }
}
}
}'レスポンスに id(Bot ID)が返ります。以降のすべての操作はこのIDが起点です。
結果を取る
// GET /api/v1/bot/:bot_id のレスポンスから成果物のダウンロードURLを引く
const res = await fetch(
`https://${region}.recall.ai/api/v1/bot/${botId}`,
{ headers: { Authorization: `Token ${apiKey}` } },
);
const bot = await res.json();
const rec = bot.recordings[0];
const videoUrl = rec.media_shortcuts.video_mixed?.data?.download_url;
const transcriptUrl = rec.media_shortcuts.transcript?.data?.download_url;
// download_url は署名付きS3リンク。認証ヘッダ不要でそのまま取れる
const transcript = await (await fetch(transcriptUrl)).json();データモデル — v1.11の3層構造を理解する
ここを掴むとドキュメントが一気に読めるようになります。v1.11ではbot → recording → media artifacts の3層です。
graph LR B["Bot<br/>会議に参加する実体<br/>1会議 = 1bot"] --> R["Recording<br/>録画セッション<br/>停止・再開で複数になりうる"] R --> T["transcript"] R --> VM["video_mixed"] R --> AM["audio_mixed"] R --> VS["video_separate"] R --> PE["participant_events"] R --> MM["meeting_metadata"] T --> D["各artifactが<br/>data.download_url を持つ<br/>署名付きS3・既定7日"] VM --> D PE --> D
重要なのは recordingがbotと1:1ではないことです。 Pause / Resume Recording を使えば1つのbotが複数のrecordingを持ち得ます。 だから recordings[0] と決め打ちするコードは、 録画制御を入れた瞬間に壊れます。
media_shortcuts は「便利ショートカット」
artifactは /api/v1/transcript/:id のように個別エンドポイントからも取れますが、 Retrieve Bot のレスポンスに含まれる media_shortcuts を使えば1リクエストで全成果物のURLに到達できます。実装上はこちらが基本です。
文字起こしのJSONスキーマ
download_url から落ちてくるのは発話(utterance)の配列です。1要素が1人の連続した発話に対応します。
// transcript の download_url が返すJSON
[
{
"participant": {
"id": 12345, // 会議内で一意の数値ID
"name": "山田 太郎",
"is_host": true,
"platform": "google_meet",
"extra_data": null,
"email": "yamada@example.com" // 取得できる条件は限定的
},
"language_code": "ja",
"words": [
{
"text": "それでは",
"start_timestamp": { "absolute": "2026-08-10T02:00:03.120Z", "relative": 3.12 },
"end_timestamp": { "absolute": "2026-08-10T02:00:03.640Z", "relative": 3.64 }
}
]
}
]webhook設計 — bot.done を待つのは古い
Recallのwebhookは大きく2系統あります。混同しやすいので分けて覚えます。
| 系統 | 設定場所 | 代表イベント | 用途 |
|---|---|---|---|
| ステータス変更webhook | ダッシュボードで購読 | bot.in_call_recording / bot.done / bot.fatal / transcript.done | ライフサイクル管理・成果物の取得タイミング検知 |
| リアルタイムendpoint | Create Bot の recording_config.realtime_endpoints | transcript.data / participant_events.join | 会議中のライブ処理 |
botのライフサイクルイベント
ステータス変更webhookで流れてくる主なイベントは次の通りです。
bot.joining_call— 接続中bot.in_waiting_room— 待機室(ロビー)で待っているbot.in_call_not_recording— 入室したが録画していないbot.recording_permission_allowed/bot.recording_permission_denied— ホストの録画許可の可否bot.in_call_recording— 録画中bot.call_ended— 退出bot.done— 停止完了、メディア取得可能bot.fatal— 致命的エラーで終了
ペイロードは data.data.code にステータス、data.data.sub_code に理由が入る二段構造です。data.bot.metadata には Create Bot 時に自分で入れた任意のキーバリューが返るので、自社DBのIDを metadata に入れておけば突合が一発で済みます。
// ステータス変更webhookのペイロード構造
{
"event": "bot.fatal",
"data": {
"data": {
"code": "fatal",
"sub_code": "meeting_requires_sign_in", // ここが原因の本体
"updated_at": "2026-08-10T02:00:00.000Z"
},
"bot": {
"id": "b1f8...",
"metadata": { "interview_id": "iv_0421" } // 自社IDを入れておく
}
}
}v1.11では artifact 単位で購読する
v1.11では recording と各 media artifact にもステータスイベントが付きました。recording.processing / done / failed / deleted と、transcript.* video_mixed.* participant_events.* などが同じ4状態を持ちます。
これが効くのは成果物の完成タイミングがずれるからです。 文字起こしは先に終わるのに、映像のエンコードは後まで掛かる。bot.done を一括で待つ設計だと、 文字起こしだけ使いたいユースケースでも映像処理を待たされます。必要なartifactの *.done を個別に購読するのが v1.11 の作法です。
署名検証は自前で書ける
RecallのwebhookはSvix互換のHMAC-SHA256署名です。webhook-id / webhook-timestamp / webhook-signature の3ヘッダが付き、 ダッシュボードの Developers > API Keys & Secrets で発行するwhsec_ 始まりのワークスペースシークレットで検証します。
import crypto from 'node:crypto';
// Recallからのwebhook / websocket / callback を検証する
export function verifyRecallRequest(args: {
secret: string; // whsec_... 形式
headers: Record<string, string>;
payload: string | null; // POSTなら生ボディ。GET/WS upgradeなら null
}): void {
const { secret, headers, payload } = args;
const msgId = headers['webhook-id'];
const msgTimestamp = headers['webhook-timestamp'];
const msgSignature = headers['webhook-signature'];
// whsec_ を剥がしてbase64デコードしたものが鍵
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const toSign = `${msgId}.${msgTimestamp}.${payload ?? ''}`;
const expected = crypto.createHmac('sha256', key).update(toSign).digest('base64');
// "v1,xxxx v1,yyyy" のようにスペース区切りで複数来ることがある(鍵ローテーション中)
for (const versioned of msgSignature.split(' ')) {
const [version, signature] = versioned.split(',');
if (version !== 'v1') continue;
const a = Buffer.from(expected, 'base64');
const b = Buffer.from(signature, 'base64');
if (a.length === b.length && crypto.timingSafeEqual(a, b)) return;
}
throw new Error('No matching signature found');
}ハマりどころは2つです。ひとつは 署名対象が生ボディであること—— Express の express.json() や Next.js の自動パースが走った後の オブジェクトを再シリアライズすると、キー順や空白の差で検証が落ちます。 もうひとつは WebSocketの検証はupgradeリクエスト時に payload: null で行うことです。
署名検証を実装する余裕がない初期段階なら、https://my-app.com/webhook?token=secret のようにクエリパラメータのトークンでも一応認証できます(推奨はされていません)。
文字起こし戦略を選ぶ
ここが最も選択肢が多く、コストと品質に直結します。 まずリアルタイムか事後かを決めます。
リアルタイム / 事後
| リアルタイム(streaming) | 事後(async) | |
|---|---|---|
| 設定タイミング | Create Bot の recording_config.transcript | 録画完了後に Create Async Transcript を叩く |
| 取得方法 | realtime_endpoints 経由で transcript.data を受信 | transcript.done を待って download_url を取得 |
| 向くケース | 会議中にAIが反応する・ライブ字幕 | 議事録生成・要約・検索インデックス |
| 精度 | モード次第(後述) | 一般に高い。会議全体の文脈が使える |
| やり直し | 不可 | 可能。同じrecordingに別プロバイダで再実行できる |
実務では両方使うことになります。会議中は低遅延モードで反応し、 終了後に高精度で作り直す。asyncは既存recordingに対して後から何度でも叩けるので、 「プロバイダを乗り換えたら過去分も再生成する」といった運用が可能です。
# 録画完了後に、既存のrecordingに対して文字起こしを作る
curl -X POST \
"https://${RECALL_REGION}.recall.ai/api/v1/recording/${RECORDING_ID}/create_transcript/" \
-H "Authorization: Token ${RECALLAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"provider": {
"recallai_async": { "language_code": "auto" }
},
"diarization": {
"use_separate_streams_when_available": true
}
}'プロバイダ選択
Recall自前の recallai_streaming / recallai_async と、 外部プロバイダ(Deepgram, AssemblyAI, ElevenLabs, AWS Transcribe, Speechmatics, Rev)が選べます。 外部プロバイダは自分でAPIキーをRecallに登録する形です。
Recall自前の recallai_streaming には2つのモードがあり、これが重要です。
| モード | 遅延 | 言語 | オプション |
|---|---|---|---|
prioritize_low_latency | 1〜3秒 | 英語のみ | 最小限 |
prioritize_accuracy | 3〜10分 | 36言語(日本語含む) | key_terms・スペル修正・profanityフィルタ等フル |
// prioritize_accuracy モードのフル設定例
{
"recording_config": {
"transcript": {
"provider": {
"recallai_streaming": {
"mode": "prioritize_accuracy",
"language_code": "auto", // 自動言語判定
"key_terms": ["PeopleX", "オンボーディング", "ATS"], // 固有名詞を効かせる
"filter_profanity": true
}
},
"diarization": {
"use_separate_streams_when_available": true
}
}
}
}話者分離は「ストリーム分離」で解く
誰が喋ったかの精度は、実装で最も苦情が来る部分です。 Recallの答えは diarization.use_separate_streams_when_available: true です。
これを立てると、混ぜた音声を後から声質で切り分けるのではなく、参加者ごとの別ストリームをそれぞれ文字起こしします。 結果として話者ラベルが「Speaker A / B / C」ではなく実際の参加者名になり、同時発話にも耐えます。 Zoom / Teams / Google Meet で利用可能(Webexは非対応)。
逆に精度が落ちるのは次のケースです。ここは仕様として受け入れるしかありません。
- 1台の端末を複数人で共有している(会議室で1マイクを囲むパターン)。プラットフォーム側が区別できない
- ストリーム分離が使えず、プラットフォームのアクティブスピーカーイベントに依存する場合。このイベント自体が不正確・欠落しうる
- 声質の似た話者を機械分離に頼る場合
リアルタイム連携 — webhook と WebSocket
会議中にデータを受け取る口は recording_config.realtime_endpoints 配列で宣言します。type に webhook か websocket を指定します。
webhook: テキスト系イベント向け
{
"meeting_url": "https://meet.google.com/abc-defg-hij",
"bot_name": "AI Interviewer",
"recording_config": {
"transcript": { "provider": { "recallai_streaming": {} } },
"participant_events": {},
"realtime_endpoints": [
{
"type": "webhook",
"url": "https://my-app.example.com/api/recall/realtime",
"events": [
"transcript.data", // 確定した発話
"transcript.partial_data", // 途中経過(確定前)
"participant_events.join",
"participant_events.leave",
"participant_events.speech_on",
"participant_events.speech_off",
"participant_events.chat_message"
]
}
]
}
}受信するペイロードは、ステータスwebhookと同じdata.data ネスト構造です。
// transcript.data の受信ペイロード
{
"event": "transcript.data",
"data": {
"data": {
"words": [
{ "text": "承知しました",
"start_timestamp": { "relative": 12.44 },
"end_timestamp": { "relative": 13.10 } }
],
"language_code": "ja",
"participant": { "id": 12345, "name": "山田 太郎", "is_host": true,
"platform": "google_meet", "extra_data": null,
"email": "yamada@example.com" }
},
"realtime_endpoint": { "id": "...", "metadata": {} },
"transcript": { "id": "...", "metadata": {} },
"recording": { "id": "...", "metadata": {} },
"bot": { "id": "...", "metadata": {} }
}
}リトライ仕様は把握しておく価値があります。 ネットワークエラーか非2xxだと1秒間隔・最大60回まで再送され、 60回失敗するとそのエンドポイントは failed になります。 手動リトライの仕組みはありません。固定1秒間隔なので、落ちている間のイベントが一気に積まれます—— 冪等性(webhook-id による重複排除)は必須です。
WebSocket: 生音声・生映像向け
音声や映像フレームをそのまま受けたいなら WebSocket です。 自前のSTT・感情分析・リアルタイム音声AIに繋ぐ場合はこちらになります。
{
"recording_config": {
"audio_mixed_raw": {},
"realtime_endpoints": [
{
"type": "websocket",
"url": "wss://my-app.example.com/api/recall/ws",
"events": ["audio_mixed_raw.data", "transcript.data"]
}
]
}
}受け取る音声フォーマットは固定です。ここは覚えておくと実装が速いです。
| 項目 | 値 |
|---|---|
| サンプルレート | 16 kHz |
| ビット深度 | 16 bit |
| チャンネル | モノラル(1ch) |
| エンコーディング | 符号付きリトルエンディアンPCM(S16LE) |
| 1メッセージあたり | 200ms チャンク |
| 転送形式 | base64エンコードされた buffer フィールド |
// WebSocketで生音声を受けてPCMバッファに戻す
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws, req) => {
// upgradeリクエストの時点で署名検証する(payload は null)
verifyRecallRequest({
secret: process.env.RECALL_WEBHOOK_SECRET!,
headers: req.headers as Record<string, string>,
payload: null,
});
ws.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.event === 'audio_mixed_raw.data') {
// 16kHz mono S16LE の200msチャンク = 3200サンプル = 6400バイト
const pcm = Buffer.from(msg.data.data.buffer, 'base64');
pushToSttPipeline(pcm);
}
if (msg.event === 'transcript.data') {
const text = msg.data.data.words.map((w: { text: string }) => w.text).join('');
const speaker = msg.data.data.participant.name;
onUtterance(speaker, text);
}
});
});参加者ごとの音声が欲しい場合は audio_separate_raw.data、 映像フレームなら video_separate_png.data やvideo_separate_h264.data を購読します。 後者のペイロードには type: "webcam" | "screenshare" が入るので、カメラ映像と画面共有を区別して処理できます。
botを「喋る参加者」にする
ここがRecallの面白いところです。botは受け取るだけでなく、会議に映像と音声を出せます。 AI面接官、リアルタイム通訳、会議中に発言するアシスタント—— この手のプロダクトはこの機能の上に立ちます。
sequenceDiagram participant M as 会議プラットフォーム participant BOT as Recall bot participant WS as 自前サーバ participant AI as LLM + TTS M->>BOT: 参加者の音声 BOT->>WS: transcript.data 確定発話 BOT->>WS: audio_mixed_raw.data 200msチャンク WS->>AI: 文脈と直近の発話を渡す AI-->>WS: 応答テキスト WS->>WS: TTSでMP3化しbase64に変換 WS->>BOT: POST /bot/:id/output_audio BOT->>M: botのマイクから再生 Note over BOT,M: 映像はoutput_mediaのwebpageが担当
映像: webpageを流し込む
botのカメラ映像として自分が用意したWebページをそのまま流せます。 アバター、字幕、リアルタイムのスライド——ブラウザで描けるものは何でも出せます。
{
"meeting_url": "https://us02web.zoom.us/j/1234567890",
"bot_name": "AI Interviewer",
"variant": {
"zoom": "web_4_core",
"google_meet": "web_4_core",
"microsoft_teams": "web_4_core"
},
"output_media": {
"camera": {
"kind": "webpage",
"config": { "url": "https://my-app.example.com/agent-view?session=abc" }
}
}
}variant の web_4_core は高スペックbotの指定です。 2250 millicores / 5250MB が割り当てられ、インタラクティブなエージェント用途の推奨構成です。 ただし単価が上がります(Pay-as-you-goで $0.60/時間)。
この output_media のwebpageはただの表示装置ではありません。 ページ内で MediaStream API を使えば会議音声にアクセスでき、 リアルタイム文字起こしのWebSocketにも繋げます。 つまりエージェントのロジックをbot内のブラウザ側に置く構成も取れるわけです。
音声: MP3をbase64で投げる
botに喋らせるAPIは驚くほど素朴です。MP3をbase64にしてPOSTするだけです。
// botに発話させる
async function speak(botId: string, mp3: Buffer) {
const res = await fetch(
`https://${region}.recall.ai/api/v1/bot/${botId}/output_audio/`,
{
method: 'POST',
headers: {
Authorization: `Token ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
kind: 'mp3',
b64_data: mp3.toString('base64'),
}),
},
);
if (!res.ok) throw new Error(`output_audio failed: ${res.status}`);
}なお、Zoom / Google Meet / Teams ではチャットメッセージの送信もできます (POST /bot/:id/send_chat_message/)。 音声で割り込みたくない情報(要約リンク、参考資料)はチャットに流すのが自然です。
運用で必ず踏む落とし穴
ここまでが機能の話。以下は本番で事故になるポイントです。
1. join_at を10分以上先に設定する
これが最重要です。Recallのbotには2種類あります。
| スケジュールbot | アドホックbot | |
|---|---|---|
| 条件 | join_at が10分以上先 | join_at が10分以内 or 未指定 |
| リソース | 専用マシンを事前確保 | 起動済みのウォームプールから確保 |
| 同時実行数 | 制限なし | 30まで |
| 遅刻 | 「絶対に遅刻しない」と明言 | プール枯渇時は失敗しうる |
| 失敗時 | — | 507 が返る。30秒後にリトライ |
つまり join_at を省略してその場でbotを立てる実装は、同時30会議の壁と 507 Insufficient Storage を常時抱えることになります。 カレンダー連携やイベント登録の時点で会議URLと開始時刻が判明した瞬間にスケジュールbotを作る——これが正解です。
更新・削除のルールも join_at の10分境界に紐づきます。
join_atの10分以上前: Update Scheduled Bot で任意フィールドを変更可、Delete Scheduled Bot で削除可join_atの10分以内:join_at自体は変更不可。作り直しが必要。削除は Remove Bot From Call を使う- 入室処理が始まった後: 更新不可(
update_bot_failed)
2. automatic_leave のデフォルトを知る
botがいつ勝手に退出するかは、指定しなければ以下のデフォルトで動きます。秒単位です。
{
"automatic_leave": {
"silence_detection": {
"timeout": 3600, // 無音が1時間続いたら退出
"activate_after": 1200 // 開始後20分経ってから判定を有効化
},
"bot_detection": {
"using_participant_events": { "timeout": 600, "activate_after": 1200 },
"using_participant_names": { "timeout": 3600, "activate_after": 1200 }
},
"everyone_left_timeout": {
"timeout": 2, // 全員退出から2秒で退出
"activate_after": 0
},
"waiting_room_timeout": 1200, // 待機室で20分待って諦める
"noone_joined_timeout": 1200, // 誰も来なければ20分で退出
"in_call_not_recording_timeout": 3600, // 録画できない状態が1時間続いたら退出
"recording_permission_denied_timeout": 30 // 録画拒否から30秒で退出
}
}効いてくるのは課金への影響です。 15分の面談を想定していても、参加者が来なければbotはnoone_joined_timeout の20分居座ります。 短い会議を大量に回すプロダクトなら、waiting_room_timeout と noone_joined_timeout は 実態に合わせて必ず短くしてください。
bot_detection は「他社のnotetaker botだけが残った会議から抜ける」ための設定です。 bot同士が延々と無音の会議に居続けて課金だけ発生する、という事故を防ぎます。
3. sub_code を潰し込む
bot.fatal が来たとき、原因は sub_code にあります。自分たちの責任範囲かどうかで分類しておくと運用が楽になります。
| 分類 | 代表的な sub_code | 対処 |
|---|---|---|
| 会議が存在しない/未開始 | meeting_not_started / meeting_not_found / meeting_ended / meeting_not_accessible | ユーザーに会議URLの再確認を促す。リトライは無意味 |
| 入室を拒否された | meeting_locked / meeting_full / meeting_requires_sign_in / meeting_requires_registration / meeting_password_incorrect | 会議設定の変更が必要。Signed-in Bots の導入を検討 |
| リンクの問題 | meeting_link_expired / meeting_link_invalid | URL再取得のフローを用意 |
| Recall側の問題 | failed_to_launch_in_time / bot_errored | リトライ可。多発するならサポートへ |
meeting_requires_sign_in が頻発するならSigned-in Bots(Googleアカウント等でログイン済みのbotを使う機能)の検討時期です。 Google Meet の「組織外を拒否」設定や Teams のロビー制約は、 匿名botではどうにもなりません。
4. エラーコードとリトライ
| コード | 意味 | リトライ方針 |
|---|---|---|
402 | クレジット残高不足(セルフサーブ) | 入金するまで無意味 |
403 | WAFにブロックされた | リクエスト内容を見直す |
405 | 不正な操作(配車済みbotへのDELETE等) | 呼び出し箇所のロジックを修正 |
409 | コンフリクト | 指数バックオフでリトライ |
429 | レート制限 | Retry-After ヘッダに従う |
502〜504 | サーバ過負荷 | リトライ |
507 | アドホックbotのプール枯渇 | 30秒後にリトライ。根治はスケジュールbot化 |
レート制限はスライディングウィンドウ方式でワークスペース単位に掛かります。 自分のワークスペースの総合的な上限はダッシュボードの Developers > Rate Limits で確認しますが、エンドポイントごとの上限はAPIリファレンスの各ページに明記されています。 たとえば Output Audio と Send Chat Message はいずれも300 requests / min / workspace です。
この300/分という数字は、botに喋らせる実装では現実的な制約になります。 TTSの出力を細かく分割して連投する設計にすると、1本の会議だけで上限に触れる可能性があります—— 発話は文単位以上のまとまりでまとめて投げるのが無難です。 429を受けたら Retry-After ヘッダを尊重してください。
コスト設計
課金は録画時間の秒単位課金です。参加者数は影響しません。
| 項目 | 単価(Pay-as-you-go) |
|---|---|
| 録画(Meeting Bot / Desktop SDK 共通) | $0.50 / 時間 |
web_4_core variant | $0.60 / 時間 |
| ストレージ | 7日間は無料。以降30日保持で $0.05 / 時間 |
| 文字起こし(Recall内蔵・async / realtime共通) | $0.15 / 時間 |
| 無料枠 | 最初の5時間 |
30分の会議+文字起こしなら $0.25 + $0.075 = 約$0.33。 月1000本回して約$330——外部プロバイダの文字起こしを使うならその分は別計上です。
コストの落とし穴
- botの居座り: 前述の
automatic_leaveデフォルト。誰も来ない会議で20分課金される - ストレージの積み上がり: 7日を超えて置くと課金対象。自前S3に退避して
retentionを短く保つほうが安い場合が多い - asyncの再実行: 文字起こしをやり直すと、その分の文字起こし料金は再度発生する
- web_4_core の常用: 単純録画だけなら不要。output_media を使うときだけ指定する
開発の回し方
webhookのローカル開発
webhookが中心のAPIなので、ローカルにトンネルを掘るのが実質必須です。 ngrok や Cloudflare Tunnel でローカルポートを公開し、 そのURLを realtime_endpoints に指定します。 公式ドキュメントにも Local Webhook Development Setup / Testing Webhooks Locally の項があります。
ここで効くのが metadata です。 Create Bot 時に任意のキーバリューを入れておくと、 すべてのwebhookペイロードの data.bot.metadata に返ってきます。 開発中は {"env": "local", "dev": "sakatsu"} のようなタグを入れておくと、 チームで共有しているワークスペースでも自分のイベントだけ拾えます。
デバッグの武器
- Explorer Dashboard — botの状態遷移、録画、文字起こしをUIで確認できる。「なぜ入室に失敗したか」はまずここを見る
- Debugging Bots のドキュメント — botの動画ログを含む調査手順が載っている
- docs MCP サーバ — Recallは公式にドキュメントMCPと
llms.txtを提供している。Claude CodeなどのAIエージェントに直接ドキュメントを読ませられるので、フィールド名の確認はこれが最速 - Coding Agents/LLM Quickstarts — AIエージェント向けのクイックスタートが公式にある
実装チェックリスト
ここまでの要点を、着手順に並べておきます。
- リージョンを決める(後から移行不可。日本向けなら
ap-northeast-1) - 録音方式を決める(迷ったら Meeting Bot API)
- API v1.11 であることを確認する(ネットのサンプルは v1.10 が混在)
- 文字起こしを検証する。日本語リアルタイムが必要なら外部プロバイダを比較する
diarization.use_separate_streams_when_availableを立てる- webhookを購読設定する(ダッシュボード)+署名検証を実装する(生ボディで)
- 必要なartifactの
*.doneを個別購読する(bot.done一本待ちにしない) join_atを10分以上先に設定する(507と同時30の回避)automatic_leaveを実態に合わせて短縮する(コスト直撃)metadataに自社IDを入れる(突合が一発になる)- webhook受信時に自前ストレージへ退避する(
download_urlは7日で失効) sub_codeをユーザー起因 / Recall起因に分類してハンドリングする
まとめ
Recall.aiの価値は「便利なAPIがある」ことではなく、プラットフォームごとの会議参加ロジックという、 差別化に一切寄与しない保守負債を外部化できることです。 ここに自社エンジニアの時間を溶かす理由は、ほとんどのプロダクトにありません。
一方で、APIの薄いラッパーとして扱うと必ず刺されます。join_at の10分境界、automatic_leave のデフォルト値、 7日で消える download_url、 そして日本語リアルタイム文字起こしのモード制約—— これらは仕様書の隅ではなく、設計の中心に置くべき制約です。
逆に output_media と output_audio の組み合わせは、 「会議に参加して喋るAI」を驚くほど短いコードで実現します。 録画APIとして使うだけなら他にも選択肢はありますが、botを対話の当事者にするという一段先の使い方まで見据えると、 Recallを選ぶ理由がはっきりしてきます。
理解度チェック
Recall.aiでbotを作るとき、___ パラメータを現在から10分以上先に設定すると「スケジュールbot」になり、同時実行数の制限がなくなり507エラーも起きなくなる。(フィールド名を英語で)