Protocol V1

API Reference

Everything you need to design an endpoint that Glance Widgets can render: what requests the widget sends, what response format your endpoint should return, and the rules the renderer follows. Every live example on this page is rendered by the same rules the app uses.

Overview#

A Glance Widgets widget is driven by one HTTPS GET request to an endpoint you own. Your endpoint returns one JSON object — a frame — that describes what to draw. The app validates and normalizes the frame, and WidgetKit renders it on the Home Screen.

A frame has two expressive channels that are composited together:

  • The root pixel grid (pixels) — a rectangular dot matrix of up to 4,096 cells laid out within the full widget canvas. Style every dot, or leave most of it to default and batch.
  • Layers — content elements with position and size: text, curves, images, icons, lines, shapes and local dot matrices.
The container (Liquid Glass, glass, solid, gradient, ambient or image) is configured in the app, not by your API. Your response only owns the content drawn on top of it. The one exception is the Image material, which can read an image from data so your server renders the whole widget — see Server-rendered images.

Quick start#

The smallest useful response is a grid. Elements in the pixel style array inherit default when their value is null; explicit objects override it.

Example
{
  "protocol": 1,
  "default": { "color": "#8EC8FB", "opacity": 0.12, "cornerRadius": 0.5 },
  "pixels": [
    [null, null, null, null, null],
    [
      null,
      { "color": "#7EDDB5", "opacity": 1 },
      null,
      { "color": "#7EDDB5", "opacity": 1 },
      null
    ],
    [null, null, null, null, null],
    [
      { "color": "#DFFF70", "opacity": 1 },
      null,
      null,
      null,
      { "color": "#DFFF70", "opacity": 1 }
    ],
    [
      null,
      { "color": "#DFFF70", "opacity": 1 },
      { "color": "#DFFF70", "opacity": 1 },
      { "color": "#DFFF70", "opacity": 1 },
      null
    ]
  ]
}
A 5 × 5 grid on a small widget
  • Deploy the endpoint anywhere that serves HTTPS.
  • In the app, create a widget and paste the URL in the API field.
  • Preview it at real Home Screen size, then add the widget to your Home Screen.

Widget content structure#

Card material and content

A widget combines the card’s visual style and material with the content inside it. Material is configured in the app’s custom widget editor, or potentially an editor on another platform in the future. The API cannot select or dynamically change the card material; it supplies content and content styling.

Layers on the card come from two places. Editor layers are added, positioned and configured in the editor’s visual workspace and saved with the widget. An editor layer can stay fully static, or it can have a data path so the API updates its position, content and style on every response. The editor’s settings are then the initial state before the first response and the fallback for any field a response leaves out.

Remote layers need no configuration in the editor: the API declares them in each response. They can be content the saved widget never contained, defined entirely by the API. You can use both kinds together; the Response body and Layers sections show how each is written.

Basic layers and pixel grids

In Glance Widgets, you can express basic content by adding layers such as text, straight lines, curves, images, icons and shapes. Layers can overlap, with their stacking order controlled by the dedicated zIndex field.

A pixel grid extends these possibilities while keeping the description and parsing simple. Your server can generate its own visual expressions as a two-dimensional array. You can also express the basic content types as pixel art when that suits your needs or preferences; the cell budget determines the available detail.

A pixel grid has two forms. A matrix layer (type: "matrix") occupies a positioned, sized region of the canvas like any other layer; its content is a grid. The root grid (top-level pixels) is outside the layer system and uses the whole content canvas as its layout area. Both share the same cell styling, layout rules and API structure. The root form makes a single, consistently styled pixel theme easier to express without creating a layer. Its layout fits and centers the grid; whether dots visually fill the card depends on the grid aspect ratio, gaps and edge inset.

The grid’s logical positions continue to exist even when some dots are hidden. You can style each dot or leave a position blank, like the unlit cells on an old red-on-black pixel display. Blank regions can form a pattern or leave room for other content layers. The “Pixels & precedence” section explains the explicit {"empty": true} value.

Three composited levels

  • Bottom: card style and material, configured in the editor as the background and container.
  • Middle: root grid, laid out across the content canvas, with individual or all cells optionally blank.
  • Top: content layers, combining editor layers and remote layers declared by the API in zIndex order. Matrix layers belong here too.

The request#

The widget sends an HTTPS GET. Your own query parameters are preserved, and the app appends reserved gw_* parameters describing the space it is drawing into. Non-HTTPS URLs are rejected.

gw_protocol
Example1
DescriptionProtocol version the client speaks.
gw_platform
Exampleios
DescriptionClient platform.
gw_family
Examplesmall · medium · large
DescriptionWidget size family.
gw_canvas_width_pt
Example158.00
DescriptionAvailable content canvas width in iOS logical pixels (pt), to two decimal places.
gw_canvas_height_pt
Example158.00
DescriptionAvailable content canvas height in iOS logical pixels (pt), to two decimal places.
gw_aspect_ratio
Example1.0000
DescriptionWidth ÷ height (four decimals).
gw_context
Exampleapp_preview
DescriptionRendering context. app_home: app home; app_preview: editor preview; widget: Home Screen widget.
gw_state_<key>
Example—
DescriptionReserved for future interaction-state extensions; not used by the current request flow. All parameter names starting with gw_ are reserved for the client and must not be used for custom query parameters.

pt is the iOS logical layout unit, corresponding to Flutter logical pixels in the app. It is neither a physical screen pixel count nor a grid cell count. At a 3× display scale, for example, 158 pt spans 474 physical pixels. Both parameters exclude the card’s content inset: width = outer widget width − 2 × contentInset, and likewise for height, with a minimum of 1 pt.

Canvas sizes vary by device. On 390–393 pt wide iPhones, the sizes are roughly 158 × 158 (small), 338 × 158 (medium) and 338 × 354 (large).

Example request
GET https://api.example.com/widget?team=core&gw_protocol=1&gw_platform=ios&gw_family=medium&gw_canvas_width_pt=338.00&gw_canvas_height_pt=158.00&gw_aspect_ratio=2.1392&gw_context=app_preview

Timeouts are short: about 8 s to send, and 12 s to receive in the app. Any 2xx status is a success. Respond with UTF-8 JSON, ideally with Content-Type: application/json.

Response body#

Return one UTF-8 JSON object, at most 512 KB. protocol is optional; explicitly return 1, the only supported non-null version. The object describes the content for this request. Read it in three groups: the root grid, the data root and the remote layer declarations in layers. Unknown top-level fields are ignored.

Root grid: pixels, default, layout and batch

pixels determines the grid’s m*n dimensions and describes the cell styles; layout arranges the cells. Building complex arrays cell by cell is tedious even with server-side code, so batch applies one style to many dots and default covers cells without an individual declaration.

pixels
Type / values(Pixel | null)[][]
Purpose and rulesRequired root grid content. Each entry is the style of one logical cell at pixels[row][column], starting top-left. A rectangle of at least 1 × 1 and at most 4,096 cells.
default
Type / valuesPixel
Purpose and rulesDefault cell style; also supplies omitted properties in explicit pixel objects and each batch style.
layout
Type / valuesMatrixLayout
Purpose and rulesArranges the whole grid: cell spacing, aspect ratio and edge inset. It does not define individual dot colors or shapes.
batch
Type / values{ pixel, coordinates }[]
Purpose and rulesApply one pixel style to a set of zero-based [row, column] coordinates. Useful for server-generated patterns without repeating a style for every dot.

Precedence is default → batch entries in order → explicit pixels objects. null keeps the default/batch result. Omitted properties in an explicit object fall back to default, rather than merging properties from a batch. Omitting a cell’s style means putting null in its slot; do not remove array entries or shorten rows. See “Pixels & precedence” for examples.

The data root: data

The optional data object defaults to an empty object and holds what your layers read. You choose the field names and nesting, for example grouping the layers of one panel under data.orders. The Image material’s data path also reads its image from here (see Server-rendered images).

data itself is just a freely organized object, and its values are not drawn by themselves. Dynamic content created in the editor, or remote layers your API returns, read a field of data that you specify through dataPath and are parsed and drawn according to the rules. If a field drives an editor layer or a remote layer, it must be an object that follows the protocol for that layer type. In general, the field a dataPath points to contains the layer’s position, content and style; see layers below and the Layers section.

Remote layers: layers

