# The API Your servers ask Misata for a dataset and get back connected tables whose stated numbers hold exactly. Use it to fill empty states and demos in your product, give each user a sandbox, or build test data on demand. Base URL: `https://api.demo.misata.studio/v1`. Every request sends a Misata key. ## Get a key In [Studio](/app), open **API** in the sidebar and create a key. It is shown once. Keep it on your server: anyone holding it builds datasets on your account. Revoke it there at any time and access ends at once. ```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.demo.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"`, and 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"). | | `schema` | Your own tables (`{"tables": [...]}`) with the `numbers` they must hold and a `window` of months. | | `ddl` | Your own schema as SQL DDL, Prisma, dbt, DBML and more. | | `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. | | `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 Ask `GET /v1/datasets/{id}?wait=60`: the answer comes as soon as the dataset is ready or failed, or after 60 seconds with it still `running`, so a loop of plain requests follows it with no polling interval to tune. Or stream its steps as server-sent events: ```bash curl -N https://api.demo.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": []} } ``` `verified` is true only when every stated number and every rule held. Anything not held is listed in `not_held`. ## Take the data **Files.** One table as Parquet or CSV, or every table in one export: ```bash curl "https://api.demo.misata.studio/v1/datasets/$ID/tables/subscriptions?format=csv" -H "Authorization: Bearer $MISATA_KEY" curl -X POST https://api.demo.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 salesforce and hubspot (each CRM's own objects with a loader script; see [Salesforce and HubSpot](/docs/learn/crm)). **Into your database.** Misata loads PostgreSQL or ClickHouse directly. Plan first: it checks the host and says what would be loaded, without touching it. ```bash curl -X POST https://api.demo.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. PostgreSQL gets every table in one transaction with its foreign keys enforced, so a failed load leaves nothing behind. A load writes only the dataset's own tables, in the schema you name, and replaces them only with `"replace": true`. The connection string is used for that one load and never stored or logged. The database must be reachable on the public internet over an encrypted connection; give Misata a user that can write only to the schema it loads. **Into your CRM.** Misata loads HubSpot or Salesforce directly, from the files its CRM export carries: companies, contacts, deals and their activities, each linked as the dataset links them. Plan first, then send `"apply": true`: ```bash curl -X POST https://api.demo.misata.studio/v1/datasets/$ID/load -H "Authorization: Bearer $MISATA_KEY" \ -H "Content-Type: application/json" -d '{"target": "hubspot", "token": "'$HUBSPOT_TOKEN'", "apply": true}' ``` Salesforce takes `instance_url` beside `token`. Records are upserted on their Misata id, so a second load updates the same records; the properties or fields the files need are created first unless `"create_fields": false`. The token is used for that one call and never stored. Fields and limits: [Salesforce and HubSpot](/docs/learn/crm#load-from-the-api). **Query it in place.** Your dashboards can read the dataset where it is, with nothing copied: ```bash curl -X POST https://api.demo.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. ## Change it A ready dataset is changed by a revision: a new version, checked before it is kept. The first is never touched. | 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 from `GET /v1/datasets/{id}/story`. | | `curves` | The monthly curves the data follows: `[{"metric", "points": {"YYYY-MM": value}}]`. | | `controls` | Named controls, as `GET /v1/datasets/{id}/controls` lists them with their ranges. | ```bash curl https://api.demo.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` with the revision's id. Follow it with `GET /v1/datasets/$ID/revisions/$REV`; it ends `ready`, `unchanged` (nothing needed to change, with why), `discarded` (the new version did not verify, so it was not kept) or `failed`. Every version also opens in Studio. ## Instructions for every dataset Say once what every dataset on your account should keep to: ```bash curl -X PUT https://api.demo.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."}' ``` Each new dataset, from Studio or your app, reads them as requirements and holds them or says which it could not. ## 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, and 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 | Status | Meaning | |---|---| | `401` | No key, or the key is not valid or was revoked. | | `402` | The account's allowance does 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 it cannot take that change. | | `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. | A key sees only its own account's datasets.