On this page
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, 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.
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.
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:
curl -N https://api.demo.misata.studio/v1/datasets/$ID/events -H "Authorization: Bearer $MISATA_KEY"Events are numbered; reconnect with ?after=<n> or Last-Event-ID to pick up where you left off. When it is ready:
{
"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:
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).
Into your database. Misata loads PostgreSQL or ClickHouse directly. Plan first: it checks the host and says what would be loaded, without touching it.
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:
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.
Query it in place. Your dashboards can read the dataset where it is, with nothing copied:
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. |
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:
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:
{"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=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with the secret, and refuse a t more
than five minutes old.
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"])) < 300Misata 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.
Prefer generating data visually?
Misata Studio lets you turn natural language prompts into relational schemas with exact outcome invariants, live preview tables, and one-click database seeding without writing code.