The optional layers array declares layers your API adds; omit it for none. Each entry has exactly three fields: id, unique in the response and starting with remote:; type, one of the seven layer types; and dataPath, pointing to the object in data that describes the layer.

A layer’s position, content and style do not go in the layers entry. They go in the object dataPath points to: frame, zIndex, rotation and transform set size and layout, content is the content and style the style. A remote layer is built entirely from that object, so it must have a frame.

Declaring one remote layer
{
  "protocol": 1,
  "pixels": [[{ "empty": true }]],
  "data": {
    "orders": {
      "badge": {
        "frame": { "right": 0.07, "top": 0.14, "width": 0.24, "height": 0.2 },
        "content": { "text": "LIVE" },
        "style": { "color": "#0B1222", "backgroundColor": "#7EDDB5", "opacity": 1, "cornerRadiusRatio": 1 }
      }
    }
  },
  "layers": [
    { "id": "remote:badge", "type": "text", "dataPath": "data.orders.badge" }
  ]
}

Each response’s layers is the complete remote layer set for that frame and is replaced as a whole by the next successful response; it is not a list of incremental edits. Layers added in the editor are not declared here; they are bound by the data path set in the editor, as described in the Layers section.

The root grid’s pixels remains required for a layer-only frame. Return [[{"empty": true}]] to leave the root grid blank and draw layers over the card material.

Complete response body type reference#

The nested object and related types below describe the response. ? marks an optional field, [] an array and | alternatives. This is a type reference, not a JSON response to copy. A layer’s type selects its corresponding content and style; fields from unrelated types are not interchangeable. The following sections explain purposes, units and conditions.

V1 response types
type WidgetResponse = {
  protocol?: 1
  pixels: (Pixel | null)[][]
  default?: Pixel
  layout?: MatrixLayout
  batch?: Batch[]
  // Objects that layers point to live anywhere in data; layers only declare remote layers.
  data?: { [key: string]: JsonValue | LayerNode }
  layers?: {
    id: `remote:${string}`
    type: 'text' | 'matrix' | 'curve' | 'image' | 'icon' | 'line' | 'shape'
    dataPath: string // points to the object in data that describes this layer
  }[]
}

// The object a dataPath points to. type selects content and style:
// text → TextContent / TextStyle, matrix → MatrixContent / (none),
// curve → CurveContent / CurveStyle, image → ImageContent / ImageStyle,
// icon → IconContent / IconStyle, line → LineContent / LineStyle,
// shape → (none) / ShapeStyle
type LayerNode = {
  frame?: Frame // required for remote layers
  zIndex?: number
  rotation?: number
  transform?: { translateX?: number; translateY?: number }
  content?: object
  style?: object
}
type Frame = {
  width: number; height: number
  left?: number; right?: number; top?: number; bottom?: number
}

type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
type Color = string // #RRGGBB / #AARRGGBB
type GapMode = 'zero' | 'near' | 'default' | 'far' | 'extraFar'
type Pixel = {
  color?: Color; opacity?: number; cornerRadius?: number
  rotation?: number; halo?: number; empty?: boolean
}
type Batch = { pixel: Pixel; coordinates: [row: number, column: number][] }
type MatrixLayout = {
  gapMode?: GapMode; edgeInsetRatio?: number; cellAspectRatio?: number
  columnGapRatio?: number; rowGapRatio?: number
}

type MatrixContent = {
  pixels?: (Pixel | null)[][] // required for remote layers
  default?: Pixel; batch?: Batch[]; layout?: MatrixLayout
}

type TextContent = { text?: string | number }
type TextStyle = ChromeStyle & {
  color?: Color; textOpacity?: number; fontSizeRatio?: number
  fontWeight?: '300' | '400' | '500' | '600' | '700'
  fontFamily?: 'system' | 'systemMonospace' | 'monospace'
  fontFamilyFallback?: string[]; letterSpacing?: number; baselineRatio?: number
  horizontalAlignment?: 'left' | 'center' | 'right'
  verticalAlignment?: 'top' | 'center' | 'bottom'
  autoWrap?: boolean; paddingRatio?: number
}
type ChromeStyle = EffectsStyle & {
  backgroundColor?: Color; opacity?: number
  cornerRadiusRatio?: number
  borderColor?: Color; borderOpacity?: number; borderWidth?: number
  gradientEnabled?: boolean; gradientColors?: Color[]
  gradientStops?: number[]; gradientAngle?: number
}
type EffectsStyle = {
  maskColor?: Color; maskOpacity?: number
  vignetteIntensity?: number
  readabilityGradient?: 'none' | 'top' | 'bottom' | 'left' | 'right' | 'both'
    | 'topLeftDiagonal' | 'topRightDiagonal'
}
type ShapeStyle = ChromeStyle & { color?: Color }
type ImageContent = { url?: string }
type ImageStyle = ChromeStyle & { color?: Color }
type IconContent =
  | {
      icon?: string; iconCodePoint?: number
      iconFont?: 'phosphorBold' | 'devicon' | 'simpleIcons' | 'material'
    }
  | { url?: string }
type IconStyle = {
  color?: Color; textOpacity?: number
  iconGlowEnabled?: boolean; iconGlowColor?: Color
  iconGlowOpacity?: number; iconGlowRadius?: number
}
type LineContent = { direction?: 'horizontal' | 'vertical' }
type LineStyle = {
  color?: Color; opacity?: number; strokeWidth?: number
  pattern?: 'solid' | 'dashed' | 'dotted'; cap?: 'round' | 'butt'
  dashLength?: number; dashGap?: number
  gradientEnabled?: boolean; gradientColors?: Color[]; gradientStops?: number[]
}

type CurveContent = {
  mode?: 'samples' | 'function'
  points?: [x: number, y: number][]
  expression?: string; parameters?: { [name: string]: number }
  domain?: [number, number]; yDomain?: [number, number]
  interpolation?: 'linear' | 'stepAfter' | 'monotone'
  markers?: {
    at: { x?: number; sampleIndex?: number; offsetPt?: [number, number] }
    symbol?: { shape?: 'circle'; radiusPt?: number; fill?: Color }
    label?: string
    labelStyle?: {
      color?: Color; fontSizePt?: number
      position?: 'top' | 'bottom' | 'left' | 'right'
    }
  }[]
}
type CurveStyle = {
  color?: Color; opacity?: number; strokeWidth?: number
  lineCap?: 'round' | 'butt' | 'square'; lineJoin?: 'round' | 'bevel' | 'miter'
  dashLength?: number; dashGap?: number
  fillColor?: Color; fillOpacity?: number; plotPadding?: number
}

Pixel style#

These fields describe one pixel’s style in the root grid’s default, batch[].pixel or pixels[row][column]. They also apply to default, batch[].pixel and pixels in a matrix layer’s content.

color
Range#RRGGBB · #AARRGGBB
DescriptionHex color. The 8-digit form is alpha first (ARGB), not CSS #RRGGBBAA.
opacity
Range0 – 1
DescriptionDot visibility, clamped and multiplied by color alpha: 0 is transparent; 1 applies no further opacity reduction.
cornerRadius
Range0 – 0.5
DescriptionRadius relative to the cell short side: 0 sharp, 0.5 maximally rounded (a circle for square cells, a capsule for rectangular cells).
rotation
Rangedegrees
Description45 turns a square into a diamond.
halo
Range0 – 1
DescriptionDot glow strength; 0 disables it. Highlights selected cells or adds a lit pixel-display appearance.
empty
Rangeboolean
Descriptiontrue skips drawing the cell.

For the root grid, if you omit default, pixels start from color #0A2D48, opacity 0.28, cornerRadius 0.18. Set opacity: 1 when you want vivid, fully opaque colors.

Pixels & precedence#

Each cell resolves in a fixed order, lowest to highest priority:

  • default (or the built-in seed if absent)
  • each batch entry, in order: later style objects replace earlier ones, and each entry’s omitted properties come from default
  • explicit, non-null values in pixels

null means no override here, not transparent. {"empty": true} is an explicit blank: nothing is drawn, but the cell keeps its slot in the grid. An explicit pixel object falls back to default for omitted color and style properties, not the batch beneath it. The empty flag is explicit: with default {"empty": true}, null slots stay blank, while an ordinary explicit pixel object draws a dot again.

