ひとつの API、ふたつの認証情報。コンソールも他のプログラムと同じく、この
API の一利用者です。人はセッション Cookie で、プログラムは Bearer トークンで、
どちらも同じルートを使います。すべてのパスは /api/v1 配下にあり、この
ページは連携プログラムが依存すべき厳選されたサブセットです。
トークンの取得
管理者として チーム → API トークン → 新しいトークン。シークレットの 表示は一度きりで、保存されるのはハッシュだけ。失効は即時です。
トークンが持つのはメンバーの権限です — 画面に映るものは変えられますが、
画面そのものには触れません。画面の登録・再起動・設定変更、メンバーの招待、
別トークンの発行はできず、管理者と記された操作はすべて 403 を返します。
すべてのリクエストに付けてください:
Authorization: Bearer anysign_<64 hex characters>エラーはどこでも同じ形式です。機械可読の reason、英語の message、
そして文が制限値に言及するときの params。
モデル:スロットではなくスタック
画面のコンテンツはレイヤーのスタックで、画面は中身のある最も高い レイヤーを表示します:
emergency 組織全体の強制表示
takeover 「いますぐこれを」— オペレーターの、またはあなたの
source 連携プログラムが保持するレイヤー
scheduled 時間帯スケジュール
base 日常のループあるレイヤーへの書き込みは他のレイヤーに触れず、レイヤーを外せば下にある ものが現れます。画面の共有を安全にする取り決めはこれです:連携プログラム が所有するのは画面ではなくレイヤー。 自分の層だけに書けば、オペレーターは いつでもあなたの上から画面を取り戻せるし、あなたが何を覆っていたかも常に 見えます。
制限
GET /limits
この API がリクエストを判定する上限値です。ハードコードせず読んでください — あなた側のリリースなしで引き上げられることがあります。
{
"upload": { "maxBytes": 33554432 },
"image": {
"maxBytes": 33554432,
"maxDimension": 4096,
"maxPixels": 8847360,
"unplayableMimes": ["image/webp"]
},
"video": { "maxBytes": 33554432 },
"playlist": { "maxItems": 500 }
}サーバー側のリサイズはありません。 保存されたバイトがそのまま重複排除の ハッシュ対象なので、変換はコンテンツアドレッシングを壊してしまいます。 リサイズはあなたの仕事です — ブラウザなら 6 行:
const bmp = await createImageBitmap(file, { imageOrientation: "from-image" });
const scale = Math.min(
1,
4096 / Math.max(bmp.width, bmp.height),
Math.sqrt(8847360 / (bmp.width * bmp.height)),
);
const canvas = Object.assign(document.createElement("canvas"), {
width: Math.floor(bmp.width * scale),
height: Math.floor(bmp.height * scale),
});
canvas.getContext("2d").drawImage(bmp, 0, 0, canvas.width, canvas.height);
const blob = await new Promise((r) => canvas.toBlob(r, "image/jpeg", 0.9));imageOrientation: "from-image" は重要です。これがないと縦位置の写真が
誤った軸で測られ、そのうえレンダラーにもう一度回転されます。
画面
GET /screens
{ "screens": [ {
"id": "…", "name": "Lobby", "site": "HQ",
"lastSeenAt": 1756400000000,
"connectivity": "online", // online | offline | never-seen
"state": "converged", // converged | syncing | no-content | unknown
"converged": true,
"content": { "layer": "takeover", "kind": "asset", "assetId": "…", "name": "menu.png" },
"layers": [ /* スタック全体、上から順に */ ]
} ] }判断の根拠にすべきフィールドは converged です。 画面が通信しており、
映すものがあり、最後に指示したバージョンを報告しているときに限って true に
なります — サーバーが送信しただけでは決してなりません。content は
スタックの最上段。報告が途絶えた画面は、最後に何と言っていようと unknown
になります。
GET /screens/:screenId は 1 台分を返し、稼働状態の詳細と、デバイスが
最後に報告した自身の状態が付きます。
PUT /screens/:screenId/layers/:layer
{ "playlistId": "…" } // プレイリストをループ再生
{ "assetId": "…", "durationMs": 15000 } // 単一の項目で画面を固定ちょうどどちらか一方。:layer は上の 5 つのいずれかです。プレイリストは
存在している必要があり、アセットはアップロード完了済みであること — まだ
存在しないバイトを指された画面はファイル欠落と数え、永遠に準備中のままに
なります。
DELETE /screens/:screenId/layers/:layer
レイヤーをひとつ外します。下にあるものが現れます。もともと空のレイヤーを クリアしても成功です — 消したかったものは消えています。
DELETE /screens/:screenId/layers
スタック全体を空にします — 「この画面を真っさらに」の直接的な手段です。
POST /screens/:screenId/commands
{ "type": "refetch" } // メンバー:いますぐ再同期
{ "type": "screenshot" } // メンバー:パネルの表示をキャプチャ
{ "type": "reboot" } // 管理者コマンドは画面の次回確認に相乗りして届き、実行結果も報告されます。
refetch はデバイスが誤ったバイトを抱えた場合の修復ツールであって、
近道ではありません — 通常の公開は画面自身の確認で届きます。
コンテンツのアップロード
POST /content?name=<filename>[&durationMs=<ms>]
ボディがそのままファイルです。型は Content-Type ヘッダーで指定します。
curl -X POST "$BASE/api/v1/content?name=menu.png" \
-H "authorization: Bearer $TOKEN" \
-H "content-type: image/png" \
--data-binary @menu.png{
"assetId": "…", "sha256": "…", "kind": "image", "bytes": 20480,
"width": 1920, "height": 1080, "deduped": false
}width と height はサーバーが測った値で、測れなかったときは null
です — ファイル自体は保存されますが、ここが null なら、その画面に何も
描画されないという合図です。画面に割り当てる前に必ず検証してください。
同一バイトの再アップロードは no-op で、既存のアセットを 200
(deduped: true)で返します。ふたつのプッシュが競合しても同じです。
これは意図的な設計です — コネクタの自然な形は「毎回ぜんぶ送る」だから
です。
応答の sha256 は保存されたバイトから当方が計算したもの。画像のガード レール(最長辺、総ピクセル数)はファイル自身の PNG/JPEG/GIF ヘッダー に対して実行され、テレビのパネルがデコードできない画像は画面を巻き込む 前に、入り口で拒否されます。
制限:1 リクエスト 32 MB、画像と動画のみ、WebP は不可 — アップロードも 同期も通りますが、パネルには決して描画されません。
32 MB 超はマルチパートで:POST /assets にファイルの sha256、mime、
バイト数、種類を渡すと assetId が返ります。PUT /assets/:assetId/parts/:n
は 10 MB 以下のパートを受け取り、それぞれ etag を返します。
POST /assets/:assetId/complete にパート一覧を渡せば完了、
POST /assets/:assetId/abort で破棄。コンソールが動画にこの経路を使うのは、
ブラウザが会場の Wi-Fi で再開する必要があるからで、同じ理由であなたにも
開かれています。ここでの sha256 はあなたの申告値です — 偽れば汚れるのは
自分のライブラリだけ。デバイスはダウンロード時に検証します。
GET /assets、GET /assets/:assetId/content
組織の準備済みアセット。後者はバイトを返し、Range に対応します。
PATCH /assets/:assetId
{ "name": "lobby-final.png" }表示名だけです。アイデンティティは sha256 なので、名前を変えてもバイトは 動かず、画面が保持しているものも無効になりません。
DELETE /assets/:assetId
決まったスケジュールでコンテンツを生成し、放っておけばライブラリ項目も
スケジュール通りに増やしてしまう連携のためのものです。アセットを保持して
いるものがあるときは削除せず 409 を返し、何が保持しているかを名指し
します:
reason |
意味 |
|---|---|
asset_still_uploading |
マルチパートアップロードがこの行を所有中。そちらを中止してください。 |
asset_in_playlists |
名指しされたプレイリストからまず外してください。 |
asset_on_screens |
画面が表示中。まずその画面のレイヤーをクリアするか差し替えてください。 |
最後のひとつは礼儀の話ではありません。画面が指しているバイトを消すと、 そのデバイスは永遠にファイル欠落を数え続け、二度と同期が完了しません。
プレイリスト
GET /playlists は一覧を返します。GET /playlists/:playlistId は項目
付きの 1 件を返すので、こちらの状態のコピーを持たずに、最後に書き込んだ
内容と突き合わせできます。
{
"playlist": { "id": "…", "name": "Photo wall" },
"items": [ {
"id": "…", "position": 0, "assetId": "…", "sha256": "…",
"kind": "image", "name": "guest-0042.jpg", "durationMs": 5000
} ]
}POST /playlists に { "name": "…" } で 201 と { "playlistId" }。
PATCH /playlists/:playlistId は名前変更。DELETE /playlists/:playlistId
は 1 件削除しますが、割り当てられた画面があれば 409 playlist_on_screens
でその画面を名指しします — 壁が映しているものの削除は、明示的な 2 段階で
なければなりません。
PUT /playlists/:playlistId/items
{ "items": [
{ "assetId": "…", "durationMs": 5000 },
{ "assetId": "…" }
] }これはリスト全体を置き換えます。そしてそれが特長です。望むリストを
送るだけで、差分の計算は一切不要。変更のないリストの再送は無害です。
順序は配列の順序。durationMs は省略可能 — 動画は自身の長さ、静止画は
デフォルト値が使われます。最大 playlist.maxItems 件。
存在しない、またはアップロード未完了のアセットを指す項目は
playlist_item_asset_not_found / playlist_item_asset_not_ready として
その assetId を名指しで拒否され、何も書き込まれません。
レート制限
ふたつの予算が、意図的に別のものを計っています。
| 予算 | キー | サイズ | 429 の reason |
|---|---|---|---|
| トークンで認証されたリクエスト | あなたのトークン | 120 / 分 | too_many_api_requests |
| 認証に失敗したリクエスト | あなたのアドレス | 20 / 分 | too_many_attempts |
ペース配分の対象は前者です。 トークン単位なので、どこかのプログラムの 暴走ループが他者の予算を使うことはなく、課金されるのはトークンだけ — プログラムがコンソールのオペレーターの枠を食いつぶすことはなく、その逆もありません。 後者は拒否だけに課金されます — 有効なトークンを持つリクエストは決して 触れません。実際に当たるのは、失効済みや打ち間違いのトークンをデプロイして 強くリトライしたときくらいです。
どちらも毎分の予算で、配信ロケーションごとに数えられます。使い切る枠では なく、バックオフすべき天井として扱ってください。429 は他の拒否と同じ形式で 返ります。バックオフしてリトライを — どちらの予算もアクセス禁止ではありません。
まだできないこと
忘れたと思われないよう、明記します:
- 画面単位・レイヤー単位のスコープはありません。 トークンは組織全体に 作用し、どのレイヤーにも書けます。「レイヤーの所有」は守るべき取り決め であって、サーバーが強制する規則ではまだありません。
- リースはありません。 同じレイヤーに書くふたつのプログラムは後勝ち です — だからこそ、ここでの書き込みはすべて監査されます。
- トークンに有効期限はありません。 コントロールは失効で、
lastUsedAtが忘れられたトークンをコンソールで可視化します。 - 外向きの webhook なし、再生証明のエクスポートなし、SSO なし。
- 429 に
Retry-Afterはありません。 どちらの予算も毎分なので、 1 分待てば必ず足ります。 - サーバー側リサイズなし — 上の制限を参照。欠落ではなく決定です。
- プッシュなし:コンテンツは即時ではなく、画面の 1 回の確認以内 (約 20 秒)で届きます。
- SDK なし。 ルートの半分は
curl1 行で済み、残りは JSON ボディを 受け取るだけです。
監査ログ
上記のすべての書き込みは — 人によるものもプログラムによるものも —
その時点での行為者の名前で記録されます。失効や名前変更で歴史は
書き換えられません。GET /audit で読むか、コンソールの
チーム → アクティビティ で確認できます。ふたりの書き手が画面を
共有し始めたとき、その共有を安全にしているのがこのログです。