Webhook Widget

Protocol

Limits and errors

Widget extensions have strict resource limits. The app enforces all of the limits below; responses that exceed them are rejected.

LimitValueNotes
JSON body 256 KB Measured before decoding. A larger response is rejected without being parsed.
Image body 2 MB Applies to `image` nodes and to bitmap responses alike.
Image pixels 2000 px On the longest side.
Nodes 300 Counted across the whole tree, including every entry.
Depth 12 How far stacks may nest.
Timeline entries 60 Extra entries are dropped, not an error.
Entry spacing 5 minutes Entries closer together are dropped, because WidgetKit ignores them.
Refresh 300–86400 s Values outside the range are clamped, not rejected.
Request timeout 10 s
Redirects 3 HTTPS is re-checked at every hop.

Invalid payloads

Decode failures produce a specific message rather than a generic one. When there is no cached response to fall back on, the widget draws a full face with the message under a small “Couldn't load” heading. That is what you see below: the widget's failure face, with the wording coming live from the same engine as every other preview on this site.

an unknown node type is refused, lowercased in the message
json
{"v": 1, "view": {"type": "Marquee"}}
text without a text field is refused
json
{"v": 1, "view": {"type": "text"}}
a colour without the hash is refused
json
{"v": 1, "view": {"type": "text", "text": "x", "color": "3DDC84"}}
a non-http image scheme is refused
json
{"v": 1, "view": {"type": "image", "url": "file:///etc/passwd"}}

Decode errors

These cases come from web/conformance/decode-cases.json, a corpus executed by both test suites — the Swift decoder in the app and the Rust engine that draws the previews here. A message documented on this page is a message both implementations are tested against.

CaseMessage
an unknown node type is refused, lowercased in the message Unknown node type "marquee"
text without a text field is refused Missing required field "text"
a five-digit hex value is refused Invalid value for "color": #12345
non-hex characters are refused Invalid value for "color": #GGGGGG
a colour without the hash is refused Invalid value for "color": 3DDC84
font style values are case-sensitive generic decoding error
timer without a date is refused Missing required field "until"
timer style names the timer style, not a font style generic decoding error
a date with no timezone is refused generic decoding error
month 13 is refused generic decoding error
day 32 is refused generic decoding error
gauge without a value is refused Missing required field "value"
an inverted gauge range is refused Invalid value for "max": 2.0
a zero-width gauge range is refused Invalid value for "max": 5.0
a non-http image scheme is refused Invalid value for "url": file:///etc/passwd
a schemeless image url is refused Invalid value for "url": example.com/a.png
a payload with neither view nor entries is refused Response contained no "view" or "entries"
a payload without v is refused Missing required field "v"
a future protocol version is refused rather than misread Payload version 2 is newer than this app supports
a view nested one level too deep is refused View tree nested deeper than 12 levels
a view one node over the limit is refused View tree exceeds 300 nodes
the node budget spans every entry, not each one separately View tree exceeds 300 nodes
padding that is neither a number nor an object is refused generic decoding error
a padding edge that is not a number is refused generic decoding error

Where the table says generic decoding error, the outcome is still a refusal — the two runtimes simply word it differently, so the corpus asserts that the payload fails without asserting how it is described.

Transport failures

ConditionMessage
Not HTTPSyou.example is not HTTPS
Credentials over plain HTTP Refusing to send credentials over plain HTTP to you.example
Non-2xx statusServer returned HTTP 503
Body too large Response is 400000 bytes, over the 262144 byte limit
Wrong content type Cannot render Content-Type "text/html"
Image too large Image is 4000px on its longest side, over the 2000px limit
Undecodable imageResponse was not a decodable image
Redirect loopExceeded 3 redirects

Counting nodes

The 300-node budget is counted across the whole response, not per entry. A timeline of 12 entries at 30 nodes each is already at 360 and will be refused. If you are generating rows in a loop, cap the loop.

Depth is counted the same way, and 12 levels is more than any sensible layout needs — if you are close to it, something is wrapping that does not need to.