Webhook Widget

Protocol

The request

The widget sends a GET request with details about the slot it needs to fill. Use that information to return a layout that fits the device and widget size.

HTTP
GET /widget?key=s3cret
      &family=systemMedium
      &w=338&h=158
      &scale=3
      &scheme=dark
      &locale=en_GB
      &tz=Europe/London
      &v=1
Host: you.example
Accept: */*

Your own query parameters are preserved, and yours win on a name collision — so ?key=… for a token is safe even though the widget also sends parameters of its own.

What you receive

Field Type Description
family string systemSmall, systemMedium or systemLarge on the Home Screen; accessoryCircular, accessoryRectangular or accessoryInline on the Lock Screen.
w number Slot width in points, for this exact device.
h number Slot height in points.
scale number Screen scale, 2 or 3. Multiply by w and h for pixels.
scheme string light or dark. A hint — see the caveat on the request page.
locale string The reader's locale, such as en_GB.
tz string IANA time zone, such as Europe/London.
v number Protocol version the app speaks.

Build for the slot

The three families are genuinely different canvases, not one layout at three scales. A small is about 158 points square; a medium is the same height and twice the width; a large is roughly square again at over twice the height. Branch on family and send the layout that suits it.

Node
app.get("/widget", (request, response) => {
  const family = request.query.family;

  // Answer with a layout that suits the slot rather than one that fits every slot.
  const view = family === "systemSmall"
    ? compactView()
    : detailedView();

  response.json({ v: 1, view });
});

w and h are the exact point size of the slot on that device, which varies by screen. Multiply by scale when you are rendering a bitmap and need pixels — see bitmap mode.

The widget is also offered in the three Lock Screen sizes, so family can arrive as accessoryCircular, accessoryRectangular or accessoryInline. They are far smaller than any Home Screen slot and iOS always draws them in a single-hue vibrant mode, which is the same code path as a tinted Home Screen: opaque backgrounds are dropped and accentable is what keeps a node in the bright group. Answer them with a couple of nodes rather than with the layout you built for a medium. The previews on this site render the three Home Screen families only; a Lock Screen slot has to be looked at on a device.

One payload, three slots
This payload was built for the small. The bigger families stretch it rather than reflow it — nothing breaks, but the extra room goes unused, which is why you branch.

Do not rely on scheme alone

scheme is the appearance at the moment of the fetch. WidgetKit renders one timeline into both appearances, so a response fetched in light mode may still be on screen after the phone switches to dark, without your server being asked again.

Semantic colour names are immune to this: they resolve on the device at the moment of drawing, so "color": "primary" is correct in both appearances. Use scheme to choose a nicer palette, and semantic names to stay readable. A bitmap cannot adapt at all, which is the main reason to prefer nodes over pixels.

Tinted Home Screen

When the reader sets the Home Screen to a tinted style, iOS discards most of the colour in a widget and redraws it in a single hue. Nothing in the request describes this, and nothing could: the mode is chosen each time the widget is drawn, one response is drawn in every mode it needs, and restyling the Home Screen redraws what is already on the phone rather than asking your server again.

It is handled on the device instead, where the answer exists. Two things follow for a payload. An opaque background is dropped in these modes — flattened, it would come out the same hue as the text sitting on it, and the widget would go blank rather than merely lose its colour. And accentable: true on a node keeps that node in the bright group rather than the dim one, which is how a number stays legible once the colour is gone.

Inspect a real request

You cannot attach a debugger to a widget extension, which makes “what did my phone ask for?” a surprisingly awkward question. This site answers it: point a widget at the endpoint below and it renders the request that produced it.

URL
https://webhookwidget.com/api/echo
What /api/echo returns
The values above are from a sample request. On a real device they are that device's own — which is how you find out the exact slot size to render a bitmap at. The endpoint branches on family like any other server should: the medium splits these across two columns, the small answers with only the four rows that fit.

Caching

Responses are cached on device and reused when a fetch fails, so a server that is briefly down shows the last good widget with a small staleness dot rather than an error. Cache keys are built from the URL with any credentials removed.

For bitmap responses, Cache-Control: max-age is the only way to ask for a refresh interval, because there is no envelope to carry refresh.