Webhook Widget

Protocol

Timelines and refreshes

iOS decides when a widget fetches new data. You cannot make it poll on demand, so use timelines for known future states and timers for anything based on the clock.

How refreshes work

refresh names the earliest moment you would like the next fetch — it is a request, not a schedule. iOS fetches some time after it, at a moment of its own choosing, from a refresh budget it manages per widget. You cannot see the budget or spend it faster; a widget that asks too often is simply granted a smaller share of what it asked for. Which is why the protocol is built around responses that stay correct between fetches.

Polling asks all day and is granted only a scattering of refreshes, while one response carrying four entries covers the same day exactly. Polling refresh: 900 asked all day · granted occasionally · wrong in between Timeline 4 entries Standup 09:00 Design review 11:30 1:1 14:00 Retro 16:30 one fetch, at 09:00 one fetch · correct at every moment
Both rows describe the same working day. The difference is whether the server answered the question once or was asked it repeatedly and ignored.

Entries

An entry is a view with a time attached. Send several and the device renders each one when its moment arrives, without contacting your server again.

json
{
  "v": 1,
  "refresh": 3600,
  "entries": [
    {"at": "2026-08-11T09:00:00Z", "view": {"type": "text", "text": "Platform 3"}},
    {"at": "2026-08-11T11:30:00Z", "view": {"type": "text", "text": "Platform 7"}},
    {"at": "2026-08-11T14:00:00Z", "view": {"type": "text", "text": "Platform 3"}}
  ]
}

Everything you know at request time can go into the timeline: a calendar you have already fetched, a train schedule, a shift rota, a countdown to a fixed date. Anything you cannot predict is what the next refresh is for.

