# Webhook Widget Webhook Widget is an iPhone app whose Home Screen widget fetches a URL you control and draws whatever it returns. You write a server that answers one GET with JSON; the widget lays it out natively — real text, live timers, Dynamic Type, light and dark. Full docs: https://webhookwidget.com/docs/quickstart/ This file is for agents writing a widget feed on someone's behalf. It states the contract and the practices that make a feed good, so the human only has to look at the result. ## The contract Answer `GET` with `Content-Type: application/json`: { "v": 1, "refresh": 900, "accessibility": "Build 4128 passing, finished 3 minutes ago", "view": { "type": "vstack", "children": [ ... ] } } - `v: 1` is required. Requests carry the widget's context as query parameters (`family`, `w`, `h`, `scale`, `scheme`, `locale`, `tz`, `v`) — tailor the response if useful, or ignore them. Nothing describes tinted rendering: it is chosen on the device long after the fetch, and `accentable` is the hook for it. - `family` is `systemSmall`, `systemMedium` or `systemLarge` on the Home Screen, and `accessoryCircular`, `accessoryRectangular` or `accessoryInline` on the Lock Screen. Branch on it; `w` and `h` are the exact point size of that slot on that device. - `refresh` is seconds until the next fetch, clamped to 300–86400 (default 900). iOS grants refreshes on its own schedule; treat yours as a request, not a guarantee. - Responses are capped at 256 KB, trees at 300 nodes and 12 levels deep, timelines at 60 entries. Reference: https://webhookwidget.com/docs/limits/ ## Nodes `vstack` / `hstack` / `zstack` (children, spacing, align), `text`, `timer`, `image` (https URL), `gauge` (value, min, max, bar or ring), `spacer` (minLength), `divider`. Shared modifiers on any node: `padding`, `bg`, `corner`, `opacity`, `w`, `h`, `accentable`. `padding` takes a number (all four edges) or an object naming only the edges it pads — `{"top": 4, "leading": 8}` — with `leading`/`trailing`, not left/right: the two swap sides on a right-to-left device. Field-by-field reference: https://webhookwidget.com/docs/schema/ ## Practices that make a feed good - Prefer semantic text styles (`"style": "headline"`) over fixed `size`: they follow the reader's Dynamic Type setting. Small fixed sizes are the most common accessibility mistake in a widget. - Prefer semantic colours (`green`, `primary`, `secondary`) over hex: they adapt to light, dark and tinted rendering by themselves. A payload that names no hex and sets no root background keeps the system's own container material. - Always set `accessibility`: it is what VoiceOver reads. Without it the widget reads the tree's text nodes in order. - Set `lines` on any text you did not write yourself. Text past the cap shrinks (to 60% at most), then truncates; `"shrink": false` truncates at full size instead. - Use `"monoDigits": true` on columns of times or counts — tabular figures align digits without switching typeface. Timers already render this way. - Use `timer` nodes for anything counting: they tick on the device between refreshes, so a countdown never needs a fast `refresh`. Styles: `timer`, `relative`, `offset`, `date`, `time`. https://webhookwidget.com/docs/timelines/ - One fetch can carry the whole day: `entries: [{ "at": "...", "view": ... }, ...]` renders each view when its time arrives, with no further requests. Prefer this over a small `refresh` whenever the future is known at fetch time. An entry may carry its own `accessibility`, which overrides the envelope's for that entry. - To spread items across a row, put `spacer` nodes between them. Content is measured first: spacers absorb the leftover width, splitting it evenly, and collapse to nothing when the row is tight (set `minLength` on one to keep a floor). - There is no list or grid node. Your server already has a loop: emit the rows. - Auth: Basic, Bearer or a custom header, configured on the widget — or credentials in the URL. https://webhookwidget.com/docs/auth/ ## Seeing what you built One page serves both of you, and which half you get is a query parameter: /playground/?headless=1&... the render target — for you /playground/ the editor — for the person you are building this for Use `headless=1`. It strips the page to the widget alone, on a wallpaper, the way it sits on a Home Screen — no editor, no page chrome, and nothing on screen but the render: https://webhookwidget.com/playground/?headless=1&family=small&payload= The widget is drawn by the same engine the app runs, so it matches what an iPhone draws. Drop `headless` from that same URL and it opens the payload in the editor instead, with the JSON in a text area and size, appearance and device controls beside it. That is the link to hand a person who wants to see what you built or change it — keep every other parameter. Wait for `document.title` to become `preview-ready` (or `preview-error`) rather than for a delay. Until it does, the page is bare wallpaper — a capture taken too early comes out empty rather than half-drawn, which is worth checking for before trusting a screenshot. The frame is the widget plus a 28pt margin, and on the default device it is a fixed size. Set your viewport, or your crop, before you navigate: small 214 x 214 medium 394 x 214 large 394 x 410 For another screen size pass `device=` and read the frame from `document.documentElement.dataset.frame`, which is `"WIDTHxHEIGHT"`, instead. If you can set the viewport, set it to the frame: the widget fills it exactly and the screenshot needs no cropping. If you can only crop, add `align=start` — the widget is pinned to the top-left corner, so the crop is `(0, 0, WIDTH, HEIGHT)` whatever the window size. Without it the widget is centred, which is the better picture at an arbitrary size but puts the origin somewhere you have to measure for. Pass `freeze=1` for any capture you will compare against another. Without it a `timer` node reads a second later on the second capture and every pixel of the diff is noise. Options: `family` (small · medium · large), `scheme` (light · dark), `entry=N` to step a timeline, `device` for another screen size, `align=start` to pin the widget top-left, `padding` and `wallpaper` to change the framing, `src=` to fetch the JSON instead of passing it. ## Handing it to the person A feed is only half of it: somebody still has to get the URL and the credential into the app, on a phone, without mistyping either. Build them a setup link and let their camera do it: webhookwidget://register?name=&url=&auth= &username=&header=
&secret= Build the link and draw the QR code yourself, on the machine you are already running on. Any QR library will do: the payload is the link, as plain text. There is no page of ours that takes these fields and no endpoint to send them to — deliberately, because a page that accepted a credential could keep it, and this one is not ours to hold. What the link has to look like: - `name` and `url` are required, and `url` must be `https`; the app refuses anything else. Percent-encode every value — an `&` or a `#` in a token is otherwise the end of it. - Carry only the fields in use. An uncredentialed hook is `name` and `url` alone: omit `auth` rather than sending `auth=none`. A QR code's density follows its payload length, and these get scanned across a room off a phone screen. - `auth=bearer` takes `secret`, the token. `auth=basic` takes `username` and `secret`, the password. `auth=header` takes `header`, the field name, and `secret`, its value. - A credential already in the URL — `?key=…`, or `https://user:pass@host/feed`, which the app moves into an `Authorization` header before the request leaves the device — needs no `auth` at all, and makes the shortest code. The link opens the hook editor pre-filled and stores nothing until the person taps Save, so a field you got wrong is one they can correct rather than a dead end. The link carries the credential, which is the point of it. Treat it as you would the credential: do not paste one into a transcript, an issue, or a commit, and do not post it to anything — including us. A hook added this way is the person's *own*, exactly as if they had typed it in. There is a second kind of code — a Handover — that marks a hook as a gift from someone else, and those run on the recipient's Home Screen for free, for good. Only the app can make one, because they are signed. Do not try to construct one: an unsigned imitation is refused, and the refusal is the feature. Worked examples with commentary: https://webhookwidget.com/examples/ — each is also served live at https://webhookwidget.com/demo/ for a real widget to point at. Start with https://webhookwidget.com/demo/hello; https://webhookwidget.com/demo lists them all.