# Agent Display API Publish an HTML app, its JSON state, photos, and spotlights to a screen your owner keeps open. Open screens update automatically. Use plain HTTPS, such as curl. - API base URL: `https://v41nm1wsna.execute-api.us-west-2.amazonaws.com/v1` - Agent OpenAPI (every operation, limit, and error): [https://console.usetd.com/contracts/agent-openapi.yaml](https://console.usetd.com/contracts/agent-openapi.yaml) - Runtime contract (sandbox, layout checks): [https://console.usetd.com/contracts/display-runtime.md](https://console.usetd.com/contracts/display-runtime.md) - Starter app to copy: [https://console.usetd.com/contracts/examples/starter.html](https://console.usetd.com/contracts/examples/starter.html) - Sticky notes, a board that turns its own pages: [https://console.usetd.com/contracts/examples/sticky-notes.html](https://console.usetd.com/contracts/examples/sticky-notes.html) - A fuller reference app: [https://console.usetd.com/contracts/examples/food-dashboard.html](https://console.usetd.com/contracts/examples/food-dashboard.html) ## Connect Send `Authorization: Bearer `, read each time from a private, mode-600 file such as `~/.config/agent-display/key`; never put it in apps, state, URLs, logs, commits, or messages. `401`: ask the owner for a new key. `403`: no access to that display. Call `GET /me` first: it returns your agent ID, `ownerTimeZone`, and your displays with their `url`. Never ask the owner for display IDs. ## Suggest what to show If the owner already asked for a display, build that. Otherwise, once connected, offer two or three ideas from what you already know of the owner's conversation and data: say what each would show and when you could update it, recommend one, and let the owner choose. Consider who can see a shared screen. Invent no personal facts, account access, or schedule: use your own tools and scheduler only if you have them and the owner wants it; otherwise say you can update the display when you next work. Connecting does not start or schedule you. ## The loop 1. `GET /me`; reuse a display that already fits the request. 2. When the app is ready, `POST /displays` with `{"name":"Kitchen"}`; it defaults to the owner's time zone, and on first run the owner's console opens it. 3. `PUT /displays/{displayId}/state` with `If-Match: 0` and `{"state":{...}}`, so the first render has data. 4. `PUT /displays/{displayId}/app`: a complete HTML document as `Content-Type: text/html`, with a brief `X-Revision-Note` for History. Copy the starter unless the request needs more. 5. Later, `GET` state, change it, and `PUT` it whole with `If-Match` at that version (`409 stale_version`: reread and retry). Republish the app only for presentation changes, before the state that needs it. 6. `PUT /displays/{displayId}/spotlight` with `{"data":{"title":"...","text":"..."},"until":""}` for time-bound content in the owner's words, not progress already shown; `until` is when it stops mattering (the pickup, the departure) unless the owner says. `DELETE` clears it early; `"from"` (up to 30 days ahead) schedules it, so a reminder is one call. Tell the owner it appears on screen and that nothing else reaches them unless you also send a message. ## Photos `PUT /displays/{displayId}/images/{name}` with the original file (at most 4 MB) as `image/jpeg`, `image/png`, `image/webp`, `image/gif`, or `image/avif` stores it under a name such as `box-8`; reusing a name replaces it. Over 4 MB (up to 20 MB): `POST /displays/{displayId}/images/{name}/upload` with `{"contentType":"image/jpeg"}`, `PUT` the file to the returned `url` with that Content-Type (`curl -T photo.jpg -H "Content-Type: image/jpeg" "$URL"`), then `PUT` the photo's route with `{"uploadId":"..."}`. Do not resize: screens orient and scale photos. Keep names in state, render `snapshot.images.` (a missing name means "no photo yet"), and `DELETE` unused names. Never base64 photos into state or apps. ## Writing the app Render from `display.onState((snapshot) => ...)`. `snapshot.state` holds your data (`null` before its first write), beside `images`, `spotlight`, `theme` (`light` or `dark`), `viewport`, and the screen's clock, `now` (an ISO time), refreshed each minute. Inline scripts and styles, photos or SVG, no network; the API explains what it rejects. An app that throws before it renders is rejected; the previous one stays. ## Patterns - **State for the whole job:** before the first publish, give every item, stage, and view the fields the owner will ask about (arriving: who brings it, ETA, replacements; an item's `where`, such as a box's shelf or where a parcel was left; a moved date's `was`) and the keys that answer requests (`"view": {"filter": "week", "sort": "due"}`, `"focus": "box-8"`, `chosen`). Alike things share one shape and one view (an idle dryer carries the washer's fields and countdown). Draw every field and stage you store, empty as well as full (a `was` with no current value, a stage before its number is known), so "show only this week", "delivered", and "show me box 8" are each one state write. - **A change carries through:** a new stage sets what it implies (out for delivery: due today), a number you worked out moves with its inputs (the leave-by time with the departure) and keeps its `was`, small beside it; a source's number that rested on what changed (boarding after a delay) is only a `was` until the source gives a new one; and what ended clears (a finished stage's fields, a focus the owner moved on from). - **Glance:** big numbers, short labels, text at least 12 px, no text inputs or taps: the owner asks you, and a TV has nothing to tap. Follow `snapshot.theme`. - **Fit the frame:** everything requested fits without clicks or page scrolling (start at 1280×720) and without sideways scrolling down to 360 px, with room for the fullest state you will store (every `was`, a full list); key things first on phones. Shrink or drop the least important content rather than squeeze everything. Size boxes to short content and let type grow; never stretch boxes to fill space. No fixed heights: the frame can shrink anytime, by 50–100 px for a spotlight band. Words wrap whole: badges under the text and smaller big type on phones; never `overflow-wrap: anywhere` on titles. - **Lists:** cap them ("3 more"); a TV cannot scroll, so only phones get a list's own scroll area. Let columns adapt to item count. What is done or ruled out steps back (struck through or faded, last, with its reason) until the owner says to remove it. - **Time:** copy the display's `timeZone` into state. Store ISO dates and times (from a screenshot, ISO datetimes on its date; a source's "22 min left" at 8:58 is an end time of 9:20) and word them in that zone from `snapshot.now`: "Due Fri", "Overdue", today, this week, a countdown before a deadline, "late" after it. Weeks run Monday–Sunday; count workdays for people's work and calendar days for waits, unless the owner says otherwise; offer to switch. Data with its own timestamp gets one "as of 6:12" note, none on a finished stage. - **Stages and status:** `status` in the service's words (placed, arriving, delivered). For one thing, the stage picks the big number (at home the leave-by time, on the way and at the airport the gate, in the air the arrival) and the small tiles hold the rest: draw every stage's view in the first app, including the owner's own moves, so "leaving now" is one state write. A status board is one tile per thing: what needs the owner comes first and biggest (an open door, a package at the door), what is fine stays small; color says state; show when each last changed. - **Show me X:** bring the `focus` item forward, big enough to read across the room, the rest small below, with the field that changes most on the small tiles too. - **Comparisons:** rows in the owner's priorities, each value marked better or worse for them; on phones, all options' key facts first, then the rows. Asked for your pick, show a "Recommended" badge with one reason and keep `chosen` for the owner's decision. - **Schedules:** one line per clash: who, when, why. A clash or gap clears when its cause moves or the owner says all of it is covered; partial cover stays a question ("Grandma till 7?"). Headers count what is open ("3 pickups unassigned"), never "No clashes" while any is. - **Diagrams:** inline SVG with a scaling `viewBox`; box text at least 14 px, using empty space before shrinking; labels beside lines or on a background. Draw each date's `was` beside it (earlier and later), so "what does that do" is a state write. - **Only what is true, in the owner's words:** the owner's content, never ad copy, notes about what you did, source labels, or disclaimers, and nothing the source has not said (no guessed "On time" or estimated boarding). A convention the numbers depend on is one legend line; reasoning, caveats, disagreement, and anything pending or failed go in the conversation. From a screenshot, read what matters into state and show it only cropped, if it adds something; never addresses, card numbers, codes, or other people's identifiers (plates, phone numbers, faces). ## Checking your work Open screens measure your app; you need not see them. A few seconds after a write, `GET /displays/{displayId}` and read `app.viewerResults`, one per screen kind (narrow/wide) with the `stateVersion` it measured: `applied` means it rendered, so fix every `warnings` entry; `rejected` comes with `error.message`; empty means no screen is open, so carry on. Never ask the owner to open or reload a screen. Tell the owner what the screens checked, not that it looks right. ## Limits State 256 KB of JSON; app 2 MB; spotlight data 16 KB, one at a time; 40 photos per display; 25 displays per account. `429` and `5xx` are retryable with backoff. ## Boundaries Agent Display neither runs nor wakes you, and cannot notify anyone: for reminder messages use your own scheduler, if you have one, and tell the owner which delivery to expect. It is not a record: when asked to log, update the owner's records and say where. Everything published appears on screen; keep secrets out of apps and state.