Example
{
  "protocol": 1,
  "layout": { "gapMode": "near" },
  "default": { "color": "#18313A", "opacity": 0.34, "cornerRadius": 0.18 },
  "pixels": [
    [
      { "color": "#7CFFB2", "cornerRadius": 0.18 },
      null,
      { "color": "#F4BC54", "rotation": 45 }
    ],
    [
      { "empty": true },
      { "color": "#FFFFFF", "opacity": 0.7 },
      null
    ]
  ],
  "batch": [
    {
      "pixel": { "color": "#38A6FF", "cornerRadius": 0.5, "opacity": 1 },
      "coordinates": [
        [0, 1],
        [1, 2]
      ]
    }
  ]
}
default → batch → explicit pixels

Batch coordinates are zero-based [row, column] integer pairs and must lie inside the grid; out-of-range coordinates reject the frame in the app.

Grid layout#

These fields describe the whole grid’s layout. Put them in the response’s layout object, or in a matrix layer’s content.layout.

The grid is uniformly fitted and centered with contain, preserving the pixel’s width-to-height ratio. Gaps and edge insets use the pixel’s short side as their reference, rather than a percentage of the whole card. Values of edgeInsetRatio, columnGapRatio and rowGapRatio are multiples of a single pixel’s short-side length. gapMode selects a preset gap multiplier, while cellAspectRatio specifies the pixel’s width-to-height ratio.

gapMode
Valueszero · near · default · far · extraFar
Defaultdefault
PurposeGap presets between adjacent pixels: 0, 0.08, 0.16, 0.28, 0.42 times a single pixel’s short-side length, respectively. Applies horizontally and vertically.
edgeInsetRatio
Values−0.45–0.45
Default0
PurposeMargin around the grid = a single pixel’s short-side length × this value. Positive values add margin; negative values extend edge pixels beyond the drawing region, where they are clipped.
cellAspectRatio
Values0.1–10
Default1
PurposeWidth / height of a single pixel. 1 makes a square, below 1 a vertical bar, above 1 a horizontal bar. Does not change the grid’s row or column count.
columnGapRatio
Values0–10
Default—
PurposeHorizontal gap between adjacent pixel columns = a single pixel’s short-side length × this value. Overrides the horizontal gapMode spacing; omitted values use the preset.
rowGapRatio
Values0–10
Default—
PurposeVertical gap between adjacent pixel rows = a single pixel’s short-side length × this value. Overrides the vertical gapMode spacing; omitted values use the preset.
Fit formula
aspect = cellAspectRatio
short = min(aspect, 1)
gx = (columnGapRatio ?? gapModeRatio) * short
gy = (rowGapRatio ?? gapModeRatio) * short
inset = edgeInsetRatio * short
spanX = columns * aspect + (columns - 1) * gx + 2 * inset
spanY = rows + (rows - 1) * gy + 2 * inset
cellHeight = min(canvasWidth / spanX, canvasHeight / spanY)
cellWidth = cellHeight * aspect
Example
{
  "protocol": 1,
  "default": { "color": "#FFFFFF", "opacity": 0.1, "cornerRadius": 0.5 },
  "layout": { "gapMode": "near", "cellAspectRatio": 0.32, "edgeInsetRatio": 0.45 },
  "pixels": [
    [
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#7EDDB5", "opacity": 1 },
      { "color": "#FFB86B", "opacity": 1 },
      { "color": "#FFB86B", "opacity": 1 },
      { "color": "#FFB86B", "opacity": 1 },
      null,
      null,
      null,
      null,
      null,
      null,
      null
    ]
  ]
}
A one-row meter: cellAspectRatio 0.32

Layers#

Layers are flat — no nesting, no children. They are drawn over the root grid; remote and editor layers are composited together by zIndex, with id order breaking ties.

Layer declarations

Each entry in layers declares one remote layer:

id
Typestring
DescriptionStarts with remote: and is unique within the response. Distinguishes layers and orders those with equal zIndex.
type
Typestring
Descriptiontext · matrix · curve · image · icon · line · shape. Selects the fields allowed in content and style of the object dataPath points to.
dataPath
Typestring
DescriptionWhere the object that describes this layer sits in data; see “Data paths” below.

Extra or missing fields, a wrong or duplicate id prefix, an unknown type, or a malformed dataPath make the whole response invalid.

Describing a dynamic layer

Whether it is an editor layer, added in the editor and saved as part of the custom widget configuration, or a remote layer returned purely by the API, a layer points to a field inside data through a path. For the former you enter the path in the matching form field in the editor; for the latter it is the dataPath field described earlier. The object a dataPath points to has the same structure in both cases, made of layout, content and style:

frame
Typeobject
DescriptionPosition and size, see below. Required for remote layers. Optional for editor layers, where it replaces the editor’s position and size as a whole.
zIndex
Typeinteger
DescriptionStacking order; larger values draw above smaller ones. Remote layers default to 0.
rotation
Typedegrees
DescriptionRotates the whole layer around its center. Remote layers default to 0. Per-pixel rotation in a matrix uses the pixel style’s rotation.
transform
Typeobject
DescriptiontranslateX, translateY: an offset applied after the frame is placed, in fractions of the layer’s own width and height.
content
Typeobject
DescriptionLayer content; fields depend on type, see “Layer styles and content”.
style
Typeobject
DescriptionLayer style; fields depend on type. matrix has no style fields because its styling is split elsewhere: what shapes the grid as a whole belongs to layout, and how each individual pixel is drawn lives in the pixels 2D array. The default style and batch styles also sit in content.

This object accepts only the fields above, and content and style accept only the fields listed for the layer’s type. An omitted field and a null field both mean “not provided”.

How widget layers merge

  • Precedence is the object dataPath points to > editor settings > renderer defaults.
  • Object values inside content and style merge field by field onto the editor’s saved values — for example a matrix’s content.default and content.layout. Strings, numbers, booleans and arrays replace the saved value as a whole.
  • frame always replaces the saved frame as a whole. Anchors only make sense together: a response right merged onto a saved left would simply lose to it. That is why a returned frame must include width and height.
  • An omitted or null field keeps the editor’s value; 0, false and empty arrays are valid overrides.
  • Each response starts again from the saved configuration. When a later response stops returning a field, the editor’s value returns; when a request fails, the last successful response stays on screen.

Matrix, text, curve, line and shape layers have a “Current preview” switch in the editor. It only affects the edit page: the default-style preview shows the editor’s values so you can tune the fallback appearance. The app home, Home Screen and StandBy always apply the response.

Layers saved in the editor (illustrative)
[
  {
    "id": "local:title",
    "type": "text",
    "frame": { "left": 0.07, "top": 0.12, "width": 0.6, "height": 0.16 },
    "zIndex": 10,
    "content": { "text": "Orders today" },
    "style": {
      "color": "#A9C4E8",
      "fontSizeRatio": 0.1,
      "fontWeight": "500",
      "horizontalAlignment": "left"
    }
  },
  {
    "id": "local:total",
    "type": "text",
    "frame": { "left": 0.07, "top": 0.32, "width": 0.6, "height": 0.4 },
    "zIndex": 11,
    "dataPath": "data.orders.total",
    "content": { "text": "—" },
    "style": {
      "color": "#FFFFFF",
      "fontSizeRatio": 0.26,
      "fontWeight": "700",
      "horizontalAlignment": "left"
    }
  }
]
API response
{
  "protocol": 1,
  "pixels": [
    [
      { "empty": true }
    ]
  ],
  "data": {
    "orders": {
      "total": {
        "content": { "text": 128 },
        "style": { "color": "#7EDDB5" }
      },
      "badge": {
        "frame": {
          "right": 0.07,
          "top": 0.14,
          "width": 0.24,
          "height": 0.2
        },
        "zIndex": 12,
        "content": { "text": "LIVE" },
        "style": {
          "color": "#0B1222",
          "fontSizeRatio": 0.08,
          "fontWeight": "700",
          "horizontalAlignment": "center",
          "verticalAlignment": "center",
          "backgroundColor": "#7EDDB5",
          "opacity": 1,
          "cornerRadiusRatio": 1
        }
      }
    }
  },
  "layers": [
    {
      "id": "remote:badge",
      "type": "text",
      "dataPath": "data.orders.badge"
    }
  ]
}
Two editor layers, one with a data path, plus one remote layer

In this example local:title has no data path and always shows its editor text. local:total is bound to data.orders.total; that object only provides the text and color, so the size, weight and position still come from the editor. remote:badge is declared in layers and takes its position, content and style from data.orders.badge.

Position and size

