Skip to content
[Developers]v1.0.0Pro

The Playfair API

Read your dashboards, answers and saved questions from your own tools. JSON over HTTPS with a Bearer key. It is read-only by construction: nothing you call here can write to Playfair or to your databases.

Read-onlyGET endpoints, plus re-running a saved question through the same safe query engine.
Scoped keysEach key carries only the scopes you give it and can be revoked in one click.
Signed webhooksAlert, report and failure events, HMAC-SHA256 signed and retried.
Base URLhttps://playfair.asrar.software/api/v1
[Basics]

Authentication

Workspace admins create keys under API & webhooks in the app. A key is shown once; Playfair stores only its SHA-256 hash. Send it in the Authorization header of every request.

Shell
curl https://playfair.asrar.software/api/v1/dashboards \
  -H "Authorization: Bearer pf_live_…"
ScopeGrants
dashboards:readList dashboards and read their tiles with the latest results.
answers:readRead answers (headline, SQL, chart, rows) and list saved questions.
saved_questions:runExecute a saved question’s SQL against its source (read-only) and return fresh rows.

A key acts as the member who created it: it sees what they can see. If that member leaves the workspace or is removed, the key stops working. The API needs the workspace to be on Pro.

Rate limits

Each key may send 60 requests per minute (the default). Every response carries the current window; a 429 tells you how many seconds to wait.

Response headers
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1790496060

Errors

Errors use one JSON shape: a human sentence you can show, and a stable code you can branch on.

401 Unauthorized
{
  "message": "This API key is invalid or has been revoked.",
  "code": "UNAUTHORIZED"
}
401UNAUTHORIZEDMissing, invalid or revoked key
402PLAN_LIMITThe workspace is not on Pro
403FORBIDDENMissing scope, suspended workspace or blocked network
404NOT_FOUNDNot found, or not visible to this key
422UNPROCESSABLEThe saved question cannot run (never answered, query error)
429RATE_LIMITEDToo many requests in this minute
[Dashboards]

List dashboards

get/dashboardsdashboards:read

Dashboards visible to the member who created the key, most recently updated first.

ParameterInDetails
pagequeryinteger, default 1
pageSizequeryinteger, default 25, max 100
Request
curl https://playfair.asrar.software/api/v1/dashboards \
  -H "Authorization: Bearer $PLAYFAIR_API_KEY"
200 OK
{
  "data": [
    {
      "id": "2b3c4d5e-6f70-4812-93a4-b5c6d7e8f901",
      "name": "E-commerce overview",
      "description": "Revenue, orders and returns for the store",
      "visibility": "team",
      "tileCount": 10,
      "refreshPolicy": {
        "mode": "interval",
        "intervalMinutes": 60
      },
      "createdAt": "2026-03-02T09:12:00.000Z",
      "updatedAt": "2026-09-26T17:40:11.000Z",
      "url": "https://playfair.asrar.dev/app/acme/dashboards/2b3c4d5e-6f70-4812-93a4-b5c6d7e8f901"
    }
  ],
  "page": 1,
  "pageSize": 25,
  "total": 1
}

Errors: 401 missing, invalid or revoked API key · 402 the workspace is not on the Pro plan · 403 the key lacks the required scope, or the workspace is suspended · 429 rate limit reached.

[Dashboards]

Get a dashboard

get/dashboards/{id}dashboards:read

A dashboard with its filters and every tile: kind, layout, chart spec, SQL and the latest cached result (no query is run).

ParameterInDetails
idrequiredpathDashboard id · uuid
Request
curl https://playfair.asrar.software/api/v1/dashboards/2b3c4d5e-6f70-4812-93a4-b5c6d7e8f901 \
  -H "Authorization: Bearer $PLAYFAIR_API_KEY"
