スマートリング「Pebble Index 01」で話した内容は、Webhookを使ってGrok Botのルーティンに送れます。
ただし、PebbleアプリにGrok BotのWebhookのURLをそのまま入れると、テスト送信が「415 ERROR」で失敗します。
原因は送る形式の違いで、Pebbleはフォーム形式(multipart/form-data)でしか送らず、Grok BotのWebhookはJSONの本文を受け取るからです。
間にCloudflare Workerを挟んでJSONに直したところ、私の環境ではテスト送信で200 OK(1083ミリ秒)が返りました。
翌日の本番では、リングで話した依頼がYouTube担当のBotまで届いています。
この記事では、私(須澤)が自分のリングとGrok Botをつないだ手順と、415でつまずいた原因、使ったWorkerのコードをまとめました。
公式の資料は、2026年11月4日時点の内容です。
できることと全体の流れ
リングのボタンを押しながら話すと、その内容が担当のBotに振り分けられ、結果がチャットに返ってきます。

- リングのボタンを押しながら話す(Hold & Talk)
- Pebbleアプリが文字起こしして、Webhookで送る
- Cloudflare Workerが、フォーム形式をJSONに直して転送する
- Grok BotのWebhookのルーティンが受け取る
- 内容に合った担当のBot(WEB制作・AI BLOG・blog・ASPの管理・ポイ活・YouTube・LINEスタンプなど)に振り分ける
- 担当のBotが作業して、結果をチャットで報告する
たとえば歩きながら「ブログのネタ、〇〇について書きたい」とつぶやけば、blog担当のBotにメモが届きます。
録音されるのは、リングのボタンを押している間だけです。
録音はBluetoothでスマホのPebbleアプリに送られ、文字起こしやWebhookの送信はアプリ側で行われます。
必要なもの
| もの | 条件・料金 | メモ |
|---|---|---|
| Pebble Index 01 | スマホにPebbleアプリが要る | Webhookの設定はアプリの「Index 01」の設定にある |
| Grok Bot | Webhookで起動するルーティンを作れること | ルーティンが動くたびに使用量を使う |
| Cloudflareのアカウント | 無料プランで足りる | Workersの無料プランは1日10万リクエストまで |
Grok Botを使えるプランは、Grok Botのメールアドレスの記事の料金プランの節にまとめています。
手順1:Grok BotでWebhookのルーティンを作る
最初に作るのは、Grok BotのWebhookで起動するルーティンです。
チャットで「Pebbleから届いた内容を振り分けるルーティンを作って」と頼めば、Botが作ってくれます。
メールでルーティンを起動するやり方は、Grok Botのメールアドレスの作り方で説明しました。
できたルーティンをPCで開くと、Webhookの欄に次の3つが出ます。
| 欄 | 中身 | 使い道 |
|---|---|---|
| POST to | WebhookのURL | 送り先。末尾がルーティンごとのIDになっている |
| key | 秘密の鍵(crsr_で始まる) | 送る側が Authorization: Bearer に入れる |
| header | Authorization: Bearer + 鍵の1行 | そのまま写せる形 |
URLの形は https://api2.cursor.sh/automations/webhook/<ID> でした。
ヘルプには、このURLにJSONの本文を送れば、Botがルーティンの指示と一緒にその本文を受け取ると書かれています。
200が返ったら、Grok Botが受け付けて実行を始めたという意味です。
指示を終えたという意味ではないので、結果はBotのチャットで確かめます。
鍵はパスワードと同じ扱いです。
チャットに貼ったり、スクリーンショットに写したりしないでください。
画面を人に見せるときは、URLの欄と鍵の欄をぼかしてください。
手順2:PebbleアプリのWebhookの設定を開く
Pebbleアプリで「Index 01」の設定を開き、「Webhook」をタップします。
Webhookは押し方ごとに設定が分かれていて、「Hold & Talk」と「Double click & hold」の2つがあります。
今回は「Hold & Talk」に設定しました。