frame defines a layer’s position and size within the content canvas. All values are proportions: horizontal values use canvas width, and vertical values use canvas height. For example, width: 0.5 makes the layer half the canvas width; top: 0.1 places its top edge 10% of the canvas height below the canvas top.

  • Width and height are required: whenever a frame is given, it must give both width and height.
  • Position from one edge: use left / right and top / bottom for distances from the corresponding canvas edges. For example, right: 0.1 leaves 10% of the canvas width between the layer’s right edge and the canvas right edge.
  • Both edges: with both left and right, the layer fills the width between those margins and width is not used. If both are 0.1, the layer occupies 80% of the canvas width. top and bottom work the same way.
  • No position: without either horizontal edge, the layer starts at the canvas left; without either vertical edge, it starts at the top. width and height are clamped to 0.02–1.5.
A frame
{
  "left": 0.1,
  "top": 0.1,
  "width": 0.4,
  "height": 0.2
}

On a 200 × 100 pt content canvas, this places the layer 20 pt from the left and 10 pt from the top, with a width of 80 pt and a height of 20 pt. Adding "transform": {"translateX": 0.5} moves it right by half its own width, another 40 pt.

width / height
Type / range0.02–1.5
PurposeRequired. Width/height as fractions of the corresponding canvas axis. Opposing anchors override size on their axis.
left / right
Type / rangenumber
PurposeDistance from left/right edge as a fraction of canvas width. Neither anchor starts at x = 0; right alone pins to the right.
top / bottom
Type / rangenumber
PurposeDistance from top/bottom edge as a fraction of canvas height. Neither anchor starts at y = 0; bottom alone pins to the bottom.
transform.translateX / translateY
Type / rangenumber
PurposeOffset after placement, in units of the layer’s own width/height; positive values move right/down.

Data paths

dataPath starts at data and walks fields and numeric indexes — data.orders.total, data.cards[0]. There are no expressions, wildcards or string keys. It must point to an object.

Accepted grammar
^data(?:\.[A-Za-z_][A-Za-z0-9_]*|\[[0-9]+\])*$

When the object is invalid

Errors in that object only affect its layer; the response stays valid. When the path does not exist, does not point to an object, the object contains a field that it or the layer’s type does not support, a value has the wrong type or an unsupported option, a frame lacks width or height, a remote layer has no frame, or a remote matrix has no content.pixels:

  • an editor layer falls back to its saved state, and the app explains why below the layer’s data path field;
  • a remote layer is not drawn;
  • the root grid and all other layers are shown as usual.

Numbers outside the ranges listed in the field tables are not errors; the renderer clamps them.