Four entries, one response Large
json
{
  "v": 1,
  "refresh": 3600,
  "accessibility": "Four meetings today, next is standup at 9",
  "entries": [
    {
      "at": "2026-08-11T09:00:00Z",
      "view": {
        "type": "vstack",
        "align": "leading",
        "spacing": 0,
        "padding": 18,
        "bg": "#FFFFFF",
        "children": [
          {
            "type": "text",
            "text": "NOW",
            "style": "caption2",
            "weight": "bold",
            "color": "blue"
          },
          {"type": "spacer", "minLength": 6},
          {
            "type": "text",
            "text": "Standup",
            "style": "title",
            "weight": "bold",
            "color": "primary"
          },
          {
            "type": "text",
            "text": "09:00 – 09:15",
            "style": "headline",
            "color": "blue"
          },
          {
            "type": "text",
            "text": "Team sync",
            "style": "subheadline",
            "color": "secondary"
          },
          {"type": "spacer", "minLength": 14},
          {"type": "divider"},
          {"type": "spacer", "minLength": 10},
          {
            "type": "text",
            "text": "LATER",
            "style": "caption2",
            "weight": "bold",
            "color": "tertiary"
          },
          {"type": "spacer", "minLength": 6},
          {
            "type": "hstack",
            "spacing": 8,
            "align": "center",
            "children": [
              {
                "type": "vstack",
                "children": [],
                "w": 6,
                "h": 6,
                "corner": 3,
                "bg": "tertiary"
              },
              {
                "type": "text",
                "text": "Design review",
                "style": "body",
                "color": "secondary"
              }
            ]
          },
          {
            "type": "hstack",
            "spacing": 8,
            "align": "center",
            "children": [
              {
                "type": "vstack",
                "children": [],
                "w": 6,
                "h": 6,
                "corner": 3,
                "bg": "tertiary"
              },
              {
                "type": "text",
                "text": "1:1",
                "style": "body",
                "color": "secondary"
              }
            ]
          },
          {
            "type": "hstack",
            "spacing": 8,
            "align": "center",
            "children": [
              {
                "type": "vstack",
                "children": [],
                "w": 6,
                "h": 6,
                "corner": 3,
                "bg": "tertiary"
              },
              {
                "type": "text",
                "text": "Retro",
                "style": "body",
                "color": "secondary"
              }
            ]
          },
          {"type": "spacer"}
        ]
      }
    },
    {
      "at": "2026-08-11T11:30:00Z",
      "view": {
        "type": "vstack",
        "align": "leading",
        "spacing": 0,
        "padding": 18,
        "bg": "#FFFFFF",
        "children": [
          {
            "type": "text",
            "text": "NOW",
            "style": "caption2",
            "weight": "bold",
            "color": "purple"
          },
          {"type": "spacer", "minLength": 6},
          {
            "type": "text",
            "text": "Design review",
            "style": "title",
            "weight": "bold",
            "color": "primary"
          },
          {
            "type": "text",
            "text": "11:30 – 12:30",
            "style": "headline",
            "color": "purple"
          },
          {
            "type": "text",
            "text": "With Priya, Sam",
            "style": "subheadline",
            "color": "secondary"
          },
          {"type": "spacer", "minLength": 14},
          {"type": "divider"},
          {"type": "spacer", "minLength": 10},
          {
            "type": "text",
            "text": "LATER",
            "style": "caption2",
            "weight": "bold",
            "color": "tertiary"
          },
          {"type": "spacer", "minLength": 6},
          {
            "type": "hstack",
            "spacing": 8,
            "align": "center",
            "children": [
              {
                "type": "vstack",
                "children": [],
                "w": 6,
                "h": 6,
                "corner": 3,
                "bg": "tertiary"
              },
              {
                "type": "text",
                "text": "1:1",
                "style": "body",
                "color": "secondary"
              }
            ]
          },
          {
            "type": "hstack",
            "spacing": 8,
            "align": "center",
            "children": [
              {
                "type": "vstack",
                "children": [],
                "w": 6,
                "h": 6,
                "corner": 3,
                "bg": "tertiary"
              },
              {
                "type": "text",
                "text": "Retro",
                "style": "body",
                "color": "secondary"
              }
            ]
          },
          {"type": "spacer"}
        ]
      }
    },
    {
      "at": "2026-08-11T14:00:00Z",
      "view": {
        "type": "vstack",
        "align": "leading",
        "spacing": 0,
        "padding": 18,
        "bg": "#FFFFFF",
        "children": [
          {
            "type": "text",
            "text": "NOW",
            "style": "caption2",
            "weight": "bold",
            "color": "teal"
          },
          {"type": "spacer", "minLength": 6},
          {
            "type": "text",
            "text": "1:1",
            "style": "title",
            "weight": "bold",
            "color": "primary"
          },
          {
            "type": "text",
            "text": "14:00 – 14:30",
            "style": "headline",
            "color": "teal"
          },
          {
            "type": "text",
            "text": "With Alex",
            "style": "subheadline",
            "color": "secondary"
          },
          {"type": "spacer", "minLength": 14},
          {"type": "divider"},
          {"type": "spacer", "minLength": 10},
          {
            "type": "text",
            "text": "LATER",
            "style": "caption2",
            "weight": "bold",
            "color": "tertiary"
          },
          {"type": "spacer", "minLength": 6},
          {
            "type": "hstack",
            "spacing": 8,
            "align": "center",
            "children": [
              {
                "type": "vstack",
                "children": [],
                "w": 6,
                "h": 6,
                "corner": 3,
                "bg": "tertiary"
              },
              {
                "type": "text",
                "text": "Retro",
                "style": "body",
                "color": "secondary"
              }
            ]
          },
          {"type": "spacer"}
        ]
      }
    },
    {
      "at": "2026-08-11T16:30:00Z",
      "view": {
        "type": "vstack",
        "align": "leading",
        "spacing": 0,
        "padding": 18,
        "bg": "#FFFFFF",
        "children": [
          {
            "type": "text",
            "text": "NOW",
            "style": "caption2",
            "weight": "bold",
            "color": "orange"
          },
          {"type": "spacer", "minLength": 6},
          {
            "type": "text",
            "text": "Retro",
            "style": "title",
            "weight": "bold",
            "color": "primary"
          },
          {
            "type": "text",
            "text": "16:30 – 17:30",
            "style": "headline",
            "color": "orange"
          },
          {
            "type": "text",
            "text": "Sprint 42",
            "style": "subheadline",
            "color": "secondary"
          },
          {"type": "spacer", "minLength": 14},
          {"type": "divider"},
          {"type": "spacer", "minLength": 10},
          {
            "type": "text",
            "text": "LATER",
            "style": "caption2",
            "weight": "bold",
            "color": "tertiary"
          },
          {"type": "spacer", "minLength": 6},
          {"type": "spacer"}
        ]
      }
    }
  ]
}
The entry control steps through the timeline the way the clock will. Each of these is a full view of its own.

Entry rules

  • Entries are sorted by at, so you do not have to send them in order.
  • Entries closer together than five minutes are dropped, because WidgetKit ignores them. A pair 30 seconds apart becomes one entry, silently.
  • At most 60 entries are kept. The rest are discarded.
  • An entry with no at starts immediately. That is what plain view is: a single entry with no time.
  • An entry may carry its own accessibility, which replaces the envelope's while that entry is on screen. Worth doing whenever the entries say different things — one label for a whole day of them is a label that is wrong most of the day.

Use timers for countdowns

A timer node updates on the device every second without any timeline entry at all. Anything that is purely a function of the clock should be a timer, not a number your server recomputes.

Two ways to show six minutes
// Refreshing every minute to update a countdown: you will not get the refreshes,
// and the widget will be wrong between the ones you do get.
{ "v": 1, "refresh": 60, "view": { "type": "text", "text": "6 min" } }

// The device counts down on its own. One fetch covers the whole wait.
{ "v": 1, "refresh": 900, "view": { "type": "timer", "until": "2026-08-11T09:06:00Z" } }
Five ways to render one date

Choosing an interval

Kind of dataIntervalWhy
Anything derived from the clock 3600+ Use a timer node and let the device do the work.
A schedule you already know 3600–21600 Send it as entries; refresh only when the schedule itself might change.
Metrics, queues, build status 900–1800 Genuinely unpredictable, so this is what the budget is for.
Anything faster than five minutes — Clamped to 300, and you will not get it. A widget is not a live dashboard.

The budget is per widget instance, so a reader with the same URL in three sizes is making three separate sets of requests. Keep responses cheap to generate.