200 OK
{
  "id": "2b3c4d5e-6f70-4812-93a4-b5c6d7e8f901",
  "name": "E-commerce overview",
  "description": "Revenue, orders and returns for the store",
  "visibility": "team",
  "refreshPolicy": {
    "mode": "interval",
    "intervalMinutes": 60
  },
  "createdAt": "2026-03-02T09:12:00.000Z",
  "updatedAt": "2026-09-26T17:40:11.000Z",
  "url": "https://playfair.asrar.dev/app/acme/dashboards/2b3c4d5e-6f70-4812-93a4-b5c6d7e8f901",
  "filters": [
    {
      "key": "region",
      "name": "Region",
      "kind": "select",
      "defaultValue": null
    }
  ],
  "tiles": [
    {
      "id": "7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
      "kind": "answer",
      "title": "Monthly revenue",
      "subtitle": "Paid orders",
      "status": "ready",
      "layout": {
        "x": 0,
        "y": 3,
        "w": 8,
        "h": 5
      },
      "answerId": null,
      "savedQuestionId": null,
      "chart": {
        "type": "line",
        "x": {
          "field": "month"
        },
        "y": {
          "field": "revenue",
          "format": "currency"
        }
      },
      "sql": "select date_trunc('month', order_date)::date as month, sum(total) as revenue from orders where status = 'paid' group by 1 order by 1",
      "markdown": null,
      "label": null,
      "lastRefreshedAt": "2026-09-27T06:00:04.000Z",
      "result": {
        "columns": [
          {
            "name": "month",
            "type": "date"
          },
          {
            "name": "revenue",
            "type": "number"
          }
        ],
        "rows": [
          {
            "month": "2026-07-01",
            "revenue": 412880.4
          },
          {
            "month": "2026-08-01",
            "revenue": 398112.9
          }
        ],
        "rowCount": 2,
        "truncated": false,
        "refreshedAt": "2026-09-27T06:00:04.000Z"
      }
    }
  ]
}

Errors: 401 missing, invalid or revoked API key · 402 the workspace is not on the Pro plan · 403 the key lacks the required scope, or the workspace is suspended · 404 dashboard not found (or not visible to this key) · 429 rate limit reached.

[Answers]

Get an answer

get/answers/{id}answers:read

The headline, narrative, confidence, caveats, SQL, chart spec and the stored rows of an answer.

ParameterInDetails
idrequiredpathAnswer id · uuid
Request
curl https://playfair.asrar.software/api/v1/answers/9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a \
  -H "Authorization: Bearer $PLAYFAIR_API_KEY"
200 OK
{
  "id": "9e8d7c6b-5a49-4382-9170-6f5e4d3c2b1a",
  "question": "Monthly revenue this year",
  "status": "ready",
  "source": {
    "id": "f291b88d-8e0f-4078-8fab-be512cac4979",
    "name": "Sample store",
    "kind": "postgres"
  },
  "headline": "Revenue reached €3.4M so far this year",
  "headlineValue": {
    "value": 3412880.4,
    "formatted": "€3.4M"
  },
  "narrative": "Revenue grew steadily except in April, when Région Sud fell 45%.",
  "sql": "select date_trunc('month', o.order_date)::date as month, sum(o.total) as revenue from orders o where o.status = 'paid' and o.order_date >= '2026-01-01' group by 1 order by 1",
  "sqlEdited": false,
  "chart": {
    "type": "line",
    "x": {
      "field": "month"
    },
    "y": {
      "field": "revenue",
      "format": "currency"
    }
  },
  "confidence": "high",
  "confidenceReasons": [
    "Uses the governed metric Net revenue"
  ],
  "caveats": [
    "Excludes 12 orders with no date"
  ],
  "usedMetrics": [
    {
      "key": "net_revenue",
      "name": "Net revenue"
    }
  ],
  "rowCount": 9,
  "durationMs": 184,
  "error": null,
  "createdAt": "2026-09-27T08:14:03.000Z",
  "url": "https://playfair.asrar.dev/app/acme/answers/9e8d7c6b-5a49-4382-9170-6f5e4d3c2b1a",
  "result": {
    "columns": [
      {
        "name": "month",
        "type": "date"
      },
      {
        "name": "revenue",
        "type": "number"
      }
    ],
    "rows": [
      {
        "month": "2026-07-01",
        "revenue": 412880.4
      },
      {
        "month": "2026-08-01",
        "revenue": 398112.9
      }
    ],
    "rowCount": 2,
    "truncated": false,
    "refreshedAt": "2026-09-27T06:00:04.000Z"
  }
}

Errors: 401 missing, invalid or revoked API key · 402 the workspace is not on the Pro plan · 403 the key lacks the required scope, or the workspace is suspended · 404 answer not found (or not visible to this key) · 429 rate limit reached.

[Saved questions]

List saved questions

get/saved-questionsanswers:read

Shared saved questions of the workspace (and the key creator’s own).

ParameterInDetails
pagequeryinteger, default 1
pageSizequeryinteger, default 25, max 100
Request
curl https://playfair.asrar.software/api/v1/saved-questions \
  -H "Authorization: Bearer $PLAYFAIR_API_KEY"