Example
{
  "protocol": 1,
  "default": { "color": "#8EC8FB", "opacity": 0.06, "cornerRadius": 0.5 },
  "layout": { "gapMode": "far" },
  "pixels": [
    [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
    [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
    [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
    [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
    [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
    [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
    [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
    [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]
  ],
  "batch": [
    {
      "pixel": { "color": "#7EDDB5", "opacity": 1, "halo": 0.5 },
      "coordinates": [
        [7, 17]
      ]
    }
  ],
  "data": {
    "orders": {
      "title": {
        "frame": {
          "left": 0.07,
          "top": 0.12,
          "width": 0.6,
          "height": 0.16
        },
        "zIndex": 10,
        "content": { "text": "Orders today" },
        "style": {
          "color": "#A9C4E8",
          "fontSizeRatio": 0.1,
          "fontWeight": "500",
          "horizontalAlignment": "left"
        }
      },
      "total": {
        "frame": {
          "left": 0.07,
          "top": 0.32,
          "width": 0.6,
          "height": 0.4
        },
        "zIndex": 11,
        "content": { "text": 128 },
        "style": {
          "color": "#FFFFFF",
          "fontSizeRatio": 0.26,
          "fontWeight": "700",
          "horizontalAlignment": "left"
        }
      },
      "badge": {
        "frame": {
          "right": 0.07,
          "top": 0.14,
          "width": 0.24,
          "height": 0.2
        },
        "zIndex": 5,
        "style": {
          "gradientEnabled": true,
          "gradientColors": ["#2A63D4", "#56B9DD"],
          "cornerRadiusRatio": 1,
          "borderColor": "#D7F6FF",
          "borderOpacity": 0.46,
          "borderWidth": 1
        }
      },
      "trend": {
        "frame": {
          "right": 0.07,
          "top": 0.16,
          "width": 0.24,
          "height": 0.16
        },
        "zIndex": 6,
        "content": {
          "icon": "ph_trend_up",
          "iconCodePoint": 58542,
          "iconFont": "phosphorBold"
        },
        "style": { "color": "#FFFFFF" }
      }
    }
  },
  "layers": [
    {
      "id": "remote:orders-title",
      "type": "text",
      "dataPath": "data.orders.title"
    },
    {
      "id": "remote:orders-total",
      "type": "text",
      "dataPath": "data.orders.total"
    },
    {
      "id": "remote:badge",
      "type": "shape",
      "dataPath": "data.orders.badge"
    },
    {
      "id": "remote:trend",
      "type": "icon",
      "dataPath": "data.orders.trend"
    }
  ]
}
Four remote layers: two texts, a shape and an icon

Layer styles and content#

The object a dataPath points to describes content in content and appearance in style. Fields are grouped by type; the content. / style. prefix shows where a field goes in that object. Colors use #RRGGBB or alpha-first #AARRGGBB; pt means iOS logical pixels. Defaults in the tables apply when a remote layer omits a field. An editor layer keeps its editor setting instead, and fresh editor presets can differ from these defaults.

Style fields shared across layer types

These fields apply to text, image and shape; each type’s table explains its opacity behavior. Other layers use the style fields listed for their own type.

style.backgroundColor
Type / valuesColor
Purpose and rulesColor filling the layer background. Text backgrounds are hidden until style.opacity is raised; shapes fall back to style.color when this field is absent.
style.cornerRadiusRatio
Type / values0–1
Purpose and rulesRound the frame corners and clip content to that outline. Radius = layer short side × value ÷ 2. 0 (the default) is square-cornered; 1 makes a square circular or a rectangle capsule-shaped.
style.borderColor
Type / valuesColor
Purpose and rulesColor of the border drawn inside the layer outline, white by default. Visible only when both borderWidth and borderOpacity exceed zero.
style.borderOpacity
Type / values0–1
Purpose and rulesBorder visibility only: 0 hides it and 1 applies no further opacity reduction; default 0. Does not change the background or content opacity.
style.borderWidth
Type / valuesnumber · pt
Purpose and rulesInward border thickness in pt, default 0. Text content is inset by the same amount. Use 0–8 pt; text clamps values to this range.
style.gradientEnabled
Type / valuesboolean
Purpose and rulesReplace the flat background with a linear gradient. Off when omitted by the API; fresh editor shapes normally save explicit true.
style.gradientColors
Type / valuesColor[]
Purpose and rulesTwo or three colors in gradient order: first at the start, last at the end, with intermediate positions set by gradientStops. Requires gradientEnabled.
style.gradientStops
Type / valuesnumber[] · 0–1
Purpose and rulesOne position per gradient color: 0 is the start and 1 the end. [0, 0.2, 1] places the middle color at 20% of the gradient. Use increasing values; absent or mismatched stops are evenly spaced.
style.gradientAngle
Type / valuesnumber · degrees
Purpose and rulesRotate only the background gradient, not the layer or content, in degrees. 0 runs left to right and 90 top to bottom. API default 0; fresh editor shapes preset 135.
style.maskColor
Type / valuesColor
Purpose and rulesA uniform color overlay above the layer content, black by default. maskOpacity controls its strength; use dark colors to dim content or another color for a wash.
style.maskOpacity
Type / values0–0.6
Purpose and rulesUniform overlay strength: 0 disables it; higher values make the mask more visible. Default 0. Does not directly reduce the underlying content opacity.
style.vignetteIntensity
Type / values0–2
Purpose and rulesDarkening at the edges and corners. 0 disables it; higher values darken the perimeter while keeping the center relatively bright. Default 0.
style.readabilityGradient
Type / valuesstring
Purpose and rulesA black overlay fading inward from a chosen edge to improve overlaid text contrast. none is off (default); top, bottom, left, right choose an edge; both covers top and bottom; topLeftDiagonal and topRightDiagonal fade from that upper corner toward the opposite corner.
Shared style anatomyScroll horizontally on narrow screens to see the full drawing · Select a note to highlight its target
Shared style anatomyDashed boundaries and dimension lines explain fields; they are not drawn by the layer.GLANCE / CHROME / DETAILLayer frame00.51frame.widthbackgroundColor · opacity1gradientColors · Stops · Angle2maskColor · maskOpacity3cornerRadiusRatio4borderWidth · Color · Opacity5readability · vignette6

Dashed boundaries and dimension lines explain fields; they are not drawn by the layer.

text · Text

  • Content: content.text is the text to show — a string, or a number shown as written. An editor layer shows its static editor text until the response provides one.
  • Style: style controls typography and layout. Backgrounds, rounding and borders use the shared fields above.
content.text
Type / valuesstring · number
Purpose and rulesThe text to show. Numbers are converted as written, so 128 displays “128”. Empty for a remote layer that omits it.
style.color
Type / valuesColor
Purpose and rulesText color, white by default; does not color the background.
style.textOpacity
Type / values0–1
Purpose and rulesGlyph visibility only, default 1. 0 hides text and 1 preserves the color’s own alpha. Text background opacity uses style.opacity.
style.opacity
Type / values0–1
Purpose and rulesText background opacity, default 0; does not control the glyphs or the entire layer.
style.fontSizeRatio
Type / values0.02–0.28
Purpose and rulesFont size = the whole content canvas short side × value, default 0.12. A 160 pt canvas short side and 0.1 give a 16 pt font, not 10% of the text frame height.
style.fontWeight
Type / valuesstring
Purpose and rulesGlyph stroke weight as strings "300", "400", "500", "600", "700", from lighter to heavier. Default "400".
style.fontFamily
Type / valuesstring
Purpose and rulesChoose glyph design: system is the default proportional system font; systemMonospace is system fixed-width, useful for counters and clocks; monospace uses Menlo.
style.fontFamilyFallback
Type / valuesstring[]
Purpose and rulesInstalled font names to try for missing glyphs; does not download fonts.
style.letterSpacing
Type / valuesnumber · pt
Purpose and rulesExtra distance between glyphs in pt, default 0 on iOS. Positive values spread letters out; negative values tighten them. Does not change font size.
style.baselineRatio
Type / valuesnumber
Purpose and rulesOptional first-line baseline position: distance from text content top = font size × value. Useful for aligning separate text layers; omit for the font’s natural baseline.
style.horizontalAlignment
Type / valuesleft / center / right
Purpose and rulesPlace text left, center or right inside the viewport after padding and border. Default right; does not move the layer frame.
style.verticalAlignment
Type / valuestop / center / bottom
Purpose and rulesPlace text at the top, center or bottom of the viewport, default top. Does not change frame.top or frame.bottom.
style.autoWrap
Type / valuesboolean
Purpose and rulesWrap at the available text width, default false. Explicit breaks remain; overflowing content is clipped.
style.paddingRatio
Type / values0–0.3
Purpose and rulesInset on each side = text frame short side × value, default 0, plus border width. Higher values leave less room for text.

When the text or its style changes, the runtime computes the smallest area the new text, font size, line breaks, padding and border need, limited to the content canvas, so the Home Screen does not clip text to an outdated area. This never rewrites frame.

Text: glyphs and content areaScroll horizontally on narrow screens to see the full drawing · Select a note to highlight its target
Text: glyphs and content areaDashed boundaries and dimension lines explain fields; they are not drawn by the layer.GLANCE / TEXT / DETAILLayer frameGlanceAPI → 128frame.widthcontent.text1fontSizeRatio · fontWeight2paddingRatio · autoWrap3color · textOpacity4letterSpacing · baselineRatio5horizontal · verticalAlignment6

Dashed boundaries and dimension lines explain fields; they are not drawn by the layer.

Example
{
  "protocol": 1,
  "pixels": [
    [
      { "empty": true }
    ]
  ],
  "data": {
    "parcels": {
      "frame": { "left": 0.08, "top": 0.22, "width": 0.84, "height": 0.56 },
      "content": { "text": 128 },
      "style": {
        "color": "#D9F2F7",
        "textOpacity": 1,
        "fontSizeRatio": 0.26,
        "fontWeight": "600",
        "fontFamily": "systemMonospace",
        "horizontalAlignment": "center",
        "verticalAlignment": "center",
        "autoWrap": false,
        "paddingRatio": 0,
        "backgroundColor": "#397DA0",
        "opacity": 0.3,
        "cornerRadiusRatio": 0,
        "borderColor": "#90BBCD",
        "borderOpacity": 0.6,
        "borderWidth": 1
      }
    }
  },
  "layers": [
    { "id": "remote:count", "type": "text", "dataPath": "data.parcels" }
  ]
}
A number, typography and background

matrix · Matrix

  • Content: content mirrors the root grid — pixels, default, batch and layout, with exactly the same meaning and rules. pixels sets the actual grid dimensions.
  • Style: a matrix has no style fields. Pixel appearance goes in content.default, batch and pixels; spacing and insets go in content.layout.
content.pixels
Type / values(Pixel | null)[][]
Purpose and rulesLocal grid content, with the same rules as the root pixels. Required for remote matrices; an editor matrix shows its default sample until pixels arrive.
content.default
Type / valuesPixel
Purpose and rulesFallback pixel style. For editor matrices it merges field by field onto the editor’s default pixel style; for remote matrices the base is color #5EA2FF, opacity 0.82, cornerRadius 0.16.
content.batch
Type / values{ pixel, coordinates }[]
Purpose and rulesBatch styles, with the same rules as the root batch. Must be returned together with content.pixels.
content.layout
Type / valuesMatrixLayout
Purpose and rulesGaps, cell aspect ratio and edge inset, with the fields in “Grid layout”. Merges field by field onto editor settings; gapMode defaults to near.
Matrix: pixels and grid layoutScroll horizontally on narrow screens to see the full drawing · Select a note to highlight its target
Matrix: pixels and grid layoutDashed boundaries and dimension lines explain fields; they are not drawn by the layer.GLANCE / MATRIX / DETAILLayer framepixels[row][column]frame.widthcontent.pixels1color · opacity · halo2edgeInsetRatio3cellAspectRatio4columnGapRatio · rowGapRatio5cornerRadius · rotation6

Dashed boundaries and dimension lines explain fields; they are not drawn by the layer.

Priority, highest first: explicit values in pixels, then batch, then the response’s default / layout, then the editor’s default / layout. The rows and columns set in the editor only shape the default sample: without valid pixels, the renderer draws a stable sample in the merged default style, so the layer does not disappear before the API is ready. Valid pixels are never resized or rearranged.

Example
{
  "protocol": 1,
  "pixels": [
    [
      { "empty": true }
    ]
  ],
  "data": {
    "title": {
      "frame": { "left": 0.06, "top": 0.08, "width": 0.88, "height": 0.14 },
      "content": { "text": "Contributions · 20 weeks" },
      "style": {
        "color": "#A9C4E8",
        "fontSizeRatio": 0.09,
        "fontWeight": "500",
        "horizontalAlignment": "left"
      }
    },
    "heatmap": {
      "frame": { "left": 0.06, "top": 0.28, "width": 0.88, "height": 0.64 },
      "content": {
        "default": { "color": "#FFFFFF", "opacity": 0.08, "cornerRadius": 0.25 },
        "layout": { "gapMode": "near" },
        "pixels": [
          [
            { "color": "#7EDDB5", "opacity": 0.85 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 0.7448688173552516 },
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.7545944628656934 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 0.9830690317402184 },
            null,
            null
          ],
          [
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 0.5229617408287728 },
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.9544462806264128 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 0.6555567507870917 },
            null,
            null,
            null
          ],
          [
            { "color": "#7EDDB5", "opacity": 0.7222294489865844 },
            null,
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.6638935382087501 },
            { "color": "#7EDDB5", "opacity": 0.6413972526463148 },
            null,
            null,
            null,
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.5311184461090914 },
            { "color": "#7EDDB5", "opacity": 0.8927628180779044 },
            { "color": "#7EDDB5", "opacity": 0.8698567704801822 },
            { "color": "#7EDDB5", "opacity": 0.5938312809225924 },
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.710199232267813 },
            { "color": "#7EDDB5", "opacity": 0.9951574673001937 },
            { "color": "#7EDDB5", "opacity": 0.974044818430527 }
          ],
          [
            null,
            { "color": "#7EDDB5", "opacity": 0.6708227479019796 },
            { "color": "#7EDDB5", "opacity": 0.9875732368676824 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 0.6668848538904473 },
            null,
            null,
            null,
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.5228328839421528 },
            { "color": "#7EDDB5", "opacity": 0.5214774850893487 },
            null,
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.7065983124568725 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 0.972886955341248 },
            null
          ],
          [
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 1 },
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.9993158218339733 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 0.7303804474628726 },
            null,
            null
          ],
          [
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 0.8229182097281214 },
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.6114321719061702 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 0.8256594886799811 },
            null,
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.7008502184288221 }
          ],
          [
            { "color": "#7EDDB5", "opacity": 0.5000626562032289 },
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.6949160968071824 },
            { "color": "#7EDDB5", "opacity": 0.8072480735714076 },
            { "color": "#7EDDB5", "opacity": 0.5647999972978959 },
            null,
            null,
            null,
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.6708958418347376 },
            { "color": "#7EDDB5", "opacity": 0.8120773109877517 },
            { "color": "#7EDDB5", "opacity": 0.6297695437501125 },
            null,
            null,
            { "color": "#7EDDB5", "opacity": 0.5926441641837852 },
            { "color": "#7EDDB5", "opacity": 0.9642279221859965 },
            { "color": "#7EDDB5", "opacity": 1 },
            { "color": "#7EDDB5", "opacity": 0.8892938715888167 }
          ]
        ]
      }
    }
  },
  "layers": [
    { "id": "remote:title", "type": "text", "dataPath": "data.title" },
    { "id": "remote:heatmap", "type": "matrix", "dataPath": "data.heatmap" }
  ]
}
A local heatmap

