ミニアプリの作り方
ミニアプリはただのWebサイトです。 中身はあなたのサーバーに置いたままで構いません。Vespoは開く場所と身分証明を貸すだけです。
全体の流れ
- ① 入口 … 券を1回だけ使って「誰が開いたか」を知る
- ② 画面 … SDKを読んで、いまの様子を受け取る
- ③ サーバー … 点数・保存・在室はここで確かめる
⚠️ ②と③を混ぜないのが一番大事です。②は速いけれど本人が細工できる、 ③は遅いけれど嘘をつけない。見た目は②、決めごとは③で作ってください。
場所と、部屋の鍵
場所は自分で決めます。Vespoはマップを強制しません。
保存の place は自由な文字列です。全員が同じ世界なら place="world"、エリア分けなら "zone_3"、一人ずつの個室なら "house_" + user.key。トークの部屋の鍵は、その中の便利な既定値の1つでしかありません。
部屋の鍵が表しているのは「マップ」ではなく「メンバー」です。
同じトークにいる人だけが同じ鍵になります。誰でも入れる場所で足りるミニアプリには要りません。「このトークの4人だけで」という閉じた集まりを作りたい時に効きます。
鍵は偽れないので、入室の仕組みを自分で作らなくて済みます。
鍵を渡すのはVespoのサーバーで、中身から部屋をたどることもできません。だから「合言葉が漏れて知らない人が入ってきた」が起きません。招待・入っていいかの判定・追い出しを自作せずに、既にある人間関係をそのまま使えます。
⚠️ 鍵が届くのは「トークから開かれた時」だけです。
Vespoの設定の一覧から開かれた時は、部屋がないので room は入っていません。同じ人が同じミニアプリを開いても、開いた場所によって届く物が変わります。鍵が無い時に一人用として成り立つように作るか、「トークから開いてください」と案内してください(保存そのものは、どちらから開いても引き継がれます)。
① 入口で券を1回だけ使う(サーバー)
Vespoから開かれると、URLに ?rv_token=… が付きます。⚠️ 券は1回きり・60秒。画面で使うと再読み込みで必ず落ちるので、入口で確かめて自分のCookieに置き換えます。
POST https://www.vespo.app/v1/mini/verify
Authorization: Bearer <あなたのAPIキー>
Content-Type: application/json
{ "token": "URLに付いてきた券" }
→ {
"user": { "key": "…", "name": "…", "avatarUrl": "…" },
"room": { "key": "…" }, // scope: room を選んだ時だけ
"payload": "…", // 招待カードから開かれた時の合言葉
"stateToken": "…" // scope: call を選んだ時だけ(下で使う)
}
⚠️ user.key はあなたのミニアプリ専用のID。同じ人でも他のミニアプリでは違う値。
⚠️ 名前とアイコンは「その人がその場で使っている箱」の物。
同じ人でも開いた場所によって変わることがあります(IDは変わりません)。② 画面はSDKを1行読むだけ
繋ぎ方が端末で3通りあるのを吸収します。⚠️ SDKは任意で、下の「生の口」でも書けます。
<script src="https://www.vespo.app/sdk/v1.js"></script>
<script>
Vespo.connect().then(function (rv) {
// ── 受け取る ───────────────────────────────
rv.on('state', function (s) {
// s.inCall / s.with / s.speaking (⚠️ 画面に届く値なので信用しない)
// s.levels … 声の大きさ 0〜1。⚠️ 黙っている人は入っていない=無ければ0
});
rv.on('visibility', function (d) { /* d.active=false なら音や時間を止める */ });
rv.on('closing', function () { /* 片付け。⚠️ 保存はここに頼らない */ });
// ── 人に出す(⚠️ 押した瞬間に呼ぶこと。パソコンは小窓を開くため)──
document.querySelector('#call').onclick = function () {
rv.invite({ payload: 'dungeon:42' }); // 誰に送るかは本人が選ぶ
rv.post({ text: 'レベル10になりました' }); // 文は下書き。本人が直せる
rv.gift({ code: 'gem' }); // 贈る。誰に贈るかも本人が選ぶ
};
// ── 売る ───────────────────────────────────
document.querySelector('#buy').onclick = function () {
rv.buy({ code: 'gem' }).then(function (r) {
// ⚠️ 返事は端末で2通り。両方書かないと片方で黙って何も起きない
if (r.opened) return; // スマホ=本体がもう開いた
window.open(r.url, '_blank'); // パソコン=あなたが開く
});
// ⚠️ 「買えた」はここでは分からない。あなたのサーバーが
// /v1/mini/purchases に聞きに行って、そこで初めて渡す。
};
// ── 使えるかは本体が教えてくれる ─────────────
if (rv.can('voice')) {
rv.voice({
volumes: { someKey: 0.3 },
pan: rv.can('voicePan') ? { someKey: -0.9 } : undefined,
});
}
if (rv.can('haptic')) rv.haptic('hit'); // 震え。⚠️ スマホの板だけ
});
</script>
型(TypeScript): https://www.vespo.app/sdk/v1.d.ts
⚠️ rv.なんとか() は名前をそのまま流します。本体に機能が増えても入れ替え不要です。③ SDKを使わない場合(生の口)
やっていることはこれだけです。⚠️ 繋ぎ方が3通りあるので、SDKはこの差を埋めているだけ。
// 受け取る(どの端末でも同じ書き方)
window.addEventListener('message', function (e) {
// ⚠️ パソコンでは最初の message に「戻り道」のポートが付いてくる。
// 後から来た方を使い、受け取ったらすぐ返事をする
if (e.ports && e.ports[0]) {
port = e.ports[0];
port.postMessage({ type: 'robverse:ready' });
}
var d = e.data;
if (d && d.type === 'robverse:state') { /* いまの様子 */ }
});
// 送る(3通りのどれか)
function toVespo(msg) {
if (port) return port.postMessage(msg); // パソコン(別タブ)
if (window.ReactNativeWebView) // スマホ(板)
return window.ReactNativeWebView.postMessage(JSON.stringify(msg));
if (window.parent !== window) return window.parent.postMessage(msg, '*'); // 枠の中
}
toVespo({ type: 'robverse:ready' }); // 最新をもう一度もらう
toVespo({ type: 'robverse:invite', rid: 'r1', payload: 'dungeon:42' });
// → { type: 'robverse:invite_result', rid: 'r1', ok: true, closed: true }
// 結果を知らせる(「レベル10になりました」など)
toVespo({ type: 'robverse:post', rid: 'r2', text: 'レベル10になりました' });
// → { type: 'robverse:post_result', rid: 'r2', ok: true, closed: true }
toVespo({ type: 'robverse:buy', rid: 'r3', code: 'gem' });
// → { type: 'robverse:buy_result', rid: 'r3', ok: true, opened: true } スマホ
// → { type: 'robverse:buy_result', rid: 'r3', ok: true, url: '…' } パソコン
toVespo({ type: 'robverse:gift', rid: 'r4', code: 'gem' });
// → { type: 'robverse:gift_result', rid: 'r4', ok: true, closed: true }
// ⚠️ パソコンでは本体の画面に面が出ます(別タブからは見えない)。
// SDKの rv.gift() は小窓を開くので、そちらを使ってください。
// ⚠️ 送ったかどうかは返りません(「送るまで閉じない」を作れないようにするため)④ 点数を決める時は自分のサーバーから
⚠️ 画面に届く値は本人が細工できます。育成・点数・在室は必ずこちらで確かめてください。どれも APIキー + stateToken の両方が要ります。
POST https://www.vespo.app/v1/mini/state { "stateToken": "…" }
→ { inCall, since, roomKey, with:[…], othersCount, now }
⚠️ 元はLiveKitの記録なので嘘をつけません。
⚠️ now は**サーバーの時刻**。時間で何かを決める(1日1回・制限時間)なら
画面の時計ではなくこれを使ってください。端末の時計はずらせます。
POST https://www.vespo.app/v1/mini/save { "stateToken": "…", "place": "…", "data": {…}, "ifVersion": 3 }
→ { ok: true, version: 4 } / 版が合わなければ 409(読み直してやり直す)
⚠️ place は「あなたが自由に決める文字列」。
4人で遊ぶ → room.key / 同じ世界 → "world" / 個室 → "house_" + user.key
⚠️ data を付けなければ「読む」。1回1MBまで。
POST https://www.vespo.app/v1/mini/place { "stateToken": "…", "place": "…" }
→ { here: [{ key, name, avatarUrl }] }
⚠️ 「居ると伝える」と「居る人を聞く」が1回で済みます。20〜30秒ごとに呼んでください
(60秒来ないと居ないことになります)。
POST https://www.vespo.app/v1/mini/score { "stateToken": "…", "board": "score", "value": 1200 }
→ { value: 1200, order: "high", keep: "best", changed: true }
⚠️ board は「あなたが自由に決める文字列」。週替わりにしたければ名前に週を入れる
("2026w35")。こちらで自動リセットはしません。
⚠️⚠️ order(high=大きいほど上 / low=小さいほど上)と keep(best=自己最高 /
last=いつも上書き)は**そのボードの最初の1回で決まって、あとから変わりません**。
返ってくる order/keep が実際に効いている物です。
⚠️ changed:false は「自己最高を更新できなかった」であって失敗ではありません。
POST https://www.vespo.app/v1/mini/purchases { "stateToken": "…" }
→ { purchases: [{ id, code, name, amountJpy, at, gift }] }
⚠️ 支払いが確定した物だけ。決済ページを開いただけの物は返りません。
⚠️⚠️ 「買えた」を画面から受け取らないでください。画面の言葉は偽装できます。
⚠️ 消える物(回数券・コイン)を売る時は id を自分の表に記録して、
一度渡した物を二度渡さないようにしてください。
⭐ gift:true は「誰かから贈られた物」。持ち主は受け取った人です。
POST https://www.vespo.app/v1/mini/rank { "stateToken": "…", "board": "score", "scope": "friends" }
→ { me: { rank, value }, total, rows: [{ key, name, avatarUrl, rank, value, me }] }
⭐ scope:"friends" = Vespoの友だち + いま開いているトークに居る人。
**誰が友だちかは渡りません**(こちらで絞ってから順位だけ返します)。
トークから開いていない時は友だちだけになります。
⚠️ scope:"all" は、そのボードに点数を置いた人みんな。
⚠️ 自分の行は圏外でも必ず入ります(「あなたは128位」を出せます)。
⚠️ 名前とアイコンは name / avatar に同意している時だけ入ります。
POST https://www.vespo.app/v1/mini/friends { "stateToken": "…" }
→ { friends: [{ key, name, avatarUrl, lastOpenedAt }] }
⭐ 友だちのうち**このミニアプリを開いたことがある人**。
rank は点数を置いた人しか返さないので、「まだ遊んでいないけど入れてはいる
友だち」を誘いたい時はこちら。
⚠️ 友だち=Vespoの友だち+いま開いているトークに居る人(rank と同じ)。
**誰が友だちかは渡りません**(このミニアプリを開いた人だけが返ります)。合図の一覧
画面と本体のやりとりはこれだけです。SDKを使う場合は覚えなくて構いません。
| 向き | 合図 | 中身 |
|---|---|---|
| 本体 → ミニアプリ | robverse:state | いまの様子。通話が始まる・終わると届きます。中に rv.can(使える物の一覧)も入っています。 |
| 本体 → ミニアプリ | robverse:visibility | { active } アプリが裏へ回ったら false。音やアニメを止めてください(板を畳んでも中は動き続けます)。 |
| 本体 → ミニアプリ | robverse:closing | { graceMs } もうすぐ終了します。片付け(保存など)をして closing_result を返してください。返すとすぐ閉じます。返さなくても graceMs(いまは500ミリ秒)で必ず閉じます。 |
| ミニアプリ → 本体 | robverse:ready | 最新の様子をもう一度もらう。画面ができた直後に1回送ります。 |
| ミニアプリ → 本体 | robverse:invite | { payload } 仲間を呼ぶ面を出す。送り先を選ぶのは本人です。→ invite_result { closed }。⚠️送ったかどうかは返りません(「送るまで閉じない」を作れないようにするため)。 |
| ミニアプリ → 本体 | robverse:post | { text } 結果をトークに知らせる面を出す(「レベル10になりました」など)。⚠️text は**下書き**で、本人が直せますし、送らずに閉じることもできます。送り先を選ぶのも本人です。→ post_result { closed }。⚠️送ったかどうかは返りません。 |
| ミニアプリ → 本体 | robverse:voice | { volumes, pan } 声の大きさと左右。→ voice_result { ok, panApplied } |
| 決まりごと | rid と *_result | 送る時に rid を付けると、同じ rid で <名前>_result が返ります。対応していない名前には ok:false / error:'unsupported' と、使える物の一覧が返ります。 |
覚えておくこと
終了する時の保存は当てにしない。
閉じる前に robverse:closing でひと声かけますが、間に合うのは保存1回ぶんくらいです。アプリごと落とされる・ブラウザを閉じられる・電波が切れる、のどれでも合図は届きません。保存は「変わった時にその都度」が本筋で、終了時のはおまけと考えてください。
画面に届く物は信用しない。
見た目に使うのは自由ですが、点数を決める時は必ず自分のサーバーから /v1/mini/state を呼んでください。
APIキーはブラウザに置かない。
画面のJavaScriptから /v1/* を叩くと、キーが誰にでも見えます。サーバーからだけ呼んでください。
券は1回きり・60秒。
入口で使って、あとは自分のCookieで動かします。画面で使うと再読み込みのたびに落ちます。
invite / gift は rv.call() で呼ばない。
SDKは名前を素通しする作りなので rv.call('gift', …) でも通ってしまいますが、小窓を開くのはSDKの中の処理なので素通りします。パソコンでは画面に何も出ません。この2つだけは rv.invite() / rv.gift() と名前付きで呼んでください。
買えたかどうかは画面に届きません。
画面に「買えた」を伝える作りにすると、その言葉を偽装してタダで手に入ります。あなたのサーバーが /v1/mini/purchases に聞きに行って、そこで初めて渡してください。値段もVespoのDBが正本なので、こちらから金額を渡すことはできません(合言葉だけ)。
同時に書く事故は ifVersion で防ぐ。
同じ place を2人が同時に書くと、付けていない方は黙って消えます。409が返ったら読み直してやり直してください。
動いている見本
みんなの木=通話していた時間で木が育つミニアプリ。①〜③を通しで使っています (身分・場所・様子・保存・招待・声)。Vespoのアプリからためしアプリを開くと遊べます。
⚠️ 育つ時間は見本のサーバーが /v1/mini/state に聞いて決めています。画面で「通話中」を書き換えても木は育ちません。これが③の形です。
登録しなくても「テスト」でURLを入れて動かせます(localhost も可)。
SDK: https://www.vespo.app/sdk/v1.js / 型: /sdk/v1.d.ts