| 項目 | 中身 |
|---|---|
| Webhook URL | 送り先のURL |
| What to send | Recording(録音)・Transcription(文字起こし)・Both(両方)。今回はTranscription |
| Headers | 送るときに付けるヘッダー。名前と値の組をいくつでも足せる |
| Sign requests | HMAC-SHA256の署名を付けるかどうか |
| Send test event | テスト送信のボタン |
| Recent runs | 送った履歴。押し方ごとに直近20件 |
ボタンの押し方に何を割り当てるかは、Index 01の設定の「Ring Button」で決めます。
Hold & Talkに選べるのは、Index agent・Web search・MCP sandbox・Webhook only・Nothingの5つです。
Grok Botにだけ送るなら、選ぶのは「Webhook only」です。
Index agentなどを選んだままでも、「Also send to webhook」のスイッチを入れれば、Webhookにも写しが送られます。
Double click & holdにも別のWebhookを設定できるので、2つのルーティンを押し方で使い分けることもできます。
つまずいたところ:415 ERROR
最初は、Grok BotのURLをそのままPebbleに入れて、Authorizationのヘッダーを付けてテストしました。
結果は 415 ERROR でした。
送る形式の違いを疑って Content-Type: application/json のヘッダーを足しても、やはり415のままでした。
上の画面のRecent runsに残っている2回の415が、このときのものです。
原因:Pebbleはフォーム形式でしか送らない
Pebbleの公式ドキュメントを読むと、理由がわかりました。
PebbleのWebhookは multipart/form-data でPOSTする決まりで、変えられません。
ヘッダーでContent-Typeを書いても、本文はフォーム形式のままです。
415(Unsupported Media Type)は、受け取る側が本文の形式を受け付けないときのエラーです。
フォーム形式で送るPebbleと、JSONの本文を受け取るGrok Botの間で、形式が合っていなかったのが415の正体でした。
Pebbleから届く項目とヘッダー
フォームに入って届く項目は、次のとおりです。
| 項目 | 中身 | 届くとき |
|---|---|---|
| transcription | 文字起こしの文章 | TranscriptionかBothのとき |
| audio | 録音(m4a・モノラル・16kHz) | RecordingかBothのとき |
| recordedAt | 録音した時刻(ミリ秒) | いつも |
| client | いつも ring | いつも |
| test | true | テスト送信のときだけ |
ヘッダーには、自分で足したもののほかに、次のものが自動で付きます。
| ヘッダー | 中身 |
|---|---|
| X-Index-Trigger | 押し方(single-click-hold・double-click-hold・テストは test-event) |
| X-Index-Webhook-Version | 仕様の版。今は 1 |
| X-Index-Delivery | 録音ごとのID。同じ録音を送り直しても同じなので、重複の判定に使える |
| X-Index-Test | テスト送信のときだけ true |
| X-Index-Signature・X-Index-Timestamp | 署名を有効にしたときだけ付く |
| X-Audio-Size | 録音を送るときだけ。録音の大きさ |
GitHubのZhigre/pebble-index-bridgeでも同じ415が取り上げられていて、間に中継役を挟んで解決しています。
解決法:Cloudflare Workerで形式を直す
そこで、PebbleとGrok Botの間にCloudflare Workerを挟み、フォーム形式をJSONに直して転送することにしました。
Workerは、Cloudflareの上で小さなプログラムを動かせる仕組みで、自分でサーバーを用意しなくて済みます。
設計で気をつけたのは、次の3点です。
- Workerに秘密の値を持たせない:Pebbleが付けてくるAuthorizationのヘッダーを、そのまま転送するだけにした
- URLのパスはそのまま使う:Pebbleには
https://<Worker名>.<サブドメイン>.workers.dev/automations/webhook/<ID>を設定し、Workerはhttps://api2.cursor.shに同じパスを付けて転送する - 転送先を固定する:受け付けるのは
/automations/webhook/で始まるパスだけ、転送先はapi2.cursor.shだけにして、何でも中継する踏み台にされないようにした
Workerのコード
export default {
async fetch(request) {
const url = new URL(request.url);
if (request.method !== "POST") return new Response("pebble-grok-bridge ok");
if (!url.pathname.startsWith("/automations/webhook/")) return new Response("not found", { status: 404 });
const auth = request.headers.get("Authorization");
if (!auth) return new Response("missing Authorization", { status: 401 });
const ct = request.headers.get("content-type") || "";
let raw = {}; let text = "";
try {
if (ct.includes("json")) { raw = await request.json(); }
else if (ct.includes("form")) {
const form = await request.formData();
for (const [k, v] of form.entries()) raw[k] = typeof v === "string" ? v : "[file: " + (v.name || k) + "]";
} else { raw = { body: await request.text() }; }
text = raw.transcription || raw.text || raw.transcript || raw.body || "";
} catch (e) { text = "(読み取り失敗) " + e.message; }
const payload = {
source: "pebble", text, test: raw.test === "true" || request.headers.get("X-Index-Trigger") === "test-event",
trigger: request.headers.get("X-Index-Trigger"), delivery: request.headers.get("X-Index-Delivery"),
recordedAt: raw.recordedAt ? new Date(Number(raw.recordedAt)).toISOString() : null,
receivedAt: new Date().toISOString(), raw
};
const res = await fetch("https://api2.cursor.sh" + url.pathname, {
method: "POST", headers: { "Content-Type": "application/json", "Authorization": auth }, body: JSON.stringify(payload)
});
return new Response(await res.text(), { status: res.status });
}
};録音のファイルが届いたときは、中身を転送せず、ファイル名の印だけを残します。
Grok Botから返ってきた状態コードは、そのままPebbleに返します。
手元で動かして確かめたこと
このコードを手元のNode.jsで動かし、Pebbleと同じ形のフォームを送って、転送される中身を確かめました。
| 送ったもの | 結果 |
|---|---|
| テスト送信と同じフォーム(録音のファイル付き) | JSONに直して api2.cursor.sh の同じパスへ転送。録音は [file: ファイル名] に置き換わる |
| 別のパスへのPOST | 404を返し、転送しない |
| Authorizationが無いPOST | 401を返し、転送しない |
Grok Botに届くJSONは、次の形になります。
{
"source": "pebble",
"text": "Index webhook test event",
"test": true,
"trigger": "test-event",
"delivery": "(送信ごとのID)",
"recordedAt": "(録音した時刻)",
"receivedAt": "(Workerが受け取った時刻)",
"raw": { "transcription": "Index webhook test event", "recordedAt": "…", "client": "ring", "test": "true" }
}デプロイのしかた
Cloudflareのダッシュボードから作るなら、流れは次のとおりです。
メニューの名前は変わることがあるので、画面に合わせて読み替えてください。
- Cloudflareのダッシュボードで「Workers & Pages」を開く
- 「Create」から、Hello Worldのひな形で新しいWorkerを作る
- 名前(例:pebble-grok-bridge)を付けてデプロイする
- 「Edit code」で中身を上のコードに置き換え、もう一度デプロイする
- 表示された https://pebble-grok-bridge.<サブドメイン>.workers.dev を控える
私の場合は、Grok BotにCloudflareのコネクタをつないで許可しただけで、Workerの作成からデプロイまでGrok Botがやってくれました。
自分で触ったのは、許可のボタンくらいです。
手順3:Pebbleの設定を書き換える
Workerができたら、PebbleアプリのWebhookの設定を次のように直します。
| 項目 | 設定する値 |
|---|---|
| Webhook URL | https://pebble-grok-bridge.<サブドメイン>.workers.dev/automations/webhook/<ID>(IDはGrok BotのURLの末尾と同じ) |
| Headers | Content-Typeの行は消し、名前 Authorization・値 Bearer crsr_… の1行だけにする |
| What to send | Transcription |
手順4:テストして保存する
「Send test event」を押すと、200 OK(1083ミリ秒)が返りました。