curve · Curve

  • Content: content declares samples or a function, plus markers that follow the curve. Curve content always comes from the API; an editor curve layer only saves its data path, initial position and fallback style.
  • Style: style controls stroke, fill and plot padding.

Curve content: properties of content in the object dataPath points to.

content.mode
Type / valuessamples / function
Purpose and rulesSelect the curve source: samples connects entries in points; function evaluates expression over domain. Defaults to function with an expression, otherwise samples.
content.points
Type / values[x, y][]
Purpose and rulesUp to 1,024 samples as numeric pairs.
content.expression
Type / valuesstring · ≤512
Purpose and rulesBounded math expression in x: + - * / ^, parentheses, sin cos exp log abs sqrt min max, pi, e and named parameters. Does not execute arbitrary scripts.
content.parameters
Type / valuesobject · ≤64
Purpose and rulesUp to 64 named numeric parameters, values clamped to ±1e9.
content.domain
Type / values[min, max]
Purpose and rulesX data range [min, max], mapping to the plot’s left/right edges. Inferred from samples when omitted, otherwise [0, 1].
content.yDomain
Type / values[min, max]
Purpose and rulesY display range [min, max]: min maps to the plot bottom and max to the top. With [0, 100], 50 is halfway up. Inferred from samples when omitted, otherwise [0, 1].
content.interpolation
Type / valueslinear / stepAfter / monotone
Purpose and rulesShape between samples: linear joins them directly; stepAfter holds the previous value before jumping; monotone smooths the connection while avoiding overshoot (default).
content.markers
Type / valuesarray · ≤32
Purpose and rulesUp to 32 circular markers with optional labels; fields below.

Marker fields: properties of each content.markers[] object.

at.x
Type / valuesnumber
Purpose and rulesX data coordinate of the marker; its height is evaluated from the curve. Choose this or sampleIndex.
at.sampleIndex
Type / valuesinteger
Purpose and rulesZero-based sample index for a marker in sample mode; use samples ordered by increasing x.
at.offsetPt
Type / values[dx, dy] · −64–64
Purpose and rulesLabel offset [horizontal, vertical] from the curve point in pt, default [0, 0]; positive values move right/down. Moves only the label, not the dot or curve.
symbol.shape
Type / valuescircle
Purpose and rulesMarkers currently render as circles.
symbol.radiusPt
Type / values1–16
Purpose and rulesCircle radius in pt, default 4.
symbol.fill
Type / valuesColor
Purpose and rulesCircle fill color, default #E6EDC9.
label
Type / valuesstring
Purpose and rulesOptional marker label; not a nested text layer.
labelStyle.color
Type / valuesColor
Purpose and rulesLabel color, default #E6EEEE.
labelStyle.fontSizePt
Type / values6–32
Purpose and rulesLabel font size in pt, default 10.
labelStyle.position
Type / valuestop / bottom / left / right
Purpose and rulesLabel placement relative to the point, default top.

Curve style: properties of style in the object dataPath points to.

style.color
Type / valuesColor
Purpose and rulesCurve stroke color; area fill uses fillColor separately.
style.opacity
Type / values0–1
Purpose and rulesOverall curve opacity, default 1; also multiplies fill and marker opacity.
style.strokeWidth
Type / values0.25–12 · pt
Purpose and rulesCurve stroke width in pt, default 2.
style.lineCap
Type / valuesround / butt / square
Purpose and rulesStroke endpoints: round adds a semicircle (default), butt ends flat at the coordinate, square extends half a stroke width with a square cap.
style.lineJoin
Type / valuesround / bevel / miter
Purpose and rulesJoins at bends: round rounds them (default), bevel cuts the corner, miter preserves a sharp corner. Most visible on linear or stepped paths.
style.dashLength
Type / values0.5–64 · pt
Purpose and rulesDash segment length in pt; omit for a solid curve.
style.dashGap
Type / values0–64 · pt
Purpose and rulesDash gap in pt, default 4; applies when dashes are enabled.
style.fillColor
Type / valuesColor
Purpose and rulesFill color between the curve and plot bottom; defaults to the stroke color.
style.fillOpacity
Type / values0–1
Purpose and rulesArea-fill opacity, default 0 (off), further multiplied by style.opacity. Does not change stroke width or stroke opacity.
style.plotPadding
Type / values0–48 · pt
Purpose and rulesInset on all four sides between the layer frame and plot in pt, default 0. Higher values shrink the plot; domain / yDomain still map into that inner area.
Curve: data coordinates and plot areaScroll horizontally on narrow screens to see the full drawing · Select a note to highlight its target
Curve: data coordinates and plot areaDashed boundaries and dimension lines explain fields; they are not drawn by the layer.GLANCE / CURVE / DETAILLayer frame010100Nowframe.widthdomain · yDomain1points · interpolation2fillColor · fillOpacity3markers · symbol · label4strokeWidth · lineCap · lineJoin5plotPadding6

Dashed boundaries and dimension lines explain fields; they are not drawn by the layer.

Markers draw circles and text, not axes. Use [x, y] samples, label and labelStyle.position for app/widget compatibility. Compose separate layers for image or icon annotations.

Example
{
  "protocol": 1,
  "pixels": [
    [
      { "empty": true }
    ]
  ],
  "data": {
    "activity": {
      "frame": { "left": 0.05, "top": 0.08, "width": 0.9, "height": 0.84 },
      "content": {
        "mode": "samples",
        "points": [
          [0, 30],
          [2, 18],
          [4, 65],
          [6, 45],
          [8, 90],
          [10, 70]
        ],
        "domain": [0, 10],
        "yDomain": [0, 100],
        "interpolation": "linear",
        "markers": [
          {
            "at": { "x": 8 },
            "symbol": { "shape": "circle", "radiusPt": 4, "fill": "#E1EEE0" },
            "label": "Now",
            "labelStyle": {
              "color": "#E1EEE0",
              "fontSizePt": 9,
              "position": "top"
            }
          }
        ]
      },
      "style": {
        "color": "#A7DAD7",
        "opacity": 1,
        "strokeWidth": 2,
        "lineCap": "round",
        "lineJoin": "round",
        "fillColor": "#77B6BB",
        "fillOpacity": 0.22,
        "plotPadding": 8
      }
    }
  },
  "layers": [
    {
      "id": "remote:activity",
      "type": "curve",
      "dataPath": "data.activity"
    }
  ]
}
Samples, area fill and a curve marker

image · Image

  • Content: content.url supplies the image — an HTTPS URL, a data:image/…;base64, URI or a raw Base64 string.
  • Style: URL images use cover to fill the frame and crop overflow. Rounding, borders and overlays use shared fields; editor-uploaded local images use contain.
