Webhook Widget

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-TypeBehaviour
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

json
{
  "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.

A widget with a tap target
json
{
  "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.