本文へスキップ

公開 API

自社のソフトウェアから画面にコンテンツを配信する — トークン、レイヤースタック、エンドポイント、制限、監査ログ。

更新日 Markdown で表示

ひとつの 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
}

widthheight はサーバーが測った値で、測れなかったときは null です — ファイル自体は保存されますが、ここが null なら、その画面に何も 描画されないという合図です。画面に割り当てる前に必ず検証してください。

同一バイトの再アップロードは no-op で、既存のアセットを 200deduped: 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 /assetsGET /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 なし。 ルートの半分は curl 1 行で済み、残りは JSON ボディを 受け取るだけです。

監査ログ

上記のすべての書き込みは — 人によるものもプログラムによるものも — その時点での行為者の名前で記録されます。失効や名前変更で歴史は 書き換えられません。GET /audit で読むか、コンソールの チーム → アクティビティ で確認できます。ふたりの書き手が画面を 共有し始めたとき、その共有を安全にしているのがこのログです。

Navigation

入力して検索…

↑↓ navigate↵ selectEsc close