Integrations

REST API

Read and write everything in Flusterduck with a secret key. Query scores, issues, heatmaps; manage sites, alerts, members.


Every piece of data in Flusterduck is accessible through two HTTP endpoints. The query endpoint reads data. The manage endpoint writes it. Both accept a secret key in the Authorization header, so you can call them from scripts, CI pipelines, internal tools, or any language with an HTTP client.

Authentication

Create a secret key in Settings > API Keys. It starts with fd_sec_. Pass it as a Bearer token:

Authorization: Bearer fd_sec_your_key_here

The key must have the right scopes. A key with query:read can read scores, issues, alerts, deploys, and raw data. A key with manage:write can create sites, update alert rules, triage issues, and manage members. You set scopes when creating the key.

MCP keys (fd_mcp_) also work for read operations.

Base URL

https://rhwhnkrqjlzyzcdhvyky.supabase.co/functions/v1

All routes are under /query (read) or /manage (write).

Reading data

All query routes accept site_id as a required query parameter.

Scores

GET /query/scores?site_id={id}

Returns confusion scores for every page on the site, sorted by score descending.

Issues

GET /query/issues?site_id={id}
GET /query/issues?site_id={id}&status=open&limit=20
GET /query/issues/{issue_id}?site_id={id}

List or fetch individual UX issues. Filter by status: open, triaged, in_progress, verified, resolved, ignored.

Alerts

GET /query/alerts?site_id={id}
GET /query/alerts?site_id={id}&status=active
GET /query/alerts/{alert_id}?site_id={id}

Deploys

GET /query/deploys?site_id={id}
GET /query/deploys/{deploy_id}?site_id={id}

Returns deploys with confusion_before and confusion_after scores, plus related issues.

Page detail

GET /query/page?site_id={id}&page=/checkout

Full detail for a single page: score, history, top elements, open issues, recent deploys, active alerts.

Elements

GET /query/elements?site_id={id}&page=/checkout

Top elements by friction signal count on a page. Includes dominant signal type, affected users, and recommendation.

Element heatmap

GET /query/element-heatmap?site_id={id}&page=/checkout&selector=button.submit

Returns up to 300 click positions on a single element, collected from rage_click, dead_click, and disabled_element_attempt signals. Both page and selector are required.

Each point uses a 0 to 1 coordinate system relative to the element's bounding box. rx: 0, ry: 0 is the top-left corner. rx: 1, ry: 1 is the bottom-right. No pixel coordinates ever leave the browser.

The SDK records these positions in two ways. Newer builds emit hx/hy as integers from 0 to 1000 (divided by 1000 on the server to get the 0-1 fraction). Older builds emit a center-relative offset (click_dx/click_dy) plus the element dimensions (el_w/el_h), and the server recovers the fraction from those. You don't need to worry about which format your SDK uses. The API always returns normalized rx/ry values.

{
  "data": {
    "page": "/checkout",
    "selector": "button.submit",
    "count": 47,
    "points": [
      { "rx": 0.512, "ry": 0.483, "signal": "rage_click" },
      { "rx": 0.891, "ry": 0.102, "signal": "dead_click" },
      { "rx": 0.334, "ry": 0.721, "signal": "disabled_element_attempt" }
    ]
  },
  "error": null
}
FieldTypeDescription
rxnumberHorizontal position within the element, 0 (left edge) to 1 (right edge)
rynumberVertical position within the element, 0 (top edge) to 1 (bottom edge)
signalstringThe signal type that generated this point

To render a heatmap overlay, multiply rx by the element's rendered width and ry by its rendered height. The positions stay accurate at any zoom level or screen size because they're fractions, not pixels.

Page heatmap

GET /query/page-heatmap?site_id={id}&page=/checkout

Returns up to 500 friction positions across the entire viewport for a page. Same signal types as the element heatmap (rage_click, dead_click, disabled_element_attempt), but coordinates are relative to the viewport instead of a single element.

