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

> Documentation Index
> Fetch the complete documentation index at: https://anysign.tv/llms.txt
> Use this file to discover all available pages before exploring further.

# 公開 API

ひとつの 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 がリクエストを判定する上限値です。ハードコードせず読んでください —
あなた側のリリースなしで引き上げられることがあります。

```jsonc
{
  "upload": { "maxBytes": 33554432 },
  "image": {
"maxBytes": 33554432,
"maxDimension": 4096,
"maxPixels": 8847360,
"unplayableMimes": ["image/webp"]
  },
  "video": { "maxBytes": 33554432 },
  "playlist": { "maxItems": 500 }
}
```

> **写真をアップロードする前に**
>
> `maxPixels` は 4096 × 2160 = 8,847,360 で、これが一番人を驚かせる制限です。
> スマホカメラそのままの写真はたいてい 4032 × 3024 = 12.2 MP — 両辺とも
> `maxDimension` の*範囲内*なのに拒否され、`reason: "image_too_many_pixels"`
> が返ります。バイト数の上限に当たることはめったにありません。

**サーバー側のリサイズはありません。** 保存されたバイトがそのまま重複排除の
ハッシュ対象なので、変換はコンテンツアドレッシングを壊してしまいます。
リサイズはあなたの仕事です — ブラウザなら 6 行：

```js
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`

```jsonc
{ "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`

```jsonc
{ "playlistId": "…" }                        // プレイリストをループ再生
{ "assetId": "…", "durationMs": 15000 }      // 単一の項目で画面を固定
```

ちょうどどちらか一方。`:layer` は上の 5 つのいずれかです。プレイリストは
存在している必要があり、アセットはアップロード完了済みであること — まだ
存在しないバイトを指された画面はファイル欠落と数え、永遠に準備中のままに
なります。

### `DELETE /screens/:screenId/layers/:layer`

レイヤーをひとつ外します。下にあるものが現れます。もともと空のレイヤーを
クリアしても成功です — 消したかったものは消えています。

### `DELETE /screens/:screenId/layers`

スタック全体を空にします — 「この画面を真っさらに」の直接的な手段です。

### `POST /screens/:screenId/commands`

```jsonc
{ "type": "refetch" }      // メンバー：いますぐ再同期
{ "type": "screenshot" }   // メンバー：パネルの表示をキャプチャ
{ "type": "reboot" }       // 管理者
```

コマンドは画面の次回確認に相乗りして届き、実行結果も報告されます。
`refetch` はデバイスが誤ったバイトを抱えた場合の修復ツールであって、
近道ではありません — 通常の公開は画面自身の確認で届きます。

## コンテンツのアップロード

### `POST /content?name=<filename>[&durationMs=<ms>]`

ボディが**そのまま**ファイルです。型は `Content-Type` ヘッダーで指定します。

```sh
curl -X POST "$BASE/api/v1/content?name=menu.png" \
  -H "authorization: Bearer $TOKEN" \
  -H "content-type: image/png" \
  --data-binary @menu.png
```

```jsonc
{
  "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`

```jsonc
{ "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 件を返すので、こちらの状態のコピーを持たずに、最後に書き込んだ
内容と突き合わせできます。

```jsonc
{
  "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`

```jsonc
{ "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` で読むか、コンソールの
**チーム → アクティビティ** で確認できます。ふたりの書き手が画面を
共有し始めたとき、その共有を安全にしているのがこのログです。

Source: https://anysign.tv/ja/docs/api/index.mdx
