Webhook Widget

Start

Quickstart

You need an HTTPS URL that returns JSON. Start with one of the examples, then replace it with your own endpoint.

1. Add an endpoint

A hook is a URL saved in the app under a name you choose. The app includes a few examples, so you can try a real widget before writing a server. Adding one is free, does not start the trial, and can be undone at any time.

You can also add a hook from a QR code. See llms.txt for the link format, or use a signed code shared by someone else.

URL
https://webhookwidget.com/demo/hello

Every demo on this site is served the same way, at /demo/<id>, with timestamps generated at the moment of the request. To add another, tap Add Hook and give it a name and an address.

2. Add the widget

Touch and hold an empty area of the Home Screen, tap Edit then Add Widget, and choose Webhook Widget at the size you want. A new widget starts on your first hook; to point it at a different one, touch and hold it, choose Edit Widget, and pick the name.

What that URL returns
json
{
  "v": 1,
  "refresh": 900,
  "accessibility": "Hello, World! Fetched from webhookwidget.com just now",
  "url": "https://webhookwidget.com/docs/quickstart/",
  "view": {
    "type": "vstack",
    "align": "leading",
    "spacing": 0,
    "padding": 16,
    "children": [
      {
        "type": "text",
        "text": "WEBHOOK WIDGET",
        "style": "caption2",
        "weight": "bold",
        "color": "secondary"
      },
      {"type": "spacer"},
      {
        "type": "text",
        "text": "Hello, World!",
        "style": "title2",
        "weight": "bold",
        "design": "rounded",
        "color": "primary",
        "lines": 2
      },
      {"type": "spacer"},
      {
        "type": "text",
        "text": "FETCHED",
        "style": "caption2",
        "weight": "semibold",
        "color": "tertiary"
      },
      {
        "type": "timer",
        "from": "2026-08-11T08:40:00Z",
        "style": "relative",
        "size": 13,
        "color": "secondary"
      }
    ]
  }
}

3. Return your own JSON

The smallest valid response is a version and a single node. Everything else in the protocol is optional.

json
{
  "v": 1,
  "view": { "type": "text", "text": "Hello from my server" }
}

Something worth looking at is not much bigger. This handler reads the appearance from the query string and answers with a heading, a number and a gauge:

Node — Express
import express from "express";

const app = express();

app.get("/widget", (request, response) => {
  const { family, w, h, scheme } = request.query;

  response.json({
    v: 1,
    refresh: 900,
    accessibility: "Four tasks left today",
    view: {
      type: "vstack",
      align: "leading",
      spacing: 2,
      padding: 16,
      bg: scheme === "dark" ? "#14181F" : "white",
      children: [
        { type: "text", text: "TODAY", style: "caption2",
          weight: "bold", color: "secondary" },
        { type: "text", text: "4 left", style: "largeTitle",
          weight: "bold", design: "rounded" },
        { type: "spacer" },
        { type: "gauge", value: 6, min: 0, max: 10, tint: "green" },
      ],
    },
  });
});

app.listen(3000);
Python — Flask
from flask import Flask, jsonify, request

app = Flask(__name__)

@app.get("/widget")
def widget():
    dark = request.args.get("scheme") == "dark"
    return jsonify({
        "v": 1,
        "refresh": 900,
        "accessibility": "Four tasks left today",
        "view": {
            "type": "vstack",
            "align": "leading",
            "spacing": 2,
            "padding": 16,
            "bg": "#14181F" if dark else "white",
            "children": [
                {"type": "text", "text": "TODAY", "style": "caption2",
                 "weight": "bold", "color": "secondary"},
                {"type": "text", "text": "4 left", "style": "largeTitle",
                 "weight": "bold", "design": "rounded"},
                {"type": "spacer"},
                {"type": "gauge", "value": 6, "min": 0, "max": 10, "tint": "green"},
            ],
        },
    })
The response above

4. Choose a refresh interval

refresh is how many seconds the widget waits before fetching again, clamped to between 5 minutes and a day. It is a request, not a promise: iOS decides when each fetch actually happens and will stretch your interval when it feels like it.

Rather than fighting that, answer one fetch with several future states. That is what timelines are for, and it is the difference between a widget that is usually right and one that is always right.

Next steps