## Quickstart (curl)

1. Credential. Use the edit key from the edit link you were given (`/b/<board>/join?key=<edit key>`) — or create a board:
```bash
curl -s -X POST https://instant-gui.com/api/v1/boards -H 'Content-Type: application/json' -d '{"title":"Shop live"}'
# → {"board":{"id":"<board>",…},"editLink":"…/b/<board>/join?key=<edit key>","writeToken":"igw_…"}
```
Every API call accepts either credential: `Authorization: Bearer <edit key or write token>`.
Need the write token (for scripts/devices) from an edit key? `curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/token -H 'Authorization: Bearer <edit key>'` (`{"rotate":true}` issues a new one).

2. Declare the whole board in ONE idempotent call (create/update/move widgets, rename the board; `"prune": true` deletes widgets not listed):
```bash
curl -s -X PUT https://instant-gui.com/api/v1/boards/<board>/spec -H 'Authorization: Bearer <credential>' -H 'Content-Type: application/json' -d @spec.json
# → {"ok":true,"created":[…],"updated":[…],"unchanged":[…],"deleted":[],"hints":[…],"lint":{…},"urls":{…}}
```
Read `hints` in the response: they flag defaults that change behaviour — controls are editor-only unless `config.viewersCanUse: true`, a number with `decimals: 0` shows rounded, overlapping layouts.
spec.json:
```json
{
  "title": "Shop live",
  "prune": true,
  "widgets": [
    {
      "key": "headline",
      "type": "markdown",
      "layout": {
        "x": 0,
        "y": 0,
        "w": 12,
        "h": 2
      },
      "data": {
        "text": "**Sales are up 12% today.** Ads paused at 14:05 by Dana."
      }
    },
    {
      "key": "revenue",
      "type": "kpi",
      "title": "Revenue today",
      "layout": {
        "x": 0,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "prefix": "$",
        "icon": "wallet"
      },
      "data": {
        "value": 4820
      }
    },
    {
      "key": "orders",
      "type": "kpi",
      "title": "Orders today",
      "layout": {
        "x": 3,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "icon": "shopping-cart"
      },
      "data": {
        "value": 131
      }
    },
    {
      "key": "conversion",
      "type": "gauge",
      "title": "Conversion",
      "layout": {
        "x": 6,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "max": 10,
        "suffix": "%",
        "decimals": 1
      },
      "data": {
        "value": 3.4
      }
    },
    {
      "key": "ads",
      "type": "actions",
      "title": "Ad campaign",
      "layout": {
        "x": 9,
        "y": 2,
        "w": 3,
        "h": 3
      },
      "config": {
        "buttons": [
          {
            "id": "pause",
            "label": "Pause ads",
            "tone": "danger"
          },
          {
            "id": "resume",
            "label": "Resume ads",
            "tone": "success"
          }
        ]
      }
    },
    {
      "key": "revenue-trend",
      "type": "line",
      "title": "Revenue per day ($)",
      "layout": {
        "x": 0,
        "y": 5,
        "w": 8,
        "h": 5
      },
      "data": {
        "series": [
          {
            "name": "revenue",
            "points": [
              [
                "2026-10-08",
                4100
              ],
              [
                "2026-10-09",
                4390
              ],
              [
                "2026-10-10",
                4820
              ]
            ]
          }
        ]
      }
    },
    {
      "key": "channels",
      "type": "pie",
      "title": "Orders by channel",
      "layout": {
        "x": 8,
        "y": 5,
        "w": 4,
        "h": 5
      },
      "data": {
        "slices": [
          {
            "name": "Organic",
            "value": 61
          },
          {
            "name": "Ads",
            "value": 44
          },
          {
            "name": "Email",
            "value": 26
          }
        ]
      }
    },
    {
      "key": "health",
      "type": "status",
      "title": "Systems",
      "layout": {
        "x": 0,
        "y": 10,
        "w": 4,
        "h": 4
      },
      "data": {
        "services": [
          {
            "name": "Checkout",
            "status": "up"
          },
          {
            "name": "Payments",
            "status": "up"
          }
        ]
      }
    },
    {
      "key": "orders-log",
      "type": "log",
      "title": "Recent orders",
      "layout": {
        "x": 4,
        "y": 10,
        "w": 8,
        "h": 4
      },
      "data": {
        "entries": [
          {
            "t": 1760000000000,
            "text": "#1045 · $129.00 · Organic",
            "level": "success"
          }
        ]
      }
    }
  ]
}
```

