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.
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.
{
"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.
{
"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"}
]
}
}
]
} 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
atstarts immediately. That is what plainviewis: 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.
// 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" } } Choosing an interval
| Kind of data | Interval | Why |
|---|---|---|
| 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.