content.url
Type / valuesstring
Purpose and rulesImage URL or Base64 data. An editor image layer whose source is set to API data reads this field from the object dataPath points to; when it provides url, the layer shows that image. Servers cannot reference device file paths.
style.opacity
Type / values0.1–1
Purpose and rulesImage visibility, default 1. 0.5 draws it at half opacity so content beneath shows through; minimum 0.1. Does not change overlay or border opacity.
style.color
Type / valuesColor
Purpose and rulesLoading placeholder color; does not tint the image.
Image: cover, clipping and overlaysScroll horizontally on narrow screens to see the full drawing · Select a note to highlight its target
Image: cover, clipping and overlaysDashed boundaries and dimension lines explain fields; they are not drawn by the layer.GLANCE / IMAGE / DETAILLayer frameframe.widthcontent.url1frame → cover crop2opacity · maskOpacity3cornerRadiusRatio · borderWidth4vignetteIntensity5readabilityGradient6

Dashed boundaries and dimension lines explain fields; they are not drawn by the layer.

Native remote images require image/*, are limited to 6 MB and downsampled to a 1,200 px longest edge. Base64 decoding is limited to 4 MB, but inline data still counts toward the 512 KB JSON limit.

icon · Icon

  • Content: a built-in icon is identified by matching content.icon, content.iconCodePoint and content.iconFont values; an image icon uses content.url. One object provides one or the other.
  • Style: icons fit completely within frame, preserving aspect ratio. textOpacity controls visibility; fields below control glyph color and glow.
content.icon
Type / valuesstring
Purpose and rulesStable catalog name, such as ph_activity, used by App previews to look up the icon. Supply it together with the two fields below for the same icon; the name and code point are not alternatives. When an editor layer receives icon, its saved code point and font are no longer used.
content.iconCodePoint
Type / valuesinteger
Purpose and rulesUnicode code point within the font specified by iconFont, used to draw the Home Screen glyph. For example, ph_activity uses 57344; this is not a list index. Must come with icon. Without it, only a subset of legacy Material names has native fallbacks, not the full current catalog.
content.iconFont
Type / valuesphosphorBold / devicon / simpleIcons / material
Purpose and rulesBundled font identifier used by the Home Screen. Must come with icon and iconCodePoint; for example, ph_activity uses phosphorBold. The same code point can draw different symbols in different fonts. Does not load remote fonts.
content.url
Type / valuesstring
Purpose and rulesImage icon URL or Base64 data, shown with contain. Cannot be returned together with content.icon. An editor icon layer whose source is set to API data accepts either a built-in icon or a url from that object.
style.color
Type / valuesColor
Purpose and rulesColor of a built-in glyph, white by default; does not tint image icons.
style.textOpacity
Type / values0–1
Purpose and rulesOverall icon opacity, default 1, for both glyphs and images. 0 hides the icon.
style.iconGlowEnabled
Type / valuesboolean
Purpose and rulesEnable glow, default false; supported for built-in glyphs and editor-uploaded local SVGs, not remote image icons.
style.iconGlowColor
Type / valuesColor
Purpose and rulesColor of the blurred glow surrounding the icon, not the glyph itself. Defaults to the icon color; requires iconGlowEnabled.
style.iconGlowOpacity
Type / values0–1
Purpose and rulesGlow opacity, default 0.65; higher values make it more visible. The icon’s textOpacity also affects final glow visibility.
style.iconGlowRadius
Type / values0.01–0.16
Purpose and rulesGlow blur radius relative to the icon’s short side, default 0.07. Higher values spread the light over a larger area without resizing the glyph.

Use the separate tool page to browse and search in the App’s category order, get the matching icon, iconCodePoint and iconFont values, adjust styles live, and copy the layer JSON. Open the icon browser & preview

Icon: glyph, size and glowScroll horizontally on narrow screens to see the full drawing · Select a note to highlight its target
Icon: glyph, size and glowDashed boundaries and dimension lines explain fields; they are not drawn by the layer.GLANCE / ICON / DETAILLayer frameframe.widthicon · codePoint · font1color · textOpacity2frame → contain3iconGlowEnabled · Color4iconGlowRadius5iconGlowOpacity6

Dashed boundaries and dimension lines explain fields; they are not drawn by the layer.

Native remote image icons are limited to 1 MB and downsampled to a 512 px longest edge. The editor manages locally uploaded icons.

Example
{
  "protocol": 1,
  "pixels": [
    [
      { "empty": true }
    ]
  ],
  "data": {
    "icons": [
      {
        "frame": {
          "left": 0.1,
          "top": 0.25,
          "width": 0.2,
          "height": 0.5
        },
        "content": {
          "icon": "ph_activity",
          "iconCodePoint": 57344,
          "iconFont": "phosphorBold"
        },
        "style": { "color": "#A7DAD7", "textOpacity": 1 }
      },
      {
        "frame": {
          "left": 0.4,
          "top": 0.25,
          "width": 0.2,
          "height": 0.5
        },
        "content": {
          "icon": "ph_lightning",
          "iconCodePoint": 58078,
          "iconFont": "phosphorBold"
        },
        "style": { "color": "#F0C778", "textOpacity": 1 }
      },
      {
        "frame": {
          "left": 0.7,
          "top": 0.25,
          "width": 0.2,
          "height": 0.5
        },
        "content": {
          "icon": "ph_trend_up",
          "iconCodePoint": 58542,
          "iconFont": "phosphorBold"
        },
        "style": { "color": "#A7DAD7", "textOpacity": 0.45 }
      }
    ]
  },
  "layers": [
    {
      "id": "remote:activity",
      "type": "icon",
      "dataPath": "data.icons[0]"
    },
    {
      "id": "remote:lightning",
      "type": "icon",
      "dataPath": "data.icons[1]"
    },
    {
      "id": "remote:trend",
      "type": "icon",
      "dataPath": "data.icons[2]"
    }
  ]
}
Glyphs, colors and opacity

line · Line

  • Content: content.direction chooses a horizontal or vertical line through the frame center. Use rotation for other angles.
  • Style: frame sets position and length, strokeWidth sets thickness, and pattern chooses solid, dashed or dotted strokes.
content.direction
Type / valueshorizontal / vertical
Purpose and rulesDirection, horizontal by default. Horizontal lines cross the frame’s vertical center; vertical lines cross its horizontal center.
style.color
Type / valuesColor
Purpose and rulesStroke color, white by default; replaced by gradientColors when a gradient is enabled.
style.opacity
Type / values0–1
Purpose and rulesOpacity of the entire line, default 1, including gradient colors and dotted circles.
style.strokeWidth
Type / values0.5–8 · pt
Purpose and rulesStroke width in pt, default 2. The frame controls length, not thickness.
style.pattern
Type / valuessolid / dashed / dotted
Purpose and rulesStroke pattern: solid is continuous (default); dashed alternates painted segments and gaps; dotted repeats circles with diameter set by strokeWidth.
style.cap
Type / valuesround / butt
Purpose and rulesEndpoints of solid strokes and dash segments: round is semicircular (default), butt ends flat. Dotted strokes always use circles and ignore this field.
style.dashLength
Type / values1–32 · pt
Purpose and rulesLength of each dash in pt, default 6; used only for dashed.
style.dashGap
Type / values0–32 · pt
Purpose and rulesEmpty distance between dash segments or dots in pt, default 4. Higher values spread them farther apart; unused for solid.
style.gradientEnabled
Type / valuesboolean
Purpose and rulesDraw the stroke with a gradient, default false.
style.gradientColors
Type / valuesColor[]
Purpose and rulesTwo or three gradient colors; requires gradientEnabled. Native widgets run left to right across the frame; app previews run top to bottom for vertical lines.
style.gradientStops
Type / valuesnumber[] · 0–1
Purpose and rulesOne increasing position per color: 0 at the start and 1 at the end. Omit for even spacing.
Line: width, dashes and capsScroll horizontally on narrow screens to see the full drawing · Select a note to highlight its target
Line: width, dashes and capsDashed boundaries and dimension lines explain fields; they are not drawn by the layer.GLANCE / LINE / DETAILLayer framedashgapframe.widthdirection · frame1strokeWidth2pattern · dashLength · dashGap3cap4gradientColors · gradientStops5color · opacity6

Dashed boundaries and dimension lines explain fields; they are not drawn by the layer.

Example
{
  "protocol": 1,
  "pixels": [
    [
      { "empty": true }
    ]
  ],
  "data": {
    "solid": {
      "frame": { "left": 0.1, "top": 0.32, "width": 0.8, "height": 0.04 },
      "content": { "direction": "horizontal" },
      "style": {
        "color": "#A4DAE3",
        "opacity": 0.55,
        "strokeWidth": 1,
        "pattern": "solid",
        "cap": "butt"
      }
    },
    "dashed": {
      "frame": { "left": 0.1, "top": 0.62, "width": 0.8, "height": 0.04 },
      "content": { "direction": "horizontal" },
      "style": {
        "color": "#A4DAE3",
        "opacity": 1,
        "strokeWidth": 3,
        "pattern": "dashed",
        "cap": "butt",
        "dashLength": 8,
        "dashGap": 6
      }
    }
  },
  "layers": [
    { "id": "remote:solid", "type": "line", "dataPath": "data.solid" },
    { "id": "remote:dashed", "type": "line", "dataPath": "data.dashed" }
  ]
}
Solid and dashed strokes with different widths and gaps

shape · Shape

  • Content: the layer itself is a rounded rectangle and needs no content. Use it for panels, badges and progress tracks.
  • Style: style defines fill and border; shared fields control rounding, gradients and overlays. Explicitly enable gradients and set visible border values.
content
Type / valuesobject
Purpose and rulesNo content value is needed; the layer itself is a filled, optionally bordered rounded rectangle.
style.color
Type / valuesColor
Purpose and rulesFlat fill color, default #2A63D4. backgroundColor takes precedence; with a gradient enabled, gradientColors determines the fill.
style.opacity
Type / values0–1
Purpose and rulesFill opacity only, default 1. 0 hides the fill while allowing the border to remain; borderOpacity controls that border separately.
Shape: fill, rounding and borderScroll horizontally on narrow screens to see the full drawing · Select a note to highlight its target
Shape: fill, rounding and borderDashed boundaries and dimension lines explain fields; they are not drawn by the layer.GLANCE / SHAPE / DETAILLayer frame00.51frame.widthcolor · backgroundColor · opacity1gradientEnabled · gradientColors2gradientStops · gradientAngle3cornerRadiusRatio4borderWidth5borderColor · borderOpacity6

Dashed boundaries and dimension lines explain fields; they are not drawn by the layer.

Example
{
  "protocol": 1,
  "pixels": [
    [
      { "empty": true }
    ]
  ],
  "data": {
    "badge": {
      "frame": { "left": 0.12, "top": 0.22, "width": 0.76, "height": 0.56 },
      "style": {
        "opacity": 1,
        "cornerRadiusRatio": 1,
        "gradientEnabled": true,
        "gradientColors": ["#225B80", "#78B6BA"],
        "gradientStops": [0, 1],
        "gradientAngle": 0,
        "borderColor": "#BCE3E4",
        "borderOpacity": 0.9,
        "borderWidth": 2
      }
    }
  },
  "layers": [
    { "id": "remote:badge", "type": "shape", "dataPath": "data.badge" }
  ]
}
Capsule with gradient fill and an independent border

Server-rendered images#

Besides describing content with pixels and layers, your server can take over drawing entirely: render the whole widget into one image with any chart library, template engine or headless browser, and return it through data. The widget shows it as an Image material that fills the canvas — no layers to configure, and none of the drawing syntax on this page to learn.

It fits well when you already have a report or image pipeline, need complex charts, custom fonts or free-form layout, or want the server to own every visual detail so you can change it without touching the widget in the app.

Set up in the app

  • In the editor, set Material to Image;
  • set Image Source to API;
  • enter the field that holds the image as the Data Path, for example data.cover;
  • optionally turn on a readability gradient if you overlay text layers.

Response

An image-only response
{
  "protocol": 1,
  "pixels": [[{ "empty": true }]],
  "data": {
    "cover": "https://cdn.example.com/widgets/orders-medium@3x.png"
  }
}
HTTPS URL
Must return image/*, up to 6 MB; the Home Screen downsamples it to 1,200 px on the longest edge.
data:image/…;base64, or raw Base64
Embedded in the response, best for small images; the whole JSON response is still limited to 512 KB.

This data path belongs to the material setting and points directly to the image string. It is not a layer and does not need an object.

The image fills the whole widget with cover scaling and is cropped at the edges. Render it at the aspect ratio of gw_canvas_width_pt × gw_canvas_height_pt from the request, at 2–3× pixel density so it stays sharp on the Home Screen.

pixels is still required; return [[{"empty": true}]] to show only the image. You can still draw pixels or layers on top, for example native text for a value that changes often.
Text inside the image does not follow system font or Bold Text settings. If the URL stays the same while the picture changes, add a version or timestamp to it so a cached image isn’t reused.

Request signing#

You can add a secret to a widget in the app so that every request carries two headers your server can verify, providing proof that the request comes from a Glance Widgets widget rather than an unrelated or malicious third party. The secret itself is never sent.

X-GlanceWidgets-Timestamp
Unix time in milliseconds, as a decimal string.
X-GlanceWidgets-Signature
Base64(HMAC-SHA256(key = secret, message = timestamp + "\n" + secret)), UTF-8 throughout.
The header names use the current product identifier GlanceWidgets. The signature covers the timestamp and secret only — not the URL, query or body.

On the server: reject stale timestamps (for example ±5 minutes), recompute the signature, and compare in constant time.

verify.ts
import crypto from 'node:crypto'

const SECRET = process.env.GLANCE_SECRET          // same value as in the app
const WINDOW_MS = 5 * 60 * 1000

export function verify(req) {
  const ts = req.get('X-GlanceWidgets-Timestamp')
  const sig = req.get('X-GlanceWidgets-Signature')
  if (!ts || !sig || Math.abs(Date.now() - Number(ts)) > WINDOW_MS) return false

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${ts}\n${SECRET}`)
    .digest('base64')

  const a = Buffer.from(sig)
  const b = Buffer.from(expected)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
verify.py
import base64, hashlib, hmac, os, time

SECRET = os.environ["GLANCE_SECRET"]
WINDOW_MS = 5 * 60 * 1000

def verify(headers) -> bool:
    ts = headers.get("X-GlanceWidgets-Timestamp")
    sig = headers.get("X-GlanceWidgets-Signature")
    if not ts or not sig or abs(time.time() * 1000 - int(ts)) > WINDOW_MS:
        return False
    message = f"{ts}\n{SECRET}".encode()
    expected = base64.b64encode(
        hmac.new(SECRET.encode(), message, hashlib.sha256).digest()
    ).decode()
    return hmac.compare_digest(sig, expected)

Test vector: secret this is secret, timestamp 1700000000000 → sSFWELbV2YwjdDQhWZwTcWlX5BWUx5J6TPpsZmuPii0=.

Limits & errors#

Body size
512 KB (524,288 bytes)
Root grid
rows × columns ≤ 4,096; ≥ 1 × 1; rectangular
Layer declarations
Each has only id, type, dataPath, with ids prefixed remote:
Protocol
1 or omitted
Curve
≤ 1,024 points · ≤ 32 markers · expression ≤ 512 chars
Images
≤ 6 MB remote · ≤ 4 MB Base64 · cover fit

Oversized bodies, invalid root grids, out-of-range batch coordinates, or malformed layer declarations cause the app to reject the whole response. Errors in the object a layer points to only affect that layer: an editor layer falls back to its saved state and a remote layer is not drawn. Curve samples, markers and parameters are truncated to their budgets; invalid or overlong expressions are not executed.

Refresh#

Home Screen widgets request a new frame on the WidgetKit timeline, about every 15 minutes. iOS decides the exact moment based on battery, usage and the system’s refresh budget. Design frames that stay meaningful between updates — include a "last updated" time in data if it matters.

Playground#

Edit the JSON and watch it render. The validator applies the same limits and precedence as the app.

Valid frame 0 cells · 0 layers · 0 bytes

Checklist#

  • HTTPS endpoint, responds 2xx within a few seconds.
  • Root is an object with "protocol": 1 and a rectangular pixels array ≤ 4,096 cells.
  • Colors are #RRGGBB (or ARGB #AARRGGBB).
  • Each layers entry has only id, type and dataPath, with ids starting remote:.
  • Every dataPath points to an object in data; for remote layers it includes a frame with width and height.
  • Grid sized from gw_aspect_ratio, not a fixed table.
  • If signing is enabled, verify the timestamp window and signature in constant time.

Questions about the protocol?

Protocol V1 · Last verified against app 1.0.0

Email us