The SDK records vx/vy as integers from 0 to 1000 (the click's position as a fraction of the viewport width and height). The server normalizes these to 0-1 before returning them.

{
  "data": {
    "page": "/checkout",
    "count": 183,
    "points": [
      { "vx": 0.724, "vy": 0.312, "signal": "rage_click", "el": "button.submit" },
      { "vx": 0.501, "vy": 0.887, "signal": "dead_click", "el": "a.terms-link" },
      { "vx": 0.223, "vy": 0.145, "signal": "dead_click" }
    ]
  },
  "error": null
}
FieldTypeDescription
vxnumberHorizontal position in the viewport, 0 (left) to 1 (right)
vynumberVertical position in the viewport, 0 (top) to 1 (bottom)
signalstringThe signal type that generated this point
elstring or absentCSS selector of the element that was clicked, when available

Points without an el field come from signals where the element couldn't be identified. This is rare but possible with dynamically removed DOM nodes.

Friction map

GET /query/journeys/friction?site_id={id}
GET /query/journeys/friction?site_id={id}&min_friction_weight=5&signal_type=rage_click&limit=500

Returns page-to-page navigation edges weighted by the friction signals that occurred along them. This powers the friction map panel in the dashboard, showing where users hit trouble as they move through your site.

The endpoint joins session navigation paths with signals that qualify as edge decorators (the friction-relevant subset of all signal types). Each edge accumulates a friction_weight from the weight and confidence of every qualifying signal on either the source or destination page.

ParameterDefaultDescription
limit250Max sessions to analyze (1 to 1000)
min_friction_weight1Minimum combined friction weight for an edge to appear (0 to 1000)
signal_typeallFilter to a single signal type, e.g. rage_click
{
  "data": {
    "site_id": "your-site-id",
    "edges": [
      {
        "from": "/pricing",
        "to": "/checkout",
        "sessions": 34,
        "friction_weight": 127.4,
        "signals": {
          "rage_click": 18,
          "dead_click": 12,
          "form_restart": 4
        },
        "examples": [
          {
            "session_id": "sess_abc123",
            "page": "/checkout",
            "signal_type": "rage_click",
            "element_selector": "button.submit",
            "occurred_at": "2026-06-21T14:32:01Z"
          }
        ]
      },
      {
        "from": "/checkout",
        "to": "/checkout",
        "sessions": 12,
        "friction_weight": 89.2,
        "signals": {
          "form_restart": 9,
          "error_encounter": 3
        },
        "examples": []
      }
    ]
  },
  "error": null
}
FieldTypeDescription
fromstringSource page path
tostringDestination page path. Same as from when users reload or loop on one page.
sessionsnumberDistinct sessions that traversed this edge with friction
friction_weightnumberCombined weight of all qualifying signals, rounded to one decimal
signalsobjectCount of each signal type observed on this edge
examplesarrayUp to 5 concrete signal instances for debugging. Each has session_id, page, signal_type, element_selector, and occurred_at.

Results are sorted by friction_weight descending and capped at 100 edges. Edges with a friction_weight below min_friction_weight are excluded before sorting.

GET /query/trends?site_id={id}&days=7
GET /query/trends?site_id={id}&page=/checkout&days=30

Score history over time. Defaults to 7 days.

Revenue

GET /query/revenue?site_id={id}

Revenue at risk across all open issues, based on the revenue config you set in Settings.

Recommendations

GET /query/recommendations?site_id={id}

Ranked list of fix recommendations derived from the current issue set.

Raw data

GET /query/raw?site_id={id}&table=signals&limit=100&sort=occurred_at&order=desc
GET /query/raw?site_id={id}&table=events&limit=50
GET /query/raw?site_id={id}&table=sessions
GET /query/raw?site_id={id}&table=page_scores
GET /query/raw?site_id={id}&table=ux_issues

Direct table access with sorting and pagination. Available tables: events, signals, sessions, page_scores, score_history, ux_issues, deploys, alerts.

CSV export

GET /query/export/events.csv?site_id={id}

Downloads raw events as CSV.

MCP context

GET /query/mcp/context?site_id={id}

A single-call summary designed for AI assistants: top scores, open issues, recent deploys, active alerts, and recommendations in one response.

Writing data

Manage routes use POST, PATCH, or DELETE. All require a key with manage:write scope.

Sites

POST   /manage/sites        { name, url }
PATCH  /manage/sites/{id}   { name, url }
DELETE /manage/sites/{id}

Alert rules

GET    /manage/alert-rules?site_id={id}
POST   /manage/alert-rules  { site_id, trigger_type, threshold, channels, config }
PATCH  /manage/alert-rules/{id}  { threshold, channels, enabled }
DELETE /manage/alert-rules/{id}

Trigger types: spike, anomaly, new_page, trend, co_occurrence, positive, budget. Channels: email, slack, webhook, mcp, pagerduty.

Issues

PATCH /manage/issues/{id}  { status, assigned_to, severity }

Triage, assign, and resolve issues.

Members

GET    /manage/members
POST   /manage/members/invite  { email, role }
PATCH  /manage/members/{id}    { role }
DELETE /manage/members/{id}

Roles: owner, admin, member, viewer.

Webhooks

GET    /manage/webhooks?site_id={id}
POST   /manage/webhooks      { site_id, url, events, secret }
PATCH  /manage/webhooks/{id} { url, events, enabled }
DELETE /manage/webhooks/{id}

SDK config

GET   /manage/sdk-config?site_id={id}
PATCH /manage/sdk-config/{environment}  { revenue_config, ... }

Integrations

GET    /manage/integrations?site_id={id}
POST   /manage/integrations  { site_id, provider, config }
DELETE /manage/integrations/{id}

Providers: slack, pagerduty, linear, github.

API keys

GET    /manage/keys?site_id={id}
POST   /manage/keys  { site_id, key_type, scopes }
DELETE /manage/keys/{id}

Key types: secret, mcp. Scopes: ingest:write, query:read, manage:write, mcp:read, webhook:write.

Rate limits

EndpointLimit
query100 RPM per org
manage30 RPM per org
guide200 RPM per site
ingest10,000 RPM per publishable key

Response format

All responses wrap data in { "data": { ... }, "error": null }. On error: { "data": null, "error": { "code": "...", "message": "..." } } with the appropriate HTTP status code.