Grok Botのチャットにも、「テストイベントを受信できました。本文は『Index webhook test event』」という報告が届いています。
テスト送信の文字起こしには、決まった文章が入る仕様です。
私のルーティンでは、テスト送信のときは振り分けをせず、受け取れたことだけを報告するようにしています。
最後に「Save」で設定を保存します。
Pebbleは、URLを入れて保存した押し方からしか送らないので、保存を忘れると何も届きません。
振り分けるルーティンの指示
Grok Botのルーティンには、次の指示を入れています。
- test が true なら、テストを受け取れたことだけ報告して終わる
- 届いたJSONから本文(text、無ければ raw.transcription)を取り出す
- 内容から担当のBot(WEB制作・AI BLOG・blog・ASPの管理・ポイ活・YouTube・LINEスタンプなど)を選ぶ
- 選んだBotに、依頼の内容をそのまま渡す
- 送信・購入・削除など取り消せない作業は、私の確認をとってから
- 結果は1〜2文で短く報告する
チャットで頼むときは、たとえば次のように書きます。
Webhookで起動するルーティンを作って。
名前は「Pebble受付」。
届くJSONの text が、私がリングに話した内容。
1. test が true なら、受信できたことだけ報告して終わる
2. text の内容から、担当のBot(WEB制作・AI BLOG・blog・ASPの管理・ポイ活・YouTube・LINEスタンプ)を1つ選ぶ
3. 選んだBotに、text をそのまま依頼として渡す
4. 送信・購入・削除など取り消せない作業は、私の確認をとってから
5. 結果は1〜2文で報告する
6. 同じ delivery が2回届いたら、2回目は何もしない6番目の重複の扱いは、次の節で説明する送り直しに備えたものです。
本番で使ってみた:リングの依頼がYouTubeのBotに届いた
設定した翌日の2026年11月5日、リングに「YouTubeで、Web制作会社として数字が伸びてバズりやすいテーマを調べて並べて」と話しました。
PebbleアプリのRecent runsには、文字起こしの送信が200 OK(473バイト・2847ミリ秒)で残っています。

