会議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 APIRecallが管理するbotが会議に参加者として入る汎用的な議事録・商談解析・AI面接。プラットフォーム横断で最も広くカバーできる参加者一覧にbotが見える。会議側の設定で入室拒否されうる
Desktop Recording SDKユーザーのPC上でローカル録音(Electronアプリに組み込む)botを見せたくない・入室許可が取れない環境。ネイティブアプリを持っているプロダクトWindows と Apple Silicon Mac のみ。ユーザーがアプリを入れる必要がある
Meeting Direct ConnectZoom 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 Westhttps://us-west-2.recall.ai
US Easthttps://us-east-1.recall.ai
EUhttps://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
v1.11のデータモデル — recordingが録画の単位、その下に種類別のmedia artifactが並ぶ

重要なのは 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ライフサイクル管理・成果物の取得タイミング検知
リアルタイムendpointCreate Bot の recording_config.realtime_endpointstranscript.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_latency1〜3秒英語のみ最小限
prioritize_accuracy3〜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 配列で宣言します。typewebhookwebsocket を指定します。

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.datavideo_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が担当
botを対話エージェントにする構成 — 入力はrealtime endpoint、出力はoutput_audio / output_media

映像: 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" }
    }
  }
}

variantweb_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_at10分以上先join_at が10分以内 or 未指定
リソース専用マシンを事前確保起動済みのウォームプールから確保
同時実行数制限なし30まで
遅刻「絶対に遅刻しない」と明言プール枯渇時は失敗しうる
失敗時507 が返る。30秒後にリトライ

つまり join_at を省略してその場でbotを立てる実装は、同時30会議の壁と 507 Insufficient Storage を常時抱えることになります。 カレンダー連携やイベント登録の時点で会議URLと開始時刻が判明した瞬間にスケジュールbotを作る——これが正解です。

更新・削除のルールも join_at の10分境界に紐づきます。

  • join_at10分以上前: Update Scheduled Bot で任意フィールドを変更可、Delete Scheduled Bot で削除可
  • join_at10分以内: 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_timeout20分居座ります。 短い会議を大量に回すプロダクトなら、waiting_room_timeoutnoone_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_invalidURL再取得のフローを用意
Recall側の問題failed_to_launch_in_time / bot_erroredリトライ可。多発するならサポートへ

meeting_requires_sign_in が頻発するならSigned-in Bots(Googleアカウント等でログイン済みのbotを使う機能)の検討時期です。 Google Meet の「組織外を拒否」設定や Teams のロビー制約は、 匿名botではどうにもなりません。

4. エラーコードとリトライ

コード意味リトライ方針
402クレジット残高不足(セルフサーブ)入金するまで無意味
403WAFにブロックされたリクエスト内容を見直す
405不正な操作(配車済みbotへのDELETE等)呼び出し箇所のロジックを修正
409コンフリクト指数バックオフでリトライ
429レート制限Retry-After ヘッダに従う
502504サーバ過負荷リトライ
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エージェント向けのクイックスタートが公式にある

実装チェックリスト

ここまでの要点を、着手順に並べておきます。

  1. リージョンを決める(後から移行不可。日本向けなら ap-northeast-1
  2. 録音方式を決める(迷ったら Meeting Bot API)
  3. API v1.11 であることを確認する(ネットのサンプルは v1.10 が混在)
  4. 文字起こしを検証する。日本語リアルタイムが必要なら外部プロバイダを比較する
  5. diarization.use_separate_streams_when_available を立てる
  6. webhookを購読設定する(ダッシュボード)+署名検証を実装する(生ボディで)
  7. 必要なartifactの *.done を個別購読するbot.done 一本待ちにしない)
  8. join_at を10分以上先に設定する(507と同時30の回避)
  9. automatic_leave を実態に合わせて短縮する(コスト直撃)
  10. metadata に自社IDを入れる(突合が一発になる)
  11. webhook受信時に自前ストレージへ退避するdownload_url は7日で失効)
  12. sub_code をユーザー起因 / Recall起因に分類してハンドリングする

まとめ

Recall.aiの価値は「便利なAPIがある」ことではなく、プラットフォームごとの会議参加ロジックという、 差別化に一切寄与しない保守負債を外部化できることです。 ここに自社エンジニアの時間を溶かす理由は、ほとんどのプロダクトにありません。

一方で、APIの薄いラッパーとして扱うと必ず刺されます。join_at の10分境界、automatic_leave のデフォルト値、 7日で消える download_url、 そして日本語リアルタイム文字起こしのモード制約—— これらは仕様書の隅ではなく、設計の中心に置くべき制約です。

逆に output_mediaoutput_audio の組み合わせは、 「会議に参加して喋るAI」を驚くほど短いコードで実現します。 録画APIとして使うだけなら他にも選択肢はありますが、botを対話の当事者にするという一段先の使い方まで見据えると、 Recallを選ぶ理由がはっきりしてきます。

理解度チェック

問題 0 / 50%
Q1

Recall.aiでbotを作るとき、___ パラメータを現在から10分以上先に設定すると「スケジュールbot」になり、同時実行数の制限がなくなり507エラーも起きなくなる。(フィールド名を英語で)