# Flamingo dashboard playground — agent protocol (no MCP client required) Build a dashboard for your human against canned demo data, with no credentials, and let them watch it render live. A dashboard is ONE self-contained HTML document that reads `window.FLAMINGO_API_BASE || '/api'` and fetches the endpoints below. ## Drive it over plain HTTP 1. CREATE a live draft — POST https://flamingo.dev.anvil.minga.io/dashboards/playground/draft body: {"html":"…","dataMode":"demo|live","examples":{"":""}, "scenario":"","dash":""} returns: 201 {"id","postUrl","eventsUrl","watchUrl","revision","warnings":[…]} `postUrl` carries your secret WRITE token — keep it. `watchUrl` carries only the read id: hand THAT to your human. A watch link can never overwrite the draft. 2. UPDATE the draft — POST the postUrl (https://flamingo.dev.anvil.minga.io/dashboards/playground/draft/) with the same body shape. Each POST re-validates and pushes the new HTML to every watcher over SSE, instantly. The "warnings" array is the same static check the editor runs — clear it before you hand off. 3. (optional) SUBSCRIBE yourself — GET https://flamingo.dev.anvil.minga.io/dashboards/playground/draft//events (text/event-stream). Emits `event: draft` with {revision,scenario,dash,html} on every update. 4. Test each state without connecting anything: GET https://flamingo.dev.anvil.minga.io/dashboards/playground/api/?scenario= returns the canned response (including 401/500) for that scenario. Scenarios: GET https://flamingo.dev.anvil.minga.io/dashboards/playground/scenarios ## Which data the preview runs against (`dataMode`) Set this when you CREATE the draft — it travels with the draft, so your human’s watchUrl renders the way you meant it to. Getting it wrong is what makes a working dashboard look broken: a page written against a live endpoint, previewed under demo examples, paints `no_example` in every panel. - "demo" (default) — canned example responses; any path without one returns 404 {"error":"no_example"}. The ONLY mode that can exercise the not-connected / empty / upstream-error states, and the only one that works with no session at all. - "live" — the viewer’s real /api, read as their own session. In "live" the preview reads AS THE HUMAN WATCHING, through the same GET-only broker a saved dashboard runs under — you are not granted anything, and a signed-out watcher just sees their own 401s. ### Composing the demo data (`examples`) Demo mode is NOT one fixed bundle. Send `"examples": {"":""}` to pick a variant per endpoint, and mix them freely — a full CRM beside a not-connected Gong, or a populated book whose engagement source is missing. Omit it and you get the server’s default mix (every endpoint at its richest variant), which is what you usually want for a first look. Every path and its available variants: GET https://flamingo.dev.anvil.minga.io/dashboards/playground/scenarios Pick deliberately: a dashboard that looks right only against its richest variant is a dashboard that breaks on the first customer whose data is thin. ## The deliverable The watchUrl is what you hand your human. Saving the dashboard into their own space is a signed-in human action (they click Save) — you cannot save on their behalf. Everything here is public, read-only, demo data. ## Endpoints a dashboard may fetch - GET /me — The signed-in user (email, groups, permissions). - GET /me/favourites — The signed-in user's pinned sidebar pages, in render order. - GET /connections/{slug}/check — Run one connector's read-only connection probe as the calling user. `ok` is the field to branch on; `details` are the facts the live call returned. Slugs come from the connection list — core names no vendor. - GET /me/home — Where `/` sends the signed-in user. `path` is null when they use the built-in landing page. - GET /crm/:instance/companies — Companies in a CRM instance, cursor-paginated (?limit, ?cursor). `:instance` is `customers` (external customers, monetary) or `users` (internal staff, admin-only). Use `parentCompanyId` to build a hierarchy — for Minga that is schools under their district — and `arr` for revenue on monetary instances. - GET /crm/:instance/contacts — Contacts in a CRM instance, cursor-paginated (?limit, ?cursor). `companyId` joins to /crm/:instance/companies. - GET /crm/:instance/deals — Deals in a CRM instance, cursor-paginated. Present only on MONETARY instances (`customers`); a non-monetary instance returns an empty list rather than pretending. - GET /crm/:instance/summary — Rollup for a CRM instance: entity totals, contacts by lifecycle stage, and — on monetary instances only — the deal pipeline by stage. `pipeline` and per-stage `amount` are OMITTED for non-monetary instances, so the response stays honest about what that instance tracks; deal COUNTS are always present. - GET /crm/:instance/sources — Each connector's sync state for this instance — last run, watermark, row counts. Use it to tell a viewer how fresh the numbers are instead of implying they are live. - GET /vendors/linear/issues — The signed-in user's assigned Linear issues. Add ?project= to instead get every issue in that project, enriched with priority, priorityLabel, assignee, and project — the shape a project board needs (these fields are absent on the plain assigned list). - GET /vendors/github/repos — The signed-in user's GitHub repositories (most recently pushed first). - GET /vendors/gong/calls — Recent Gong calls (trailing 30 days), read via the shared donor credential. The viewer must have opted in at /connections; Gong is not per-user. - GET /vendors/gong/calls/detailed — Recent Gong calls enriched with each call's participants (the "who's"). Same donor-credential + opt-in gating as /vendors/gong/calls; one request powers a filter-by-participant. - GET /vendors/gong/calls/:id/transcript — One call's transcript as speaker turns, read on demand via the donor credential. Each segment's speakerId joins to a participant from /vendors/gong/calls/detailed. - GET /vendors/intercom/conversations — One page of Intercom conversations as the signed-in user (pass ?cursor= from the previous page's nextCursor to continue). - GET /vendors/hubspot/companies — Companies read directly from the HubSpot portal. Unpaginated — the whole list in one response. For the normalised, paginated view with ARR and the district/school hierarchy, use /crm/customers/companies instead. - GET /vendors/hubspot/contacts — Contacts read directly from the HubSpot portal. Unpaginated. `companyId` joins to /vendors/hubspot/companies. - GET /vendors/hubspot/deals — Deals read directly from the HubSpot portal. Unpaginated. `companyId` is null for a deal not attached to a company. - GET /cx/owners — The CSM roster: owner id → name/email. Pass ?activeOnly=true to drop departed owners. Owner ids join to /cx/book?owner=. - GET /cx/book — One CSM's book of business, fully joined — the accounts assigned to ?owner=, each with health, silence, tier, ARR, enrolment and reach. This is the endpoint a CSM dashboard wants — one row per COMPANY. Per-district figures (sites tracked, silent sites, average health, at-risk count) are a group-by on `districtName` over these rows; the endpoint does not pre-aggregate them. Window via ?days= (default 30) or ?from=&to= (YYYY-MM-DD). ?basis=real (default) uses the actual HubSpot book assignment; ?basis=activity is a DEMO stand-in that groups by who logged a touch — "who logged the touch" is NOT "who owns the account", so a dashboard using it MUST label it as derived. The response echoes `basis` back so you can. - GET /cx/districts — District-grain health and cadence rollup across every district, for the window (?days= or ?from=&to=). One row per district, so ARR here is already at the right grain — unlike the per-school rows in /cx/book. Full response schemas + the error envelope: GET https://flamingo.dev.anvil.minga.io/dashboards/playground/endpoints (or read the authoring guide below, which embeds the same contract). ## Authoring guide Flamingo dashboard authoring A dashboard is a single self-contained HTML document. To read data, fetch the flamingo API at the injected base: const API = window.FLAMINGO_API_BASE || '/api'; const res = await fetch(API + '/me/favourites'); In the playground the base is /dashboards/playground/api and responses come from the selected scenario. Once a human saves the dashboard to their ~name space, the base is /api and the identical code hits their real, authenticated data. Shape of a dashboard: a dashboard does NOT have to fetch anything. A self-contained dashboard that ships its data embedded in the HTML is a first-class, fully legitimate shape — a baked-in snapshot renders instantly and never 500s on a vendor a viewer has not connected. It is not "doing it wrong"; the validator only nudges you to keep the door open to live data. So if the dashboard ALSO wants live data, still read window.FLAMINGO_API_BASE (|| '/api') and fetch — then the same file works live once saved, while any embedded snapshot stays as the fallback. Embedding data and fetching live data are complementary, not a choice you can get wrong. Endpoints and their response schemas: call list_endpoints, or over plain HTTP GET /dashboards/playground/endpoints. Available paths: /me, /me/favourites, /connections/{slug}/check, /me/home, /crm/:instance/companies, /crm/:instance/contacts, /crm/:instance/deals, /crm/:instance/summary, /crm/:instance/sources, /vendors/linear/issues, /vendors/github/repos, /vendors/gong/calls, /vendors/gong/calls/detailed, /vendors/gong/calls/:id/transcript, /vendors/intercom/conversations, /vendors/hubspot/companies, /vendors/hubspot/contacts, /vendors/hubspot/deals, /cx/owners, /cx/book, /cx/districts Error envelope: every non-2xx response is { error: , hint?: }. Always handle non-200 — notably a 401 whose error is "_not_connected", which means the viewer must connect that vendor first. Each endpoint above lists the exact error codes it can return; code against those, not against a guess. Scenarios (see list_scenarios, or GET /dashboards/playground/scenarios) are a PLAYGROUND-ONLY testing aid. The ?scenario= query param selects example data in the playground; never put it in your dashboard code — saved dashboards just call /api with no scenario param. Sharing for review: call create_dashboard_draft with your HTML (or, with no MCP client, POST {"html":…} to /dashboards/playground/draft) — it validates the draft and returns a link in one step (optionally validate_dashboard first). Your human opens the link to preview (rendered against a scenario) and Save. Saving is a signed-in human action — POST the HTML as text/html to /~//; an unauthenticated save returns 401 { error: "unauthorized" }. The scenario list and its default come from GET /dashboards/playground/scenarios — the authoritative source, rather than a copy here that can drift. More docs (all public, no login): the human-readable API reference (Scalar) at /docs, and the credential-free playground — page at /dashboards/playground, agent protocol at /dashboards/playground/llms.txt (which embeds this same guide).