# Platform API Your servers ask Misata for a dataset and get back tables whose stated numbers hold exactly, with an answer key that proves them. Use it to fill empty states and demos in your product, or to check that numbers your AI computes are right. Base URL: `https://api.misata.studio/v1`. Every request sends a Misata key. ## Get a key In Studio, open **API** in the sidebar and create a key. It is shown once, and the examples on that page carry it. Keep it on your server: anyone holding it builds datasets on your credits. Revoke it there at any time; access ends at once. The same page keeps your standing instructions, your webhook secret and the datasets your app has made. ```bash export MISATA_KEY=msk_... ``` ## Start a dataset Describe it in words, give your own tables with the numbers they must hold, or name a built-in business. ```bash curl https://api.misata.studio/v1/datasets \ -H "Authorization: Bearer $MISATA_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: demo-acme-42" \ -d '{ "prompt": "A B2B SaaS with three plans over the last two years. MRR grows from $50,000 to $200,000, with 2% monthly churn spiking to 5% in Q3 2025.", "webhook_url": "https://your.app/hooks/misata", "external_user": "acme-42" }' ``` It answers `202` at once with the dataset's `id` and `status: "running"`; the build carries on. A retry with the same `Idempotency-Key` within a day returns the same dataset and is not charged again. | Field | What it is | |---|---| | `prompt` | The data in words. Every number in it is held exactly or said not held. | | `instructions` | How this dataset should be made, beside what it is about ("company names fictional", "amounts in euros"). Read as requirements: each is held or said not held. | | `schema` | Your own tables (`{"tables": [...]}`) with `numbers` (totals, levels, rates, shares by month) and `window`. | | `ddl` | Your own schema as SQL DDL, Prisma, dbt, DBML and more; its numbers go in `schema.numbers`. | | `template` | A built-in business: `saas_sales`, `ecommerce`, `fintech`, `healthcare`, `aml`, `iot_telemetry`, `education`, and more. | | `story` | A template's stated months, rates and events. | | `as_of` | The data's last day, `YYYY-MM-DD`; today when not given. | | `seed` | The same request and seed make the same data, byte for byte. | | `webhook_url` | An `https` address told when the dataset is ready or failed. | | `external_user` | Your own id for whoever it is for; returned with the dataset, never read. | ## Follow it Poll `GET /v1/datasets/{id}`, or stream its steps as server-sent events: ```bash curl -N https://api.misata.studio/v1/datasets/$ID/events -H "Authorization: Bearer $MISATA_KEY" ``` Events are numbered; reconnect with `?after=` or `Last-Event-ID` to pick up where you left off. When it is ready: ```json { "id": "8e4de4426abe", "status": "ready", "verified": true, "tables": {"accounts": 168, "subscriptions": 269, "invoices": 2728}, "stated_numbers": {"held": 3, "total": 3, "not_held": []}, "realism_score": 94.4 } ``` `verified` is true only when every stated number and every rule held. Anything not held is listed in `not_held`, and the dataset says so. Nothing is ever reported as met when it was not. ## Take the data Pick the way that fits your product. **1. Files.** One table as Parquet or CSV, or every table in one export: ```bash curl https://api.misata.studio/v1/datasets/$ID/tables/subscriptions?format=csv -H "Authorization: Bearer $MISATA_KEY" curl -X POST https://api.misata.studio/v1/datasets/$ID/exports -H "Authorization: Bearer $MISATA_KEY" \ -H "Content-Type: application/json" -d '{"format": "sql", "dialect": "postgres"}' ``` An export returns a link valid for 15 minutes that needs no key. Formats include csv, parquet, sql (Postgres, MySQL, SQL Server, Snowflake, BigQuery, ClickHouse), duckdb, sqlite, dbt and the ClickHouse pack. **2. Into your database.** Misata loads Postgres or ClickHouse directly. Plan first: it checks the host and says what would be loaded, without touching it. ```bash curl -X POST https://api.misata.studio/v1/datasets/$ID/load -H "Authorization: Bearer $MISATA_KEY" \ -H "Content-Type: application/json" \ -d '{"target": "postgres", "url": "postgres://loader:...@db.your.app:5432/analytics", "schema": "demo"}' ``` Send `"apply": true` to load it. Postgres gets every table in one transaction with its foreign keys declared and enforced, so a failed load leaves nothing behind. The rows and every key are read back and reported. ClickHouse gets MergeTree tables from typed Parquet. A load writes only the dataset's own tables, in the schema or database you name. It replaces them only with `"replace": true`. The connection string is used for that one load and is never stored or logged. The database must be reachable on the public internet over an encrypted connection; private networks are refused. Give Misata a user that can write only to the schema it loads. **3. Query Misata directly.** For an empty state, your dashboards can query the dataset where it is, with nothing copied: ```bash curl -X POST https://api.misata.studio/v1/datasets/$ID/query -H "Authorization: Bearer $MISATA_KEY" \ -H "Content-Type: application/json" -d '{"sql": "SELECT plan_name, count(*) AS subscriptions, sum(mrr) AS mrr FROM subscriptions GROUP BY 1 ORDER BY 3 DESC"}' ``` One read-only `SELECT` (DuckDB SQL) at a time, over the dataset's own tables, within a time and memory limit. ## Instructions for every dataset Say once what every dataset on your account should keep to, and each new one reads it, from Studio or your app: ```bash curl -X PUT https://api.misata.studio/v1/instructions -H "Authorization: Bearer $MISATA_KEY" \ -H "Content-Type: application/json" -d '{"text": "Company names fictional, never real brands. Amounts in US dollars."}' ``` They are read with each new design and build as requirements, held or said not held, and kept on the dataset (`instructions.standing` beside `instructions.request`). Changing them changes datasets made from then on. An empty `text` removes them. A revision of an existing dataset changes only what it names and reads neither. ## Change it A ready dataset is changed by a revision: a new version, checked before it is kept. The first is never touched. Send one change: | Field | What it changes | |---|---| | `instruction` | What to change, in words, as your user said it ("far fewer accounts", "refunds rarer in December"). | | `story` | A template dataset's stated months, rates and events: read it from `GET /v1/datasets/{id}/story`, edit, send. | | `curves` | The curves the data follows: `[{"metric", "points": {"YYYY-MM": value}}]`; `[]` removes them. | | `controls` | Named controls (how strongly a cause acts, staff at peak, planted problems), as `GET /v1/datasets/{id}/controls` lists them with their ranges. | ```bash curl https://api.misata.studio/v1/datasets/$ID/revisions -H "Authorization: Bearer $MISATA_KEY" \ -H "Content-Type: application/json" -d '{"instruction": "There are far too many accounts; make it about 120"}' ``` It answers `202` at once with the revision (`rev_...`). Follow it with `GET /v1/datasets/$ID/revisions/$REV`: ```json { "id": "rev_19b052bb89c8", "of": "8e4de4426abe", "status": "ready", "understood": "Fewer accounts", "applied": ["accounts now has 120 rows"], "dataset_id": "c41a7d0e9b12" } ``` | `status` | Meaning | |---|---| | `reading` | A correction in words is being read. | | `building` | Its new version is being made; `dataset_id` names it. | | `ready` | The new version is ready and verified as any dataset is. | | `unchanged` | Nothing in the data needed to change; `problems` says why. Not charged. | | `discarded` | The new version did not verify, so it was not kept. Not charged. | | `failed`, `interrupted` | It did not finish; `message` says why. Send it again. | `webhook_url` on a revision posts `dataset.revision.` when it ends, signed as a build's webhook is. An `Idempotency-Key` makes a retried revision return the first. `GET /v1/datasets/{id}/revisions` lists a dataset's revisions and `GET /v1/datasets/{id}/versions` every version in its family, oldest first. Every version also opens in Studio, to read, check and edit by hand. ## Check your numbers Every dataset has an answer key: each number it is held to, with the SQL that reads it and its value by month. ```bash curl https://api.misata.studio/v1/datasets/$ID/answer-key -H "Authorization: Bearer $MISATA_KEY" curl "https://api.misata.studio/v1/datasets/$ID/questions?n=20" -H "Authorization: Bearer $MISATA_KEY" ``` Check what your product computed, either the SQL it wrote or the numbers it shows: ```bash curl -X POST https://api.misata.studio/v1/datasets/$ID/check -H "Authorization: Bearer $MISATA_KEY" \ -H "Content-Type: application/json" -d '{"sql": "SELECT strftime(issued_at, '\''%Y-%m'\'') m, sum(amount_usd) FROM invoices GROUP BY 1", "kpi": "mrr"}' ``` A wrong one is told what it computes instead (billed revenue for MRR, churn over subscriptions instead of accounts), with the one change that makes it right, proved by running it. ## Webhooks When a dataset is ready or failed, Misata posts to your `webhook_url`: ```json {"type": "dataset.ready", "dataset": {"id": "...", "status": "ready", "verified": true, "external_user": "acme-42"}} ``` Each post is signed. Read your secret from `GET /v1/webhook-secret`. Check the `Misata-Signature` header, written `t=,v1=`, where `v1` is HMAC-SHA256 of `"."` with the secret. Refuse a `t` more than five minutes old. ```python import hmac, hashlib, time def verified(secret: str, header: str, body: bytes) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) mac = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest() return hmac.compare_digest(mac, parts["v1"]) and abs(time.time() - int(parts["t"])) < 300 ``` Misata tries three times, only over `https`, and follows no redirect. ## Errors and limits | Status | Meaning | |---|---| | `401` | No key, or the key is not valid or was revoked. | | `402` | The account's credits do not cover the build; nothing was charged. | | `404` | No dataset with that id on this account. | | `409` | The dataset is still running, or failed; or a change it cannot take (a story on a dataset with no template). | | `422` | A field cannot be held; `detail.field` names it and `detail.message` says what to write. | | `429` | The account is at its limit; wait `detail.retry_after` seconds. | Builds cost what they cost in Studio, from the same credits. A key sees only its own account's datasets.