Protocol
The response
A response can contain a JSON view or an image. The Content-Type header tells
the widget which one it received.
| Content-Type | Behaviour |
|---|---|
application/json, */*+json | Parsed as the envelope below. |
image/* | Drawn edge to edge, filling the widget. See bitmap mode. |
| Anything else |
The body is sniffed for a leading { or an image header, then
rejected if it is neither.
|
The envelope
{
"v": 1,
"refresh": 900,
"accessibility": "Bus 42 arriving in 6 minutes",
"url": "https://you.example/open",
"view": { "type": "vstack", "children": [] }
} | Field | Type | Default | Description |
|---|---|---|---|
v | number | required | Protocol version, currently 1. A payload that names none — or a version above what the app knows — is refused rather than misread. |
refresh | number | 900 | Seconds until the next fetch. Clamped to 300–86400. |
view | node | — | The view tree. Shorthand for a single-entry entries. |
entries | entry[] | — | Future states, each { "at": ISO-8601, "view": node }, optionally with its own accessibility. Use this or view, not both. |
accessibility | string | — | VoiceOver label for every entry. Without it the widget reads its text nodes aloud instead. An entry that names its own overrides this while it is on screen. |
url | string | — | Opened when the widget is tapped. A web address, or another app's scheme — a rota widget can open the rota app. The app's own webhookwidget: scheme is refused, as are javascript:, data: and file:. |
A response needs either view or entries. view is
shorthand for a single entry that starts now; anything you can express with it you can
express with entries, so start with view and reach for
entries when one state is not enough.
VoiceOver text: accessibility
Without this field, VoiceOver reads the widget's text nodes in order, which for the departures example produces “Camden Road Southbound 42 Kings Cross 6:00”. With it, the widget says what it means.
Write it as a sentence a person would say. It is the one part of the payload that carries no layout at all, so it is easy to forget and cheap to get right. The note under every preview on this site shows what VoiceOver would read.
Tapping the widget: url
The widget is a single tap target. Tapping it opens url — a website, or a
scheme your own app registers, which is the usual way to jump straight to the relevant
screen.
{
"v": 1,
"url": "https://webhookwidget.com/docs/response/",
"accessibility": "Open the response documentation",
"view": {
"type": "vstack",
"align": "leading",
"spacing": 4,
"padding": 16,
"bg": "#151A21",
"children": [
{
"type": "text",
"text": "TAP TARGET",
"style": "caption2",
"weight": "bold",
"color": "#6D7786"
},
{
"type": "text",
"text": "One link",
"style": "title2",
"weight": "bold",
"color": "white"
},
{
"type": "text",
"text": "The whole widget opens the url in the envelope.",
"style": "footnote",
"color": "#9AA4B2",
"lines": 3
},
{"type": "spacer"},
{
"type": "text",
"text": "webhookwidget.com",
"style": "caption2",
"color": "cyan"
}
]
}
} Versioning
v declares which version of the protocol the payload speaks, and every payload
must carry it — today that means "v": 1. The app refuses a version above what
it knows, or a payload that names none, rather than guessing and showing something subtly
wrong.
If a fetch fails
A failed request does not blank the widget. The last successful response is shown again with a small orange staleness dot, so a reader sees old data marked as old rather than an error where their information used to be.
That applies to network failures, non-2xx statuses, oversized bodies and payloads that do not decode. The exact wording of a decode failure is on limits and errors, and every one of those messages can be reproduced live on that page.