話した1分後には、Grok Botから「YouTubeのBotに回しました」という報告が届きました。

依頼を受けたYouTubeのBotは調べものを始め、伸びている動画の例と再生数を添えて、テーマの候補を返してきました。

ルーティンに書いた振り分けの指示どおりに、担当のBotまで依頼が渡っています。
文字起こしの聞き違いはGrok Botが補ってくれた
Pebbleアプリに残った文字起こしより、Grok Botが受け取って言い直した依頼のほうが正確でした。
文字起こしに聞き違いがあっても、Grok Botが前後の文脈から意図を読み取り、正しい内容でYouTubeのBotに渡しています。
私のPebbleアプリでは、音声の認識に「parakeet-tdt-0.6b-v3」というモデルが選ばれていました。
文字起こしが完全でなくても依頼が伝わるので、言い回しを気にせず、思いついたまま話せます。
使う前に知っておきたいこと
鍵をどこに置くか
私の方法では、Grok Botの鍵をPebbleアプリのヘッダーに入れ、Workerは中継するだけです。
GitHubのZhigre/pebble-index-bridgeは逆で、Grok Botの鍵は中継役のサーバーに置き、Pebbleには別に作った鍵だけを入れます。
| 筆者の方法 | pebble-index-bridge | |
|---|---|---|
| 中継役 | Cloudflare Worker | 自分のサーバーのDocker |
| Grok Botの鍵の置き場所 | Pebbleアプリのヘッダー | 中継役のサーバー |
| Pebbleに入れる鍵 | Grok Botの鍵そのもの | 中継役のための別の鍵 |
| 中継役が持つ秘密 | なし | Grok Botの鍵 |
| スマホの設定が漏れたとき | Grok Botの鍵を作り直す | 中継役の鍵だけ作り直せばよい |
| 準備の手間 | Workerを1つ作るだけ | サーバー・Docker・公開用の通り道が要る |
どちらが良いかは、どこに秘密を置きたいか次第です。
鍵が漏れたと思ったら、Grok Botにルーティンの鍵を作り直してもらい、Pebbleの設定も書き換えます。
失敗すると送り直され、重複することがある
Pebbleは、通信の失敗や408・429・5xxの応答のとき、1分ごとに最大10回まで送り直します。
415のような4xxは送り直されないので、最初の415は、Recent runsに失敗が残るだけでした。
WorkerはGrok Botの応答をそのまま返すため、Grok Bot側が5xxを返すと、Pebbleが同じ録音を送り直します。
同じ録音のX-Index-Deliveryは何度送っても同じなので、WorkerがJSONの delivery に入れたこの値で、ルーティン側が重複を見分けられます。
ルーティンの回数と使用量
Grok Botのルーティンが動ける回数は、すべてを合わせて1時間に300回、1つのルーティンでは1時間に200回までです。
超えると「Automation run rate limit exceeded」で失敗し、自動では送り直されません。
ルーティンは、1回動くたびに週の使用量が減る仕組みです。
テスト送信でもルーティンは実際に動くため、試す回数は少なめにしておくと安心です。
さらに安全にするなら:署名を検証する
Pebbleの「Sign requests」をオンにすると、HMAC-SHA256の署名がヘッダーに付きます。
Worker側でこの署名を確かめると、Pebble以外から届いた要求を断れるのが利点です。
私はAuthorizationの鍵で足りると判断して入れていませんが、URLと鍵を人に見せる機会がある人には向いています。
公式ドキュメントの手順をWorkers向けに書いたのが、次のコードです。
async function verifyIndexSignature(request, rawBody, secret) {
const h = (name) => request.headers.get(name) || "";
const version = h("X-Index-Webhook-Version");
const timestamp = h("X-Index-Timestamp");
const signature = h("X-Index-Signature");
if (version !== "1" || !/^\d+$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
if (!/^[0-9a-f]{64}$/.test(signature)) return false;
const isTest = h("X-Index-Test") === "true" ? "1" : "0";
const prefix = new TextEncoder().encode(
`v${version}\n${timestamp}\n${h("X-Index-Delivery")}\n${h("X-Index-Trigger")}\n${isTest}\n`
);
const signed = new Uint8Array(prefix.length + rawBody.byteLength);
signed.set(prefix);
signed.set(new Uint8Array(rawBody), prefix.length);
const key = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]
);
const sigBytes = new Uint8Array(signature.match(/../g).map((x) => parseInt(x, 16)));
return crypto.subtle.verify("HMAC", key, sigBytes, signed);
}Workerの本体には、次のように差し込みます。
// fetch(request) を fetch(request, env) にして、本文を読む前にこう差し込む
const rawBody = await request.arrayBuffer();
if (!(await verifyIndexSignature(request, rawBody, env.INDEX_WEBHOOK_SECRET))) {
return new Response("invalid signature", { status: 401 });
}
// フォームは、先に読んだ本文から作り直して読む
const form = await new Response(rawBody, { headers: { "content-type": ct } }).formData();署名は届いた本文そのものに対して計算されるので、フォームとして読む前に、本文をそのままの形で読みます。
署名の鍵には、Pebbleアプリに入れたものと同じ値を使い、Workerのシークレット(例の名前は INDEX_WEBHOOK_SECRET)に登録しておく形です。
公式ドキュメントは、32バイト以上のランダムな値を勧めています。
このコードは、公式の手順どおりに署名を付けた要求を作り、手元のNode.jsで確かめました。
| 試した要求 | 結果 |
|---|---|
| 正しい署名 | 通る |
| テスト送信の形(X-Index-Test: true) | 通る |
| 本文を1バイト変えた | 断る |
| 違う鍵で署名した | 断る |
| 10分前の時刻 | 断る(5分を超えたら断る) |
使い方のアイデア
- 移動中に思いついたブログのネタを、blogのBotにメモする
- 「〇〇さんのサイト、ここを直す」と、WEB制作のBotに作業を登録する
- ポイ活のキャンペーンの確認を、ポイ活のBotに頼む
- YouTubeの企画やLINEスタンプのアイデアを、思いついたその場で残す
スマホを取り出さなくていいので、思いつきを逃さないのがいちばんの良さです。
Grok Botのほかに、話しかけて仕事を任せられるAIのボットはAIボット4つの比較で比べています。
この記事で確かめた範囲
- 確認日:公式ドキュメントとヘルプは2026年11月4日に読んだものです
- 見たもの:Pebble公式のWebhooksとIndex 01、Grok BotヘルプのRoutines、CloudflareのWorkersの制限(いずれも英語)
- 実際に試したこと:自分のPebble Index 01とGrok Botをつなぎ、415から200 OKになるまでと、11月5日の本番の依頼がYouTubeのBotに届くまで
- 手元で確かめたこと:記事に載せたWorkerのコードと署名検証のコードを、Pebbleと同じ形の要求で動かした結果
画面の名前や仕様は変わることがあるので、設定の前に公式の資料も合わせて見てください。
まとめ
- Pebble Index 01は、話した内容の文字起こしをWebhookで送れるので、Grok Botのルーティンにつなげられる
- Pebbleはフォーム形式でしか送らず、Grok BotのWebhookはJSONの本文を受け取るので、直接つなぐと415になる
- Cloudflare Workerを挟んでJSONに直せば通る(Workerには秘密を持たせず、転送先も固定する)
- Pebbleは失敗すると送り直すので、ルーティン側で delivery を見て重複を除く
- 本番では、リングで話した依頼がYouTubeのBotまで届き、文字起こしの聞き違いもGrok Botが補った
- ルーティンは動くたびに使用量を使い、1時間あたりの回数にも上限がある
- 鍵は人に見せず、スクリーンショットではURLと鍵をぼかす
リングに話しかけるだけで仕事が進むのは、思った以上に快適です。
Pebble Index 01を持っている人は、試してみてください。
公式の資料は、次のページです。
Webhooks(Pebble Developers)
Routines(Grok Bot ヘルプ)