3. Keep it live — push by key:
```bash
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/revenue -H 'Authorization: Bearer <credential>' -d '{"value": 5010}'
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/revenue-trend -H 'Authorization: Bearer <credential>' -d '{"append": {"revenue": 5010}}'          # x = now
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/revenue-trend -H 'Authorization: Bearer <credential>' -d '{"append": {"revenue": ["2026-10-11", 5010]}}'  # explicit x (date/ms)
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/orders-log -H 'Authorization: Bearer <credential>' -d '{"append": {"t": 1760000100000, "text": "#1046 · $54.50 · Email", "level": "success"}}'   # t = epoch ms (default: now)
```

4. Read controls back (what the viewer clicked): `GET /api/v1/boards/<board>/widgets/ads` → `data.lastAction`, or set `config.webhookUrl` to be called on every click.

5. Check the design: `GET /api/v1/boards/<board>/lint` → `{"ok":true,"warnings":[],"suggestions":[…]}`. Fix every warning.

6. Embed (iframe, works for private boards too):
```bash
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/view-tokens -H 'Authorization: Bearer <edit key>' -d '{"label":"Website"}'
# → {"embedUrl":"https://instant-gui.com/e/<board>?token=<view token>","iframe":"<iframe src=…>"}
```
Look (validated; the board's saved `embedStyle` applies first): `&theme=auto|light|dark|<theme id>` (auto = viewer's light/dark), `&bg=transparent` (no page background/padding — sits on the host card), `&accent=<hex>`, `&radius=<px 0-32>`, `&density=compact`, `&font=system|serif|mono|<font-family list>`, `&c.<token>=<color>` (tokens: background, foreground, card, cardForeground, border, input, primary, primaryForeground, muted, mutedForeground, success, warning, danger, info, chart2–chart6), `&page=<page title>`. Easiest on a web page: `<div data-instantgui-board="<board>" data-token="<view token>" data-inherit-theme></div><script src="https://instant-gui.com/embed.js" async></script>` (auto height, adopts the page's colors/font, follows its light/dark), or `InstantGUI.mount(el,{board,token,transparent:true,colors:{background:"var(--surface)",foreground:"var(--text)",primary:"var(--accent)"}})`. Host messages: post `{type:"instantgui:theme",reset:true,theme,bg,radius,density,font,vars:{background,foreground,card,border,primary,…}}` to retheme live; the embed posts `{type:"instantgui:ready"}`, `{type:"instantgui:resize",height}` (it never sizes by the frame's height) and `{type:"instantgui:scheme",scheme}` (set the iframe's color-scheme to it when transparent). `https://instant-gui.com/theme.css` gives a host page InstantGUI's tokens as `--ig-*` variables. Default board embed look: `PATCH /api/v1/boards/<board>` with `{"embedStyle":{"theme":"auto","bg":"transparent","vars":{…}}}`. Controls in an embed are locked unless the token was created with `{"canUseControls":true}` (the default token is view-only; a button also only does something if a Connector, `config.webhookUrl` or your script reading `data.lastAction` handles it; edit key only; use it only on private pages — presses show "by embed: <label>"). The parameter is `token` (`t` works as an alias). A private board without a valid token answers 403, a wrong or revoked token is refused too. Revoke: `DELETE /api/v1/boards/<board>/view-tokens/<id>`.

Change one widget: `PUT /api/v1/boards/<board>/widgets/<key>` with `{"type","title","config","data","layout"}` (create or update), or `PATCH …/widgets/<key>` with `{"layout":{"x":0,"y":0,"w":6,"h":4}}` to move/resize.
Rename/theme/visibility/default embed look: `PATCH /api/v1/boards/<board>` with `{"title","theme","visibility","embedStyle"}` (visibility needs the edit key).
List widgets: `GET /api/v1/boards/<board>/widgets`. Widget schemas: `GET /api/v1/widgets`. OpenAPI: `GET /api/v1/openapi.json`. MCP: `https://instant-gui.com/api/mcp` (tool `apply_board_spec`).

## Change a board in place
"Rename it, move that widget, pause that button, embed it": each is ONE call on the board you already built. Never recreate the board or send the spec again for these.
```bash
# Rename the board
curl -s -X PATCH https://instant-gui.com/api/v1/boards/<board> -H 'Authorization: Bearer <credential>' -d '{"title":"Shop live (EU)"}'
# Move or resize a widget (only the fields you send change)
curl -s -X PATCH https://instant-gui.com/api/v1/boards/<board>/widgets/revenue -H 'Authorization: Bearer <credential>' -d '{"layout":{"x":0,"y":0,"w":6,"h":3}}'
# Pause a control: viewers can no longer press it (editors still can). Turn it back on with true
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/widgets/ads -H 'Authorization: Bearer <credential>' -d '{"config":{"viewersCanUse":false}}'
# Embed a private board: give the site a view token (revocable, read-only)
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/view-tokens -H 'Authorization: Bearer <credential>' -d '{"label":"Website"}'
# → the embed answer holds "embedUrl" (https://instant-gui.com/e/<board>?token=<view token>) and a ready "iframe"
```
MCP: update_board (rename), update_widget_layout (move/resize), push_data with {"config":{…}} (pause), create_view_token (embed).

## Press a control you just wired
Wiring a button is not done until you have pressed it. The press runs the Connector command on the user's machine (no connector, or a name no control sends, answers 404 with the names that exist).
```bash
# 1. Press it as the editor (edit key or write token). Name = <widget key> or <widget key>/<button id>; list the valid names with GET https://instant-gui.com/api/v1/boards/<board>/actions
curl -s -X POST https://instant-gui.com/api/v1/boards/<board>/actions -H 'Authorization: Bearer <credential>' -d '{"action":"backup/run"}'
# → 202 {"ok":true,"event":{"id":"<event id>","action":"backup/run","status":"pending"},"result":"…/actions?events=<event id>"}
# 2. Read the result (poll until status is "done" or "error"; "result" holds the command's output)
curl -s https://instant-gui.com/api/v1/boards/<board>/actions?events=<event id> -H 'Authorization: Bearer <credential>'
# → {"ok":true,"events":[{"id":"<event id>","action":"backup/run","status":"done","result":"backup ok",…}],…}
```
MCP: trigger_action {action:"backup/run"}, then list_actions for the result. Viewers can press a control only if its `config.viewersCanUse` is true; with the edit key or write token you always can.

Calling it with plain HTTP? Send both Content-Type: application/json and Accept: application/json, text/event-stream — without that Accept header the server answers 406 and names the missing header. The reply is a server-sent event: a line 'event: message' followed by a line starting 'data: ' that holds the JSON-RPC response; read the JSON from that data line.
```bash
curl -X POST https://instant-gui.com/api/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

# Designing an InstantGUI board (for AI agents)

A board is read in 3 seconds on a phone. Show, don't tell. Every rule below is checked by `GET /api/v1/boards/<board>/lint`.

## Pick the widget by the question
| The user asks… | Use |
|---|---|
| "How much / how many right now?" | `kpi` (one number + trend) or `stats` (2–6 numbers in a row) |
| "How full / how close to the limit?" | `gauge` (dial) or `progress` (bar toward a goal) |
| "How is it changing?" | `line` — append one point per update |
| "Which is bigger?" | `bar` |
| "What's the split?" | `pie` (≤ 6 slices) |
| "Is it healthy?" | `status` (several services) or `indicator` (one light) |
| "What just happened?" | `log` (append lines) or `timeline` |
| "What do I do now?" | `actions` / `button` / `toggle` / `slider` / `input` / `select` |
| "Why?" (context) | ONE `markdown` headline, ≤ 2 sentences |

## Rules
1. **Numbers and charts, not prose.** At most one markdown widget: a headline of ≤ 2 sentences (≤ 280 characters). Never put numbers, lists or status in text — they each have a widget.
2. **Most important thing top-left** (x 0, y 0). Then group widgets by the question they answer; one page per audience or topic (≤ 16 widgets a page).
3. **Always add actions** the viewer would want next: Restart, Pause, Resume, Run now, Acknowledge, Refresh. An on/off state (paused, maintenance) is a toggle in the actions widget, not two buttons: `config.toggles: [{"id":"paused","label":"Paused","on":"pause-all","off":"resume-all"}]` + push the real state as `data.states: {"paused": true}`. A toggle whose ON state is bad (a safety stop, an outage flag) gets `"onTone": "danger"` so it turns red when on. A pick-one-or-more set (focus list, filter) is a chip set: buttons + `data.selected: ["<button id>", …]` — selected chips are filled with a check and `aria-pressed="true"`, the rest neutral; push the real selection after each change (a click only sends the button's action). Never show a selection only by colour or a ✓ in the label. A click reaches you three ways: the InstantGUI Connector runs a command on the user's machine (`/docs/connector`), a webhook (`config.webhookUrl`, https) gets a POST, or your script reads it back (`GET …/widgets/<key>` → `data.lastAction` / `data.on` / `data.value`). Controls are editor-only by default (the human's edit link); set `config.viewersCanUse: true` only if anyone with the view link may press them.
4. **Trends beat snapshots.** If a number changes over time, also keep a `line` of it (append a point each update). Don't put numeric tables where a bar/line chart works.
5. **Title every widget with what it answers and the unit**: "Revenue today ($)", "p95 latency (ms)". Put units in kpi `prefix`/`suffix`, not in the title AND the value.
6. **Color means state**: use thresholds (kpi/gauge `thresholds`) and `status` levels up/degraded/down — not decoration. Green = fine, amber = look, red = act.
7. **Mobile-first layout.** 12-column grid, rows ≈ 52 px. Phones (≤ 640 px) stack widgets in reading order (y, then x). Good sizes: kpi w 3 h 3 · stats w 6–12 h 2 · charts w ≥ 6 h ≥ 4 · status/log w 4–6 h 4–6 · actions w 3–4 h 2–3 · headline w 12 h 2.
8. **Update cadence**: push when the value changes, at most every few seconds per widget; append to charts/logs instead of rewriting them; send the full spec again only when the board's *structure* changes (the spec is idempotent, so re-sending it is safe — in a spec, `data` of streams like line/log/table only seeds a new widget and never wipes what you appended).
9. **Stable keys**: lowercase, short, meaningful (`revenue`, `orders-log`). Your scripts push by key forever.
10. **Use the same labels across widgets so they cross-filter.** Clicking a bar, pie slice, line or a text cell in a table filters the page by that value (a chip bar shows it; click again, ✕ or Esc clears): charts highlight the matching category/series/slice, tables and logs narrow to rows/lines containing it, other widgets are untouched. It works only when values match exactly (case-insensitive) — name bar categories, pie slices and table cells the same ("North", not "north region").

## Anti-patterns (the lint flags these)
- Walls of text, several markdown widgets, or a markdown "report" of numbers.
- A table of numbers where a chart would answer at a glance.
- 20+ widgets on one page; widgets 1–2 columns wide; charts < 3 rows tall.
- No actions: a dashboard you can only look at.
- Leaving sample data in widgets.

## InstantGUI dashboards (recommended instructions — paste into CLAUDE.md, .cursor/rules or custom instructions)
When I ask for a dashboard, status page or "a UI for this", publish it to InstantGUI (https://instant-gui.com):
- If I give you an edit link (…/b/<board>/join?key=<edit key>), use the edit key as "Authorization: Bearer <edit key>". Otherwise create a board: POST /api/v1/boards and give me its editLink.
- Declare the whole board in one call: PUT /api/v1/boards/<board>/spec (idempotent; include layout). Then push live values with POST /api/v1/boards/<board>/widgets/<key>.
- Charts and numbers over text: kpi/stats/gauge for numbers, line for trends, bar for comparisons, status for health, log for events; at most ONE short markdown headline.
- Always add the actions I'd want next (pause/restart/run now) as actions/button/toggle widgets.
- Check GET /api/v1/boards/<board>/lint and fix every warning before telling me it's done.
- For a website/iframe: POST /api/v1/boards/<board>/view-tokens and use the returned embedUrl.
Docs: https://instant-gui.com/llms.txt