200 OK
{
  "data": [
    {
      "id": "0f1e2d3c-4b5a-4968-8778-695a4b3c2d1e",
      "name": "Weekly revenue by region",
      "question": "Revenue by region last week",
      "tags": [
        "weekly",
        "regions"
      ],
      "sourceId": "f291b88d-8e0f-4078-8fab-be512cac4979",
      "lastAnswerId": "9e8d7c6b-5a49-4382-9170-6f5e4d3c2b1a",
      "lastRunAt": "2026-09-22T07:00:00.000Z",
      "updatedAt": "2026-09-22T07:00:00.000Z"
    }
  ],
  "page": 1,
  "pageSize": 25,
  "total": 1
}

Errors: 401 missing, invalid or revoked API key · 402 the workspace is not on the Pro plan · 403 the key lacks the required scope, or the workspace is suspended · 429 rate limit reached.

[Saved questions]

Run a saved question

post/saved-questions/{id}/runsaved_questions:run

Executes the saved question's SQL (its override, or the SQL of its latest answer) against its source through Playfair's read-only query engine and returns fresh rows. Nothing is written to your database; the run is recorded in the query log with origin api.

ParameterInDetails
idrequiredpathSaved question id · uuid
limitqueryinteger, default 1000, max 5000
Request
curl -X POST https://playfair.asrar.software/api/v1/saved-questions/5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d/run \
  -H "Authorization: Bearer $PLAYFAIR_API_KEY"
200 OK
{
  "savedQuestionId": "0f1e2d3c-4b5a-4968-8778-695a4b3c2d1e",
  "name": "Weekly revenue by region",
  "question": "Revenue by region last week",
  "sql": "select r.name as region, sum(o.total) as revenue from orders o join regions r on r.id = o.region_id where o.status = 'paid' group by 1 order by 2 desc",
  "columns": [
    {
      "name": "region",
      "type": "string"
    },
    {
      "name": "revenue",
      "type": "number"
    }
  ],
  "rows": [
    {
      "region": "Île-de-France",
      "revenue": 118220.1
    },
    {
      "region": "Région Sud",
      "revenue": 36214.5
    }
  ],
  "rowCount": 2,
  "truncated": false,
  "durationMs": 92,
  "cached": false,
  "costClass": "light",
  "queryRunId": "4d5e6f70-8192-4a3b-9c4d-5e6f7a8b9c0d",
  "computedAt": "2026-09-27T08:20:11.000Z"
}

Errors: 401 missing, invalid or revoked API key · 402 the workspace is not on the Pro plan · 403 the key lacks the required scope, or the workspace is suspended · 404 saved question not found (or not visible to this key) · 422 the saved question has never run, or the query failed (invalid SQL, timeout) · 429 rate limit reached.

[Webhooks]

Outbound webhooks

Add HTTPS endpoints under API & webhooks and choose the events they receive. Playfair sends a JSON POST; any 2xx acknowledges it. Anything else is retried after 1 min, 5 min, 30 min, 2 h. The event id stays the same across retries, so you can de-duplicate. Private and loopback addresses are refused.

alert.triggeredA metric or saved question crossed an alert condition.
schedule.sentA scheduled report was delivered to its recipients.
answer.failedA question could not be answered (query error, timeout).
POST · alert.triggered
{
  "id": "evt_5f0c8b7e2a1d4c3b9e8f7a6b5c4d3e2f",
  "type": "alert.triggered",
  "createdAt": "2026-09-27T08:00:00.000Z",
  "workspace": {
    "id": "926aaf8c-7de5-412c-b0dd-25152d975149",
    "slug": "acme",
    "name": "Acme"
  },
  "data": {
    "alertId": "6f1c2a8e-4b0d-4a51-9f0e-3c8d2b7a1e55",
    "name": "Revenue in Région Sud below €40k/week",
    "condition": {
      "kind": "threshold",
      "op": "<",
      "value": 40000
    },
    "value": 36214.5,
    "previous": 52480.1,
    "link": "https://playfair.asrar.dev/app/acme/schedules?alert=6f1c2a8e"
  }
}
X-Playfair-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
X-Playfair-EventThe event type, e.g. alert.triggered
X-Playfair-DeliveryUnique id of this attempt
X-Playfair-TimestampUnix seconds, same as t= in the signature

Verifying signatures

Each endpoint has its own signing secret (whsec_…). Compute an HMAC-SHA256 of <t>.<raw body> with it and compare in constant time. Reject timestamps older than five minutes.

verify.js
import crypto from "node:crypto";

// rawBody: the request body as received (a string, before JSON.parse)
export function verifyPlayfairSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  // Reject events older than 5 minutes (replay protection)
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Verify against the raw body. Parsing and re-serialising the JSON changes the bytes and breaks the signature.

[Reference]

OpenAPI

The machine-readable description (OpenAPI 3.1) lives at /api/v1/openapi.json. Import it into Postman, Insomnia or a client generator; this page is rendered from the same document.