Developers
API documentation
From your own code: find real audience cohorts, run studies against them and read the results as JSON. The same operations are available to AI assistants through the MCP server.
Base URL: https://getmimi.xyz/api/v1
Quickstart
- Make a key in Settings › API and MCP and keep it as
MIMI_API_KEY. - Describe who should answer, and get matching cohorts back.
- Create a study: it starts running at once.
- Ask for the results, waiting up to 55 seconds a call, until they are ready.
curl -X POST https://getmimi.xyz/api/v1/audiences/describe \
-H "Authorization: Bearer $MIMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"description": "women 25-40 in the US into clean skincare", "limit": 5}'curl -X POST https://getmimi.xyz/api/v1/studies \
-H "Authorization: Bearer $MIMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Spring launch headline",
"versions": ["Glow that lasts all day. No filter needed.",
"Clean ingredients. Visible results in 7 days."],
"questions": [
{"text": "Which would make you stop scrolling?", "type": "choice",
"options": ["Version A", "Version B", "Neither"]},
{"text": "What would put you off?", "type": "open"}
],
"audience": {"describe": "women 25-40 in the US into skincare", "count": 4}
}'curl "https://getmimi.xyz/api/v1/studies/$STUDY_ID/results?waitSeconds=55" \
-H "Authorization: Bearer $MIMI_API_KEY"
# 202 with "ready": false while it runs; call again until 200 with "ready": trueYou can skip step 1 and pass audience.describe straight to POST /studies; Mimi then picks the cohorts and tells you which it chose and why in audiencePicked.
Authentication
Send your key as a bearer token on every request: Authorization: Bearer mimi_… (an x-api-key header works too). A key acts as you: it sees your studies, spends your budget and creates studies in your account. Keys start with mimi_, are shown once when you make them, and can be revoked at any time in settings, which stops them at once. Keep them on a server, never in a browser or an app bundle.
Each key has scopes. read reads studies, results and audiences. run creates and runs studies and website tests and asks the paid questions: ask_study and ask_followup both need it, because each answer costs credits. share turns public links on or off. A key can also have a spend cap in credits: runs, website tests and paid answers all count against it, and a call that would go past it is refused with 402 spend_cap_reached. A key reaches only its owner's own studies, whatever the owner's role. GET /me shows a key's scopes, cap and spend.
Conventions
- Requests and responses are JSON.
GETparameters go in the query string; lists asa,b,c. - Shares are numbers from 0 to 1, rounded to three places.
lowandhighare the range around each share. - Times are ISO 8601 in UTC.
- Every response carries an
X-Request-Id. Quote it when you contact support. POST /studiesandPOST /website-testsaccept anidempotencyKey(a UUID). The key is kept per account with a fingerprint of the request. Sending the same request with the same key returns what the first one made, with"replayed": true, so a retry after a timeout never starts a second run and is not charged again. The same key with a different request is refused with409 idempotency_mismatch: send the first request unchanged, or use a new key.
Errors
Every error has the same shape, with a stable code to branch on and a message for people.
HTTP/1.1 400 Bad Request
Content-Type: application/json
X-Request-Id: 6f1c2b8e-…
{
"error": {
"code": "invalid_request",
"message": "The request is not valid. See `details` for each field.",
"details": {
"questions.0.options": "A choice question needs 2 to 10 options.",
"audience": "Give either `cohorts` or `describe`, not both."
}
}
}| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | Send an API key as `Authorization: Bearer mimi_…`. Make one in Settings › API and MCP. |
forbidden | 403 | This key cannot do that. Its owner can change what it may do in Settings › API and MCP. |
not_ | 404 | Nothing with that id belongs to this key's account. |
invalid_ | 400 | The request is not valid. See `details` for each field. |
conflict | 409 | The request conflicts with the resource's current state. |
not_ | 409 | The study has no finished results yet. Wait for its run to finish. |
idempotency_ | 409 | That idempotencyKey was already used with a different request. Send the first request again unchanged to get its result, or use a new key for a new request. |
rate_ | 429 | Too many requests. Slow down and retry after `Retry-After` seconds. |
insufficient_ | 402 | There are not enough credits for this run. Nothing was spent; buy credits at https://getmimi.xyz/app/credits/buy. |
spend_ | 402 | This key has reached its spend cap. Nothing was spent; its owner can raise the cap in Settings › API and MCP. |
unavailable | 503 | A service mimi depends on did not answer. Retry shortly. |
internal | 500 | Something went wrong on mimi's side. Retry, and contact support if it persists. |
Rate limits
Each account can make 120 requests a minute across the API and MCP server together. Past that, requests answer 429 rate_limited with a Retry-After header in seconds. Work that costs more has its own limit: follow-ups are limited to 40 an hour, questions to a study's chat to 60 an hour (the app's Chat included), and files read by mimi's vision step to 30 an hour. Uploads have no limit. GET /me says what is left of each and when it resets. Study runs, web research and website tests have no daily cap and no cap on how many run at once: credits are their only limit. Each study runs one run at a time, and runs beyond the service's capacity wait in its queue rather than fail.
Waiting for a run
A run usually takes about a minute, several minutes with web research. Rather than polling quickly, pass waitSeconds (up to 55) to GET /studies/{id} or GET /studies/{id}/results: the request is held open and answers as soon as the run finishes, or when the wait is over. Results answer 202 with "ready": false and the run's progress while it is still going, and 200 with "ready": true when it is done.
Results read the study's latest run. Pass runId (the run.id that GET /studies/{id} returns) to read a particular run, for example an earlier finished one while a re-run is going. To stop a run, call POST /studies/{id}/cancel (cancel_run): the run ends before its next paid call and its credit hold is returned, as for any run that did not finish. A run still going after three hours is cancelled and refunded on its own. In the app, a run can be stopped from its progress card.
A run where fewer than half of its planned answers came back is marked degraded: true, with coverage the share that did (in run on results, and on the run in GET /studies/{id}). It is charged only for the answers that came back; treat its result as a thin sample. What a run was charged is credits.charged on the study's run and runMeta.creditChargedCents on results: the total, its tier's extras included. The parts are in credits.breakdown and runMeta.credits: run, extras, total and the quote it started with.
async function mimi(path: string, init: RequestInit = {}) {
const res = await fetch(`https://getmimi.xyz/api/v1${path}`, {
...init,
headers: { Authorization: `Bearer ${process.env.MIMI_API_KEY}`, "Content-Type": "application/json" },
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body;
}
const study = await mimi("/studies", { method: "POST", body: JSON.stringify(draft) });
let results = await mimi(`/studies/${study.id}/results?waitSeconds=55`);
while (!results.ready) results = await mimi(`/studies/${study.id}/results?waitSeconds=55`);
console.log(results.headline, results.questions[0].overall);Endpoint reference
| GET | /me | The account this key acts as, its credit balance, its limits, and what the key may do and spend. |
| POST | /audiences/describe | Plain words in, matching real audience cohorts out, best first, with why each matched. |
| GET | /audiences | Browse the cohort catalogue by keyword, family, gender and age. |
| GET | /audiences/{key} | One cohort: who its members are, its size, topics and sample comments. |
| GET | /studies | Your studies, newest first, with status and one-line result. |
| POST | /media/uploads | A short-lived URL to upload an image, video, audio or PDF file for a study's creative. |
| POST | /studies | Test a creative, compare versions or run a survey against real audience cohorts. |
| GET | /studies/{id} | A study's setup and its run's status and progress; can wait for it to finish. |
| POST | /studies/{id}/runs | Start (or re-run) a study's simulation. |
| GET | /studies/{id}/quote | What a run of the study would cost in credits, and whether the balance covers it. |
| POST | /studies/{id}/archive | Move a study out of the active list, or bring it back. |
| POST | /studies/{id}/cancel | Stop a study's run that is still going. |
| GET | /studies/{id}/results | The finished result: shares with ranges per question, version and cohort; objections; quotes; recommendation. |
| POST | /studies/{id}/ask | Ask a question about a finished run's results and get a written answer, as the report's Chat panel gives it. |
| POST | /studies/{id}/followups | Ask the study's cohorts a new question; each answers in its own voice. |
| GET | /studies/{id}/followups | The follow-up questions asked of a study and each cohort's answer. |
| POST | /studies/{id}/share | A read-only link to the study's report that anyone can open, once the person confirms. |
| DELETE | /studies/{id}/share | Turn the public link off. |
| POST | /studies/{id}/performance | Send back each ad's real numbers once it has run: impressions, clicks, installs, spend, revenue. |
| GET | /studies/{id}/performance | The numbers reported for each ad, with click rate, install rate and ROAS, beside the ad Mimi picked. |
| POST | /feed-tests | Put an image or video ad in feeds of real posts and have simulated people from an audience scroll past it. |
| GET | /feed-tests | Your Feed Emulator tests, newest first, with status. |
| GET | /feed-tests/{id} | A Feed Emulator test's setup, status and progress; can wait for it to finish. |
| GET | /feed-tests/{id}/sessions | Each person's feed and every step they took on it, with their reason. |
| GET | /feed-tests/{id}/report | Calibrated stop, watch and engagement rates against the real posts around the ad, recall and comment themes. |
| POST | /feed-tests/{id}/cancel | Stop a test that is still running. |
| POST | /website-tests | Send simulated people from an audience to a website and start recording what they do. |
| GET | /website-tests | Your website tests, newest first, with status and one-line result. |
| GET | /website-tests/{id} | A website test's setup, status and progress; can wait for it to finish. |
| GET | /website-tests/{id}/results | What people did on the site and why: outcomes, score, what put them off, fixes, picks, the page checklist and each person. |
| GET | /website-tests/{id}/fixes | Ready-to-paste prompts for fixing the site, one for each part of the score and one for the whole site. |
| POST | /website-tests/{id}/cancel | Stop a test that is still running. |
| POST | /website-tests/{id}/archive | Move a website test out of the active list, or bring it back. |
| POST | /website-tests/{id}/share | A read-only link to the test that anyone can open, with no account, once the person confirms. |
| DELETE | /website-tests/{id}/share | Turn the public link off. |
Account
Who am I
GET/api/v1/meMCP tool whoami
Return the mimi account the API key belongs to, its credit balance (100 credits = 1 USD) and its limits: what is left of each budget (requests a minute, follow-ups and chat questions an hour, files read by the vision step an hour) and when each resets, plus the key's own limits: its scopes (read, run, share), its spend cap in credits (null for none) and what it has spent and holds. Study runs, web research and website tests have no daily cap and no cap on how many run at once; credits and the key's spend cap are their only limits. Call it first to check a key works.
No parameters.
curl "https://getmimi.xyz/api/v1/me" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/me", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/me",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Audiences
Describe an audience
POST/api/v1/audiences/describeMCP tool describe_audience
Find the real audience cohorts that fit a plain-language description of who should weigh in ("moms in the US into meal prep", "men 18-24 into sneakers"). Returns cohort keys ranked by fit, how mimi read the description (gender, ages, place, topics) and a one-line reason per match. Use the keys in create_study.
| Field | Type | Description |
|---|---|---|
descriptionrequired | string | Who should weigh in, in plain words: "women 25-40 in the US into clean skincare".3–500 chars |
limit | integer | How many cohorts to return, best match first.1–24 · default 10 |
curl -X POST "https://getmimi.xyz/api/v1/audiences/describe" \
-H "Authorization: Bearer $MIMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "women 25-40 in the US into clean skincare",
"limit": 5
}'const res = await fetch("https://getmimi.xyz/api/v1/audiences/describe", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"description": "women 25-40 in the US into clean skincare",
"limit": 5
}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/audiences/describe",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
json={
"description": "women 25-40 in the US into clean skincare",
"limit": 5
},
)
data = res.json()Search audiences
GET/api/v1/audiencesMCP tool search_audiences
Search mimi's catalogue of real audience cohorts (people grouped by what they watch and comment on) by keyword, family (interest, creator, US panel), gender and age. Returns keys, labels, sizes and whether each cohort can be used in a new study. Prefer describe_audience for a natural-language brief.
| Field | Type | Description |
|---|---|---|
query | string | Words to find in a cohort's label, interest or summary, e.g. "skincare" or "home espresso".0–64 chars |
family | "interest" | "creator" | "panel" | interest: people grouped by what they engage with (default). creator: one creator's or brand's audience. panel: US general-population cells by age and gender. |
gender | "women" | "men" | Only cohorts whose label states this gender. |
ageMin | integer | Only cohorts whose stated age range reaches this age.13–100 |
ageMax | integer | Only cohorts whose stated age range starts at or below this age.13–100 |
page | integer | 1–200 · default 1 |
perPage | integer | 1–50 · default 20 |
curl "https://getmimi.xyz/api/v1/audiences?query=espresso&perPage=10" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/audiences?query=espresso&perPage=10", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/audiences?query=espresso&perPage=10",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Get an audience
GET/api/v1/audiences/{key}MCP tool get_audience
Read one audience cohort by key: its label, summary, size, what its members talk about and, where available, verbatim comments that show how they speak.
| Field | Type | Description |
|---|---|---|
keyrequired | string | A cohort key from search_audiences or describe_audience, e.g. aud-v3-tiktok-412.3–64 chars |
curl "https://getmimi.xyz/api/v1/audiences/aud-v3-tiktok-412" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/audiences/aud-v3-tiktok-412", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/audiences/aud-v3-tiktok-412",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/audiences/aud-v3-tiktok-412
Studies
List studies
GET/api/v1/studiesMCP tool list_studies
List the account's studies, newest first, with each one's goal, status and one-line result when it has finished.
| Field | Type | Description |
|---|---|---|
view | "active" | "archived" | default "active" |
limit | integer | 1–100 · default 25 |
cursor | string | The nextCursor of the page before, for the next page; leave out for the newest studies.1–2000 chars |
curl "https://getmimi.xyz/api/v1/studies" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/studies", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/studies",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Upload media
POST/api/v1/media/uploadsMCP tool upload_media
Get a short-lived URL to upload one image, video, audio or PDF file (40 MB at most) that the audience should see with a creative. POST the file's bytes to uploadUrl with the file's Content-Type; it answers {"storageId": ...}. Then pass {storageId, filename, mimeType, sizeBytes} in create_study's media. Unused uploads are deleted after a day.
| Field | Type | Description |
|---|---|---|
filenamerequired | string | The file's name, e.g. "launch.mp4".1–200 chars |
mimeTyperequired | "image/jpeg" | "image/png" | "image/webp" | "image/gif" | "video/mp4" | "video/quicktime" | "video/webm" | "audio/mpeg" | "audio/wav" | "application/pdf" | The file's type. |
sizeBytesrequired | integer | The file's size in bytes, 40 MB at most.1–41943040 |
curl -X POST "https://getmimi.xyz/api/v1/media/uploads" \
-H "Authorization: Bearer $MIMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "launch.mp4",
"mimeType": "video/mp4",
"sizeBytes": 8400000
}'const res = await fetch("https://getmimi.xyz/api/v1/media/uploads", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"filename": "launch.mp4",
"mimeType": "video/mp4",
"sizeBytes": 8400000
}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/media/uploads",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
json={
"filename": "launch.mp4",
"mimeType": "video/mp4",
"sizeBytes": 8400000
},
)
data = res.json()Create a study
POST/api/v1/studiesMCP tool create_study
Create a study and (by default) start its run: real audience cohorts react to a creative, compare 2-6 versions, or answer survey questions. Attach the creative's images, video, audio or PDFs with media (upload each file first with upload_media). Give the audience as cohort keys or as a plain description mimi matches for you. A run usually takes about a minute, several minutes with web research; follow it with get_study or get_results using waitSeconds. Costs credits (100 credits = 1 USD): the most it can cost is held when it starts and you pay only what it used. quote_study prices a draft before it runs. Send an idempotencyKey to retry safely: the same key and request return the first study (replayed: true), never start a second run and are not charged again, and the same key with a different request is refused with idempotency_mismatch. Needs a key with the run scope; a key with a spend cap starts no run that would take it past the cap.
| Field | Type | Description |
|---|---|---|
namerequired | string | A name you will recognise in the studies list.3–120 chars |
goal | "read" | "ab" | "multi" | "backlash" | "concept" | "survey" | "spread" | read: one creative. ab: two versions. multi: up to six. backlash: who objects and how strongly. concept: pressure-test an idea. survey: questions only, no creative. spread: how it travels between cohorts. Inferred from the versions when left out. |
creative | string | What the audience reacts to: an ad script, a post, a headline, a product idea. Use `versions` instead to compare several.10–4000 chars |
versions | string[] | Two to six alternatives to compare, A first.2–6 items |
context | string | Where and how people meet the creative: "a 15-second TikTok ad", "a subject line in a promotions tab".0–2000 chars |
description | string | A note for yourself about what this study is for.0–1000 chars |
questionsrequired | object[] | 1-10 questions. At least one must have options (choice, score or ranking). A ranking counts as two.1–10 items |
questions[].textrequired | string | The question as a respondent reads it.1–500 chars |
questions[].type | "choice" | "open" | "score" | "ranking" | choice: pick one option. open: a free-text answer. score: a 0-100 rating. ranking: order the options best to worst.default "choice" |
questions[].options | string[] | 2-10 options for a choice question, 3-10 for a ranking. Leave out for open and score.0–10 items |
audiencerequired | object | |
audience.cohorts | string[] | Exact cohort keys to ask (from search_audiences or describe_audience).1–12 items |
audience.describe | string | Or describe who should answer; mimi picks the best-matching cohorts.3–500 chars |
audience.count | integer | With `describe`: how many cohorts to pick.1–12 · default 5 |
fundingGoalUsd | integer | Concept studies only: a crowdfunding goal in US dollars. The results then include the chance a campaign reaches it.1–100000000 |
depth | "quick" | "standard" | "extended" | "max" | Responses per cohort: quick 6, standard 12, extended 24, max 40. Each cohort answers in model calls that state a whole distribution, so depth is how many responses those are read as, not how many simulation calls are made (quick asks 6 open answers per cohort instead of 12). extended also asks every cohort one follow-up why after the run, and max three follow-ups per cohort and a deeper analysis; their price includes that work, charged only when it was done.default "standard" |
research | "off" | "current" | current: read today's web context about the creative's subject before the audience answers.default "off" |
run | boolean | Start the run now. false saves a draft you can run later with run_study.default true |
seed | integer | A whole number from 0 to 4294967295 that fixes mimi's own random choices for the run: draw order, the quotes each call is shown, propagation. The models still answer afresh, so two runs with one seed are close, not identical. A fresh seed is drawn when left out; get_results returns the one used as runMeta.seed.0–4294967295 |
idempotencyKey | uuid | A UUID. Sending the same request with the same key again returns the first study instead of making a second, and never starts a second run. The same key with a different request is refused with idempotency_mismatch (409). |
media | object[] | Images, video or audio the audience sees with the creative, uploaded first with upload_media. Up to 4 files per version. mimi reads each file (frames and sound for a video) before the run, as the app does for an upload. Versions may share the same text when their media differ: that tests the image or video.0–12 items |
media[].storageId | string | The storageId the upload URL answered with.1–64 chars |
media[].filename | string | The file's name, e.g. "launch.mp4".1–200 chars |
media[].mimeType | "image/jpeg" | "image/png" | "image/webp" | "image/gif" | "video/mp4" | "video/quicktime" | "video/webm" | "audio/mpeg" | "audio/wav" | "application/pdf" | The file's type. |
media[].sizeBytes | integer | The file's size in bytes, 40 MB at most.1–41943040 |
media[].version | "a" | "b" | "c" | "d" | "e" | "f" | Which version the file belongs to: a is the creative (or the first version), b the second, and so on.default "a" |
links | object[] | Social posts the audience sees as the creative, by link instead of upload. They count toward the 4 files per version.0–12 items |
links[].url | string | A public TikTok, YouTube, Instagram or X post, e.g. "https://www.tiktok.com/@brand/video/7301234567890". mimi reads the post (its video, caption and sound) as the app does for a pasted link.8–2000 chars |
links[].version | "a" | "b" | "c" | "d" | "e" | "f" | Which version the post belongs to, as for media.default "a" |
curl -X POST "https://getmimi.xyz/api/v1/studies" \
-H "Authorization: Bearer $MIMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Spring launch headline",
"versions": [
"Glow that lasts all day. No filter needed.",
"Clean ingredients. Visible results in 7 days."
],
"context": "A 15-second TikTok ad",
"questions": [
{
"text": "Which version would make you stop scrolling?",
"type": "choice",
"options": [
"Version A",
"Version B",
"Neither"
]
},
{
"text": "How likely are you to try it, 0 to 100?",
"type": "score"
},
{
"text": "What would put you off?",
"type": "open"
}
],
"audience": {
"describe": "women 25-40 in the US into skincare",
"count": 4
},
"depth": "standard"
}'const res = await fetch("https://getmimi.xyz/api/v1/studies", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Spring launch headline",
"versions": [
"Glow that lasts all day. No filter needed.",
"Clean ingredients. Visible results in 7 days."
],
"context": "A 15-second TikTok ad",
"questions": [
{
"text": "Which version would make you stop scrolling?",
"type": "choice",
"options": [
"Version A",
"Version B",
"Neither"
]
},
{
"text": "How likely are you to try it, 0 to 100?",
"type": "score"
},
{
"text": "What would put you off?",
"type": "open"
}
],
"audience": {
"describe": "women 25-40 in the US into skincare",
"count": 4
},
"depth": "standard"
}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/studies",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
json={
"name": "Spring launch headline",
"versions": ["Glow that lasts all day. No filter needed.", "Clean ingredients. Visible results in 7 days."],
"context": "A 15-second TikTok ad",
"questions": [{
"text": "Which version would make you stop scrolling?",
"type": "choice",
"options": ["Version A", "Version B", "Neither"]
}, {
"text": "How likely are you to try it, 0 to 100?",
"type": "score"
}, {
"text": "What would put you off?",
"type": "open"
}],
"audience": {
"describe": "women 25-40 in the US into skincare",
"count": 4
},
"depth": "standard"
},
)
data = res.json()Get a study
GET/api/v1/studies/{id}MCP tool get_study
Read a study: what it asks, which cohorts answer, and its run's status (draft, running, complete, failed), phase and progress. Pass waitSeconds (up to 55) to hold until the run finishes, then call get_results.
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id, as create_study or list_studies returned it.1–64 chars |
waitSeconds | integer | Hold the request up to this many seconds while the run is in progress, and answer as soon as it finishes.0–55 · default 0 |
curl "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb
Run a study
POST/api/v1/studies/{id}/runsMCP tool run_study
Start a run for a draft study, or run a finished one again (for a fresh sample, a different depth, or with or without web research: pass depth or research as quote_study takes them). Refused while a run is in progress. Costs credits like create_study; refused with insufficient_credits when the balance cannot cover it, and with spend_cap_reached when it would take the key past its spend cap.
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id, as create_study or list_studies returned it.1–64 chars |
depth | "quick" | "standard" | "extended" | "max" | Responses per cohort for this run; the study's own setting when left out. |
research | "off" | "current" | current: read today's web context first; off: run without it. The study's own setting when left out. quote_study takes the same option to price it. |
seed | integer | A whole number from 0 to 4294967295 that fixes mimi's own random choices for the run: draw order, the quotes each call is shown, propagation. The models still answer afresh, so two runs with one seed are close, not identical. A fresh seed is drawn when left out; get_results returns the one used as runMeta.seed.0–4294967295 |
idempotencyKey | uuid | A UUID, new for each run you mean to start. Sending the same request with the same key again (say after a timeout) returns the run the first one started, with replayed: true, and never starts or charges a second. The same key with a different request is refused with idempotency_mismatch (409). |
curl -X POST "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/runs" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/runs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/runs",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/runs
Price a run
GET/api/v1/studies/{id}/quoteMCP tool quote_study
Price a run of a study before starting it: its quoted price, which is the most it is charged and what is held when it starts (estimate and hold are the same number), in credits (100 credits = 1 USD), and the account's balance. Pass depth or research to price a different run than the study's own settings: extended adds one follow-up why per cohort after the run and max three per cohort plus a deeper analysis, so each depth up costs more. Create the study with run: false to price it before it runs, then start it with run_study.
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id, as create_study or list_studies returned it.1–64 chars |
depth | "quick" | "standard" | "extended" | "max" | Responses per cohort to price; the study's own setting when left out. |
research | "off" | "current" | Price reading today's web context first; the study's own setting when left out. |
curl "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/quote" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/quote", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/quote",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/quote
Archive a study
POST/api/v1/studies/{id}/archiveMCP tool archive_study
Archive a study (archived: true) so it leaves the active list, or restore it (archived: false). Nothing is deleted: its runs, results and share link are kept, and list_studies with view "archived" lists it.
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id, as create_study or list_studies returned it.1–64 chars |
archived | boolean | true moves the study out of the active list; false brings it back.default true |
curl -X POST "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/archive" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/archive", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/archive",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/archive
Stop a run
POST/api/v1/studies/{id}/cancelMCP tool cancel_run
Stop the study's run in progress (or the run named by runId). Its job on the simulation service is cancelled before its next paid call, the responses so far are discarded, and its credit hold is returned less what the answers already made used (nothing, if none came back). A run still going after three hours is cancelled and refunded in full on its own. A run that already finished is left as it is (stopped: false).
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id.1–∞ chars |
runId | string | The run to stop. Leave it out to stop the study's run that is in progress.1–∞ chars |
curl -X POST "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel
Results
Get results
GET/api/v1/studies/{id}/resultsMCP tool get_results
Read a finished study's results: for every question the answer shares with their ranges, overall, per version and per cohort; averages for scores; orders for rankings; open answers with their themes; objections ranked by how upset each cohort is; representative quotes; and the written recommendation, findings, risks and next steps. Also the reads the report shows: the backlash level and peak upset, the virality level per version, how a comparison's versions stand against each other, a concept's funding forecast, and the run's cohorts, versions, settings, coverage, release, timing and cost. Lists are capped (50 objections, 24 quotes, 20 open answers per question); in a comparison the quotes are shared across its versions; `totals` says how many there were. Pass waitSeconds to wait for a running study. Reads the study's latest run unless you pass runId (as get_study or the app's ?run= link names it), so an earlier finished run stays readable while a re-run is going or after one failed. Answers `ready: false` with the run's progress while it is still going. `runMeta.creditChargedCents` is what the run was charged, its analysis extras included, and `runMeta.credits` gives the parts (run, extras, total, quote). `run.degraded` is true when fewer than half of the run's planned answers came back (it is charged only for the answers that came back; `run.coverage` is the share that did). The written parts (recommendation, themes, virality) land a minute or two after the run; `analysis` says which are ready, and a wait holds for them too.
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id, as create_study or list_studies returned it.1–64 chars |
waitSeconds | integer | Wait up to this many seconds for a running study to finish before answering.0–55 · default 0 |
calls | boolean | Also return, for each question, every model call's own answer distribution per cohort and version (questions[].calls), to compute your own intervals. Large; off by default.default false |
runId | string | The run to read, as get_study or the app's ?run= link names it. The study's latest run when left out.1–64 chars |
curl "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/results" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/results", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/results",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json(){
"ready": true,
"runId": "k97c2d…",
"study": { "id": "j57a8f…", "name": "Spring launch headline", "goal": "ab", "status": "complete" },
"run": { "status": "complete", "finishedAt": "2026-10-06T09:14:02.000Z", "responsesReturned": 96, "responsesAttempted": 96, "degraded": false, "coverage": 1 },
"headline": "Version B leads with 3 of 4 cohorts",
"recommendation": { "decision": "Publish Version B", "confidence": "medium", "rationale": "…" },
"versionPreference": {
"verdict": "b", "shareA": 0.38, "state": "validated_winner", "validatedWinner": "b",
"versions": [{ "key": "a", "share": 0.38, "low": 0.33, "high": 0.43 }, { "key": "b", "share": 0.62, "low": 0.57, "high": 0.67 }],
"ranking": ["b", "a"], "shortlist": null, "measure": "act", "kind": "text",
"evidence": { "cohorts": 4, "cohortsAgreeing": 3, "callsReturned": 16, "callsPlanned": 16, "thin": false },
"claim": { "status": "passed", "kind": "text", "unit": null, "heldOutTests": 1500, "calledShare": 0.44, "calledAccuracy": 0.8712, "scoredOn": "2026-10-14", "settingsMatch": "unpinned" },
"basis": "Version B is far enough ahead side by side, and this kind of creative passed the test's accuracy check."
},
"fundingForecast": null,
"questions": [
{
"id": "q1",
"text": "Which would make you stop scrolling?",
"type": "choice",
"basis": "samples",
"overall": [
{ "option": "Version B", "share": 0.52, "low": 0.42, "high": 0.62, "responses": 96 },
{ "option": "Version A", "share": 0.33, "low": 0.24, "high": 0.43, "responses": 96 }
],
"lead": { "option": "Version B", "share": 0.52, "rangesSeparate": true, "decisive": true },
"byCohort": [
{ "cohort": "aud-v3-tiktok-412", "label": "Women 25–34 in the US into skincare",
"options": [{ "option": "Version B", "share": 0.63 }, { "option": "Version A", "share": 0.25 }] }
]
}
],
"objections": [
{ "cohort": "aud-v3-tiktok-88", "label": "Women 30–40 in the US into clean beauty",
"severity": "mild", "kind": "taste", "likelyAction": "nothing", "upset": 0.31,
"objection": "\"Visible results in 7 days\" sounds like every other claim.", "who": "…" }
],
"backlash": { "level": "medium", "peakUpset": 0.18, "meanUpset": 0.09, "strongestAction": "post-about-it", "headline": "…" },
"virality": { "status": "done", "versions": [{ "version": "b", "level": "medium", "basis": "model-estimate", "shareIntent": 3.4 }] },
"versionSummary": {
"lean": { "version": "b", "questionId": "q1", "points": 19, "overlap": false },
"means": [{ "version": "b", "meanPositiveShare": 0.52, "questions": 1 }, { "version": "a", "meanPositiveShare": 0.33, "questions": 1 }]
},
"quotes": [{ "cohort": "aud-v3-tiktok-412", "stance": "positive", "text": "ok the 7 days thing got me…" }],
"findings": [{ "point": "…", "evidence": "…" }],
"risks": ["…"],
"nextSteps": ["…"],
"cohorts": [{ "key": "aud-v3-tiktok-412", "label": "Women 25–34 in the US into skincare" }],
"runContext": { "cohorts": [{ "key": "aud-v3-tiktok-412", "label": "…" }], "versions": [{ "key": "a", "text": "…", "mediaBrief": null }] },
"runMeta": { "seed": 4242, "temperature": 0, "promptVersion": "…", "release": "…", "startedAt": "…", "finishedAt": "…", "creditChargedCents": 45,
"credits": { "run": 37, "extras": 8, "total": 45, "quote": 52 }, "coverage": [] },
"limits": { "objections": 50, "quotes": 24, "openAnswersPerQuestion": 20 },
"totals": { "objections": 4, "quotes": 18, "openAnswers": 96 },
"caveat": "Simulated responses from model agents grounded in real audience cohorts…",
"links": { "app": "https://getmimi.xyz/app/studies/j57a8f…", "share": null }
}fundingForecast is set on a concept study made with a fundingGoalUsd: { "goalUsd": 12500, "chance": 0.41 }, the chance a crowdfunding campaign reaches the goal. It is null otherwise, and while the forecast is still being worked out after the run.
backlash and virality are the reads the report shows, computed the same way; each is null when the run did not measure it. versionSummary is set on a study that compares versions. Lists are capped at 50 objections, 24 quotes and 20 open answers per question, and totals says how many there were. In a comparison the quotes are shared across its versions, so each version keeps its share.
A comparison's cohorts are in byVersionByCohort, each version's shares per cohort with the same ranges as overall. runContext.versions[].mediaBrief is the exact description of each version's media the cohorts were given. With calls=true, each question also carries calls: every model call's own distribution by cohort, version and call, and runMeta.calls says whether the run kept them (inline) or not.
Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/results
Ask about a study
POST/api/v1/studies/{id}/askMCP tool ask_study
Ask a question about a finished study's results and get a written answer, the same one the report's Chat panel gives. It answers only from what that run recorded: every cohort's answers, the shares, objections, quotes and the written analysis. It runs nothing new and asks the cohorts nothing; to put a new question to the cohorts themselves, use ask_followup. Asks about the latest finished run unless you pass runId. Waits for the whole answer (usually well under a minute) and returns it as markdown, with at most one chart left in it as a fenced ```chart block of JSON (bar, stacked, compare or heatmap; the shapes are in the API docs), plus sources (the question numbers, version keys and cohort count it drew on), up to three followups (next questions it left open) and action (one untested change it recommends, or null). Its numbers are model estimates read from the simulated run, not measurements. The question and answer also appear in the study's Chat in the app. Each answer costs credits (shown in the Chat panel), charged once it is written; a failed answer is free. 60 questions an hour per account, shared with the app's Chat; questions are 500 characters at most. Needs a key with the run scope, since it spends credits; each answer counts on the key's spend cap, and is refused with spend_cap_reached when the key has reached it.
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id, as create_study or list_studies returned it.1–64 chars |
questionrequired | string | A question about the run's recorded results: "Which cohort liked Version B least, and why?"3–500 chars |
runId | string | The run to ask about, as get_study or the app's ?run= link names it. The latest finished run when left out.1–64 chars |
idempotencyKey | uuid | A UUID, new for each call. Sending the same request with the same key again (say after a timeout) returns the first answer with replayed: true and is not charged again. The same key with a different request is refused with idempotency_mismatch (409). |
curl -X POST "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/ask" \
-H "Authorization: Bearer $MIMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"question": "Which cohort liked Version B least, and why?"
}'const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/ask", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"question": "Which cohort liked Version B least, and why?"
}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/ask",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
json={
"question": "Which cohort liked Version B least, and why?"
},
)
data = res.json(){
"runId": "k97c2d…",
"answer": "**Parents 25–34 liked Version B least: 31% chose it, against 58% across the panel.**\n- They read \"Visible results in 7 days\" as an empty claim (Q3).\n\n```chart\n{\"type\":\"bar\",\"title\":\"Chose Version B (Q1)\",\"unit\":\"%\",\"data\":[{\"label\":\"Students\",\"value\":64},{\"label\":\"Parents 25–34\",\"value\":31}]}\n```",
"sources": { "questions": [1, 3], "versions": ["b"], "cohorts": 4 },
"followups": ["What would make Version B believable to parents?", "Does price change the parents' pick?"],
"action": { "kind": "test_change", "change": "Replace \"7 days\" with a named ingredient in Version B" }
}answer is markdown: bold, bullets and at most one small table or one chart. A chart is left in the text as a fenced ```chart block holding one JSON object. Draw it, or read its numbers; it is never more than one per answer. Its unit is % and its values are whole percents from the run's own data. Questions are named as they are numbered in the report (Q2) and versions by letter (Version B).
| type | Shows | JSON |
|---|---|---|
bar | One number per row, such as a share by cohort. | {"type":"bar","title":"Chose Version B (Q1)","unit":"%","data":[{"label":"Parents","value":31}]} |
stacked | How each row splits across the options; a row's values add up to about 100. | {"type":"stacked","title":"Would you try it? (Q2)","unit":"%","options":["Yes","Maybe","No"],"rows":[{"label":"Parents","values":[31,45,24]}]} |
compare | Two to four versions side by side, with the winner when there is one. | {"type":"compare","unit":"%","items":[{"label":"Version A ($29)","value":73,"note":"Fewer objections"},{"label":"Version B ($39)","value":58}],"winner":"Version A ($29)"} |
heatmap | Rows by columns, such as cohorts by questions; null where there is no data. | {"type":"heatmap","unit":"%","columns":["Q1","Q2"],"rows":[{"label":"Parents","values":[70,null]}]} |
title is optional on every chart and note on a compare item. sources says what the answer drew on: the question numbers, the version keys and how many cohorts; it is null when the answer did not say. followups holds up to three questions worth asking next, and action the one untested change the answer recommends, or null.
It answers from the run's recorded results only, and its numbers are model estimates like the rest of the result. Each answer costs credits, charged once it is written; a failed answer is free. It needs a key with the run scope, and each answer counts against the key's spend cap: past it the call is 402 spend_cap_reached, and without enough credits 402 insufficient_credits. A question longer than 500 characters is 400 invalid_request; a study with no finished run is 409 not_ready; another question about the same run still being answered is 409 conflict; past 60 questions an hour it is 429 rate_limited with Retry-After; and when the answer could not be written it is 503 unavailable, safe to ask again.
Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/ask
Ask a follow-up
POST/api/v1/studies/{id}/followupsMCP tool ask_followup
Ask a finished study's cohorts a follow-up question ("what would make this more believable?"). Each cohort answers in its own voice, grounded in what its real members say. Takes up to a minute. 40 follow-ups an hour per account. Each cohort's answer costs credits, charged once it is written. Needs a key with the run scope; the answers count on the key's spend cap, and are refused with spend_cap_reached when the key has reached it.
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id, as create_study or list_studies returned it.1–64 chars |
questionrequired | string | A follow-up the study's cohorts answer in their own voice: "What would make you trust this brand more?"3–500 chars |
cohorts | string[] | Only these cohorts of the study answer; all of them when left out.1–12 items |
idempotencyKey | uuid | A UUID, new for each call. Sending the same request with the same key again (say after a timeout) returns the first answer with replayed: true and is not charged again. The same key with a different request is refused with idempotency_mismatch (409). |
curl -X POST "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/followups" \
-H "Authorization: Bearer $MIMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"question": "What would make you trust this claim?"
}'const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/followups", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"question": "What would make you trust this claim?"
}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/followups",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
json={
"question": "What would make you trust this claim?"
},
)
data = res.json()Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/followups
List follow-ups
GET/api/v1/studies/{id}/followupsMCP tool list_followups
List the follow-up questions asked of a study's latest run, with each cohort's answer.
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id, as create_study or list_studies returned it.1–64 chars |
curl "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/followups" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/followups", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/followups",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/followups
Report how the ads did
POST/api/v1/studies/{id}/performanceMCP tool report_ad_performance
After you run the ads a study compared, send back how each one did, so Mimi's pick can be checked against what happened and Mimi improves as it sees more ads. Name each ad by the study's version key ("a", "b" and on, as get_study lists them; "a" for a single creative) and send its counts (impressions, clicks, installs), money (spend, revenue, currency) or rates (clickRate as clicks per impression, installRate as installs per click, from 0 to 1), engagementSeconds, and any other number by name in metrics. Send what you have: a report merges into the last one, so a number sent replaces the one stored and one left out is kept. Send cumulative totals, not the day's change. About the latest finished run unless you pass runId. Answers what get_ad_performance returns. Free; needs a key with the run scope.
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id, as create_study or list_studies returned it.1–64 chars |
runId | string | The run whose pick you tested, as get_study names it. The latest finished run when left out.1–64 chars |
source | string | Where the numbers come from, e.g. your ad network's name.1–80 chars |
adsrequired | object[] | One entry per ad you ran, each named by its version key.1–6 items |
ads[].versionrequired | string | Which of the study's ads this is: its version key, "a" to "f", as get_study lists them. "a" for a study of one creative. |
ads[].adId | string | Your own id for the ad, to match it in your system.1–120 chars |
ads[].impressions | integer | Times the ad was shown.0–∞ |
ads[].clicks | integer | Taps or clicks on the ad.0–∞ |
ads[].installs | integer | App installs the ad drove.0–∞ |
ads[].spend | number | What the ad cost, in `currency`.0–∞ |
ads[].revenue | number | Revenue the ad's installs brought in, in `currency`, for ROAS.0–∞ |
ads[].currency | string | ISO 4217 code for spend and revenue, e.g. USD. |
ads[].clickRate | number | Clicks per impression, 0 to 1 (0.034 is 3.4%), if you send the rate instead of the counts.0–1 |
ads[].installRate | number | Installs per click, 0 to 1, if you send the rate instead of the counts.0–1 |
ads[].engagementSeconds | number | Average engagement time per impression, in seconds.0–∞ |
ads[].metrics | object | Any other number you track for the ad, by name, e.g. { "d7Retention": 0.21 }. Up to 20. |
ads[].from | string | Start of the period the numbers cover. |
ads[].to | string | End of the period the numbers cover. |
curl -X POST "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/performance" \
-H "Authorization: Bearer $MIMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "Your ad network",
"ads": [
{
"version": "a",
"adId": "cr_1042",
"impressions": 120000,
"clicks": 3120,
"installs": 410,
"spend": 1850,
"revenue": 2960,
"currency": "USD",
"from": "2026-10-01",
"to": "2026-10-14"
},
{
"version": "b",
"adId": "cr_1043",
"impressions": 118500,
"clicks": 2490,
"installs": 288,
"spend": 1820,
"revenue": 1710,
"currency": "USD",
"from": "2026-10-01",
"to": "2026-10-14"
}
]
}'const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/performance", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"source": "Your ad network",
"ads": [
{
"version": "a",
"adId": "cr_1042",
"impressions": 120000,
"clicks": 3120,
"installs": 410,
"spend": 1850,
"revenue": 2960,
"currency": "USD",
"from": "2026-10-01",
"to": "2026-10-14"
},
{
"version": "b",
"adId": "cr_1043",
"impressions": 118500,
"clicks": 2490,
"installs": 288,
"spend": 1820,
"revenue": 1710,
"currency": "USD",
"from": "2026-10-01",
"to": "2026-10-14"
}
]
}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/performance",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
json={
"source": "Your ad network",
"ads": [{
"version": "a",
"adId": "cr_1042",
"impressions": 120000,
"clicks": 3120,
"installs": 410,
"spend": 1850,
"revenue": 2960,
"currency": "USD",
"from": "2026-10-01",
"to": "2026-10-14"
}, {
"version": "b",
"adId": "cr_1043",
"impressions": 118500,
"clicks": 2490,
"installs": 288,
"spend": 1820,
"revenue": 1710,
"currency": "USD",
"from": "2026-10-01",
"to": "2026-10-14"
}]
},
)
data = res.json()Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/performance
How the ads did
GET/api/v1/studies/{id}/performanceMCP tool get_ad_performance
Read the real numbers reported for a study's ads (report_ad_performance), one entry per version: the counts and money as sent, and clickRate (clicks per impression), installRate (installs per click), installsPerThousand (installs per 1,000 impressions) and roas (revenue over spend), each worked out from the counts when they were sent, else as reported. Beside them, the ad Mimi picked: only a version a validated side-by-side verdict named (versionPreference.state "validated_winner"). Otherwise mimiPick is a no-call (version null, basis "no_call") with its reason, for images and video, too-close and unvalidated runs alike. For each measure, which ad actually did best and whether it was Mimi's pick (null for a no-call, which is never scored). About the latest run with numbers reported unless you pass runId.
| Field | Type | Description |
|---|---|---|
idrequired | string | The study's id, as create_study or list_studies returned it.1–64 chars |
runId | string | The run to read. The latest run with reported numbers when left out.1–64 chars |
curl "https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/performance" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/performance", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/performance",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/studies/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/performance
Feed Emulator
Test an ad in a feed
POST/api/v1/feed-testsMCP tool create_feed_test
Start a Feed Emulator test: simulated people from one real audience cohort each scroll an Instagram-style feed (default) or an enabled X feed of 15 to 20 real recent posts picked for their interests, with your ad placed at a random slot from 4 to 7. Each person sees only what a scroller sees (a post's top until they stop; a video plays muted only while they keep watching) and decides on every post whether to scroll past, pause or stop, and whether to like, comment, share, save, open the profile or tap the button; afterwards they are asked what they remember. Upload the ad first with upload_media (a photo 1:1 or 4:5, or a video 9:16 or 4:5 up to 60 seconds; a video also needs its first frame uploaded as `cover`), then give the brand, caption, button and the audience (a cohort key, or `audience.describe`). An optional versionB from the same brand is compared with each person seeing one version. Returns the test's id; it runs for several minutes. Then call get_feed_test with waitSeconds until status is "finished" and get_feed_test_report for calibrated rates against the real posts around the ad. Costs credits (100 credits = 1 USD): the most it can cost is held when it starts, and it keeps only what its people used. Send an idempotencyKey to retry safely. X uses sampled public warehouse posts from scenario authors, not verified individual follow relationships; unavailable or insufficient warehouse evidence never triggers paid collection.
| Field | Type | Description |
|---|---|---|
audiencerequired | object | Whose members the simulated people are drawn from. English-speaking audiences only. |
audience.cohort | string | The exact cohort whose members scroll (a key from search_audiences or describe_audience).3–64 chars |
audience.describe | string | Or describe who should scroll ("US moms into meal prep"); mimi picks the best-matching cohort.3–500 chars |
platform | "instagram" | "tiktok" | "x" | The app whose feed people scroll: its layout, its real recent posts and its ad shapes. Instagram: a square or 4:5 picture, or a 9:16 or 4:5 video. TikTok: a 9:16 picture or video. X: a square, 16:9 or 4:5 picture, or a 16:9, square or 9:16 video.default "instagram" |
adrequired | object | The ad (Version A). |
ad.mediarequired | object | The ad's picture or video, uploaded first with upload_media. |
ad.media.storageIdrequired | string | The storageId upload_media's URL answered with.1–64 chars |
ad.media.mimeTyperequired | string | The file's type as uploaded: image/jpeg, image/png, image/webp, video/mp4, video/quicktime or video/webm.3–100 chars |
ad.media.widthrequired | integer | The picture's or video's width in pixels.100–10000 |
ad.media.heightrequired | integer | Its height in pixels. A photo is square or 4:5, a video 9:16 or 4:5.100–10000 |
ad.media.durationSec | number | A video's length in seconds (60 at most). Required for a video.0.5–60 |
ad.cover | object | For a video: its first frame as a JPEG, PNG or WebP picture (uploaded with upload_media), which is what a scroller sees before they stop. A photo is its own cover. |
ad.cover.storageId | string | The storageId upload_media's URL answered with.1–64 chars |
ad.cover.mimeType | string | The file's type as uploaded: image/jpeg, image/png, image/webp, video/mp4, video/quicktime or video/webm.3–100 chars |
ad.brandrequired | string | The brand's name as it shows on the post.1–80 chars |
ad.caption | string | The post's caption.0–2200 chars · default "" |
ad.ctarequired | "Shop now" | "Learn more" | "Sign up" | "Download" | "Book now" | "Get offer" | "Order now" | "Watch more" | "Contact us" | "Subscribe" | The ad's button. |
versionB | object | An optional Version B from the same brand. Each person sees one version; people are paired by interest and each pair shares a feed. |
versionB.media | object | The ad's picture or video, uploaded first with upload_media. |
versionB.media.storageId | string | The storageId upload_media's URL answered with.1–64 chars |
versionB.media.mimeType | string | The file's type as uploaded: image/jpeg, image/png, image/webp, video/mp4, video/quicktime or video/webm.3–100 chars |
versionB.media.width | integer | The picture's or video's width in pixels.100–10000 |
versionB.media.height | integer | Its height in pixels. A photo is square or 4:5, a video 9:16 or 4:5.100–10000 |
versionB.media.durationSec | number | A video's length in seconds (60 at most). Required for a video.0.5–60 |
versionB.cover | object | For a video: its first frame as a JPEG, PNG or WebP picture (uploaded with upload_media), which is what a scroller sees before they stop. A photo is its own cover. |
versionB.cover.storageId | string | The storageId upload_media's URL answered with.1–64 chars |
versionB.cover.mimeType | string | The file's type as uploaded: image/jpeg, image/png, image/webp, video/mp4, video/quicktime or video/webm.3–100 chars |
versionB.brand | string | The brand's name as it shows on the post.1–80 chars |
versionB.caption | string | The post's caption.0–2200 chars · default "" |
versionB.cta | "Shop now" | "Learn more" | "Sign up" | "Download" | "Book now" | "Get offer" | "Order now" | "Watch more" | "Contact us" | "Subscribe" | The ad's button. |
people | integer | How many simulated people scroll (2 to 50, 20 by default; 4 or more with a Version B).2–50 · default 20 |
idempotencyKey | uuid | A UUID. Sending the same request with the same key again returns the first test instead of starting a second. |
curl -X POST "https://getmimi.xyz/api/v1/feed-tests" \
-H "Authorization: Bearer $MIMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"audience": {
"describe": "US women 25-40 into walking and wellness"
},
"ad": {
"media": {
"storageId": "kg2...",
"mimeType": "image/jpeg",
"width": 1080,
"height": 1350
},
"brand": "Trailfoot",
"caption": "Meet the shoe that walks with you.",
"cta": "Shop now"
},
"people": 20
}'const res = await fetch("https://getmimi.xyz/api/v1/feed-tests", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"audience": {
"describe": "US women 25-40 into walking and wellness"
},
"ad": {
"media": {
"storageId": "kg2...",
"mimeType": "image/jpeg",
"width": 1080,
"height": 1350
},
"brand": "Trailfoot",
"caption": "Meet the shoe that walks with you.",
"cta": "Shop now"
},
"people": 20
}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/feed-tests",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
json={
"audience": {
"describe": "US women 25-40 into walking and wellness"
},
"ad": {
"media": {
"storageId": "kg2...",
"mimeType": "image/jpeg",
"width": 1080,
"height": 1350
},
"brand": "Trailfoot",
"caption": "Meet the shoe that walks with you.",
"cta": "Shop now"
},
"people": 20
},
)
data = res.json()List Feed Emulator tests
GET/api/v1/feed-testsMCP tool list_feed_tests
List the account's Feed Emulator tests, newest first, with each one's audience, versions and status. view "archived" lists the archived ones instead.
| Field | Type | Description |
|---|---|---|
view | "active" | "archived" | default "active" |
limit | integer | 1–50 · default 25 |
curl "https://getmimi.xyz/api/v1/feed-tests" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/feed-tests", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/feed-tests",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Get a Feed Emulator test
GET/api/v1/feed-tests/{id}MCP tool get_feed_test
Read a Feed Emulator test: its audience, its versions and where it stands (queued, collecting posts, scrolling, calibrating, finished, failed, cancelled) with how many people are done. Pass waitSeconds (up to 55) to hold until it finishes, then call get_feed_test_report.
| Field | Type | Description |
|---|---|---|
idrequired | string | The Feed Emulator test's id, as create_feed_test or list_feed_tests returned it.1–64 chars |
waitSeconds | integer | Hold the request up to this many seconds while the test is in progress, and answer as soon as it finishes.0–55 · default 0 |
curl "https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb
Get each person's scroll
GET/api/v1/feed-tests/{id}/sessionsMCP tool get_feed_test_sessions
Read each simulated person's scroll: their version, where the ad sat, and for every post what they did (scrolled past, paused, stopped; how far into a video; liked, commented with what, shared to whom, saved, opened the profile, tapped) and why, then what they remembered. Other people's posts appear as numbered blanks (post-1, post-2): they were used inside the run only. Pass `person` for one person.
| Field | Type | Description |
|---|---|---|
idrequired | string | The Feed Emulator test's id, as create_feed_test or list_feed_tests returned it.1–64 chars |
person | integer | One person's number: answer with that person's scroll alone, step by step.0–49 |
curl "https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/sessions" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/sessions", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/sessions",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/sessions
Get a Feed Emulator report
GET/api/v1/feed-tests/{id}/reportMCP tool get_feed_test_report
Read a finished Feed Emulator test's report: how often people stopped for the ad, how far they watched it and how they engaged with it, each as a lift over the real posts in the same people's feeds (calibrated, never a raw simulated answer; an absolute rate only where the real posts' own counts anchor it), unaided and aided recall corrected for saying yes to anything, the themes of what people commented with their comments, and a check of how well the people tracked the real posts' real engagement. With a Version B it says which drew more engagement only once Feed Emulator has passed its check on real ad outcomes; until then it says there is no reliable winner call. Until the test finishes it answers ready: false; pass waitSeconds to wait.
| Field | Type | Description |
|---|---|---|
idrequired | string | The Feed Emulator test's id, as create_feed_test or list_feed_tests returned it.1–64 chars |
waitSeconds | integer | Hold the request up to this many seconds while the test is in progress, and answer as soon as it finishes.0–55 · default 0 |
curl "https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/report" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/report", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/report",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/report
Cancel a Feed Emulator test
POST/api/v1/feed-tests/{id}/cancelMCP tool cancel_feed_test
Stop a Feed Emulator test that is still in progress. What people had already done is kept and is charged for; a test that is already over is left as it is.
| Field | Type | Description |
|---|---|---|
idrequired | string | The Feed Emulator test's id, as create_feed_test or list_feed_tests returned it.1–64 chars |
curl -X POST "https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/feed-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel
Website testing
Website tests were called customer tests. The older addresses under /api/v1/customer-tests and the older tool names (create_customer_test, list_customer_tests, get_customer_test, get_customer_test_results, get_customer_test_fixes, cancel_customer_test, share_customer_test, unshare_customer_test) still work and run the same code. Use the names below in new work.
Test a website
POST/api/v1/website-testsMCP tool create_website_test
Start a website test: simulated people drawn from one real audience cohort visit a public website in a browser, look around, and say what they think and what they would do. Give the site's address, the audience (a cohort key, or `audience.describe` to let mimi pick one), who they are to the site (`visitor`: customers by default, or investors, job candidates, partners, press, or your own description), an optional question or task, and how many people (10, 20, 50 or 100). Returns the test's id; it runs for a few minutes. Then call get_website_test with waitSeconds until status is "finished", get_website_test_results for what they did and why, and get_website_test_fixes for ready-made prompts to fix the site. Nobody signs in, fills in a form or buys anything; a site whose bot check keeps the browser out is reported as blocked, never worked around. Tests are limited by available credits and wait for browser capacity across the deployment; more than one test may be queued or running. Tests of pages behind a sign-in are started from the app, not here. Costs credits (100 credits = 1 USD): the most it can cost is held when it starts, and when it ends it keeps only what its people used (their steps and browser time); a test that fails gives the hold back. Refused with insufficient_credits when the balance cannot cover the hold, and with spend_cap_reached when it would take the key past its spend cap. Send an idempotencyKey to retry safely: the same key returns the first test, and the same key with a different request is refused with idempotency_mismatch.
| Field | Type | Description |
|---|---|---|
urlrequired | string | The public website to test, e.g. "https://example.com". People start on this page; they can open other pages of the same site.4–2000 chars |
audiencerequired | object | Whose members the simulated people are drawn from. One audience a test; run another test for another audience. |
audience.cohort | string | The exact cohort whose members visit (a key from search_audiences or describe_audience).3–64 chars |
audience.describe | string | Or describe who should visit ("seed-stage investors who follow defence tech"); mimi picks the best-matching cohort.3–500 chars |
visitor | object | Who the people are to the site. Customers when left out. |
visitor.kind | "customer" | "investor" | "candidate" | "partner" | "press" | "other" | What they are on the site for. customer: might buy or use it. investor: weighing the company. candidate: thinking of working there. partner: an organisation that might work with it. press: deciding whether it is a story. other: describe them in `note`.default "customer" |
visitor.note | string | Who they are, more exactly, in your own words: "procurement officers at a defence agency". Required for kind "other".0–200 chars |
question | string | What they should find out or do, in plain words: "Decide whether you would buy from this site, and say what settled it." A task with a budget ("pick a gift for $75 to $100") holds people to it and checks the price. A fitting default for the visitor kind is used when left out.0–2000 chars |
task | "pick" | "find" | "none" | How the question is to be read, when Mimi's own reading should be corrected. pick: each person picks one thing ("find your favourite" is a pick). find: each person looks for a named thing. none: an opinion only. Read from the question when left out. |
people | integer | How many simulated people visit: 10, 20, 50 or 100. More is steadier and slower.-9007199254740991–∞ · default 10 |
device | "desktop" | "mobile" | The size of browser they use.default "desktop" |
locationState | string | US state code for all visitor browser proxies, such as CA or NY. When omitted, explicit audience state evidence is used or diverse synthetic US states are assigned; these are browser settings, not real participant locations. |
landingUrl | string | A page on the same site where people who arrive from an ad or a post start, when that is not the front page.0–2000 chars |
sharedCapture | boolean | false (default): each person opens the site in a browser of their own and sees what it shows them. true: one browser reads the site once and everyone walks that reading, which is faster.default false |
signIn | boolean | Not available through the API. A test of pages behind a sign-in is started from the app, where its owner signs in to a demo account in a live browser. |
idempotencyKey | uuid | A UUID. Sending the same request with the same key again returns the first test instead of starting a second. The same key with a different request is refused with idempotency_mismatch (409). |
curl -X POST "https://getmimi.xyz/api/v1/website-tests" \
-H "Authorization: Bearer $MIMI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"audience": {
"describe": "software engineers who follow developer tools"
},
"visitor": {
"kind": "candidate",
"note": "senior software engineers"
},
"question": "Decide whether you would apply to work here, and say what settled it.",
"people": 10,
"device": "desktop"
}'const res = await fetch("https://getmimi.xyz/api/v1/website-tests", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://example.com",
"audience": {
"describe": "software engineers who follow developer tools"
},
"visitor": {
"kind": "candidate",
"note": "senior software engineers"
},
"question": "Decide whether you would apply to work here, and say what settled it.",
"people": 10,
"device": "desktop"
}),
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/website-tests",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
json={
"url": "https://example.com",
"audience": {
"describe": "software engineers who follow developer tools"
},
"visitor": {
"kind": "candidate",
"note": "senior software engineers"
},
"question": "Decide whether you would apply to work here, and say what settled it.",
"people": 10,
"device": "desktop"
},
)
data = res.json()List website tests
GET/api/v1/website-testsMCP tool list_website_tests
List the account's website tests, newest first, with each one's site, audience, who visited, status and one-line result once it has finished. view "archived" lists the archived ones instead.
| Field | Type | Description |
|---|---|---|
view | "active" | "archived" | default "active" |
limit | integer | 1–100 · default 25 |
curl "https://getmimi.xyz/api/v1/website-tests" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/website-tests", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/website-tests",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Get a website test
GET/api/v1/website-tests/{id}MCP tool get_website_test
Read a website test: the site, the audience, who the people are to it, the question, and where it stands (queued, capturing, visiting, writing, finished, failed, cancelled) with how many people are done. A failed test says why, including "blocked" when the site's bot check kept the browser out. Pass waitSeconds (up to 55) to hold until it finishes, then call get_website_test_results.
| Field | Type | Description |
|---|---|---|
idrequired | string | The website test's id, as create_website_test or list_website_tests returned it.1–64 chars |
waitSeconds | integer | Hold the request up to this many seconds while the test is in progress, and answer as soon as it finishes.0–55 · default 0 |
curl "https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb
Get website test results
GET/api/v1/website-tests/{id}/resultsMCP tool get_website_test_results
Read a finished website test: how each visit ended (tapped a call to action, might come back, not for them, gave up) as counts, people kept out by a bot check, the score out of 100 and its parts, the headline, what put people off (themes with how many people, a fix and quotes), what works, what to fix first, what people picked on a task test, what the front page's first screen plainly shows, the pages and routes people took, and every person in brief. Pass `person` (a number from `everyone`) for that person's visit action by action: what they noticed, said and did. Until the test finishes it answers ready: false; pass waitSeconds to wait. Every count is computed from the stored visits. The people are simulated: present the result as a strong prior for what to fix and test, not as measured behaviour.
| Field | Type | Description |
|---|---|---|
idrequired | string | The website test's id, as create_website_test or list_website_tests returned it.1–64 chars |
waitSeconds | integer | Wait up to this many seconds for a test in progress to finish before answering.0–55 · default 0 |
person | integer | One person's number from `everyone[].person`: answer with that person's visit, action by action, instead of the whole result.0–99 |
curl "https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/results" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/results", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/results",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/results
Get prompts to fix the site
GET/api/v1/website-tests/{id}/fixesMCP tool get_website_test_fixes
Read a finished website test's fix plan: for each part of the score (weakest first) where it stands, the line to reach, what to change, people's own words, and a ready-made prompt that asks for the change; then one prompt for the whole site. Use this when the job is to go and improve the site: the prompts are written from the test's own counts and quotes and can be followed as they are. The targets are a line to reach, not a forecast; run the test again afterwards to see what moved.
| Field | Type | Description |
|---|---|---|
idrequired | string | The website test's id, as create_website_test or list_website_tests returned it.1–64 chars |
curl "https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/fixes" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/fixes", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.get(
"https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/fixes",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/fixes
Cancel a website test
POST/api/v1/website-tests/{id}/cancelMCP tool cancel_website_test
Stop a website test that is still in progress and close its browsers. What people had already done is kept. A test that is already over is left as it is.
| Field | Type | Description |
|---|---|---|
idrequired | string | The website test's id, as create_website_test or list_website_tests returned it.1–64 chars |
curl -X POST "https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/cancel
Archive a website test
POST/api/v1/website-tests/{id}/archiveMCP tool archive_website_test
Archive a website test (archived: true) so it leaves the active list, or restore it (archived: false). Nothing is deleted: its result, its visits and its share link are kept, and list_website_tests with view "archived" lists it.
| Field | Type | Description |
|---|---|---|
idrequired | string | The website test's id, as create_website_test or list_website_tests returned it.1–64 chars |
archived | boolean | true moves the test out of the active list; false brings it back.default true |
curl -X POST "https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/archive" \
-H "Authorization: Bearer $MIMI_API_KEY"const res = await fetch("https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/archive", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MIMI_API_KEY}`,
},
});
const data = await res.json();import os, requests
res = requests.post(
"https://getmimi.xyz/api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/archive",
headers={"Authorization": f"Bearer {os.environ['MIMI_API_KEY']}"},
)
data = res.json()Path: /api/v1/website-tests/j57a8f2kq9w3x1m6v4r0d5n2p8s7t3yb/archive
How answers are made and comparisons judged
Each cohort answers in two model calls per version (more when a second model also answers), and each call states the share of the cohort that would pick each option. So basis is model_estimate, and low and high are the range across calls, not a confidence interval. depth sets how many responses those shares are read as, which does not change the number of simulation calls (quick asks 6 open answers per cohort instead of 12). The two higher depths add work after the simulation, and quote_study includes it: extended asks every cohort one follow-up why, and max asks three follow-ups per cohort and writes a deeper analysis. Their answers are in the analysis and its follow-ups, and that work is charged only when it was done. A seed fixes mimi's own random choices, not the models' answers, so two runs with one seed are close, not identical.
In a comparison, each version's answers come from separate calls: a cohort answering one version never sees the others. versionPreference comes from a separate side-by-side pass. Each cohort sees all the versions (2 to 6) together under neutral letters, in a balanced rotation of orders, so every version appears in every position equally often. It splits 100 points twice: by what its members would do at first glance (act) and by which they would call better (prefer). Cohorts count equally. The verdict comes from act, or from prefer for an identity change such as a logo or a name; measure says which.
A version is ahead only when two things hold: its margin over the runner-up reaches the cutoff for its kind (0.125 for text, 0.10 when a second model's reads are averaged in, 0.15 with media), and the margin's 80% interval stays above zero. That interval, like versions[].low and high, reflects how far the cohorts agree. It is not an error bar against real outcomes. A winner is shown only when that kind of test has passed an accuracy check on real outcomes and the call rests on enough evidence (at least 3 cohorts and 80% of the side-by-side calls returned).
Read state. validated_winner is the only state that names a version, in validatedWinner, and claim is the accuracy record behind it. lean means a version is ahead but not validated. too_close (two versions) and shortlist (three to six: the versions that cannot be separated from the top) name no version. no_reliable_call is every image or video comparison, at any number of versions: on real ad tests the call has not yet beaten a coin flip, so no version is named. not_run means no cohort answered. verdict keeps its older values (a version key, too_close, unvalidated for images and video, unavailable) for callers that read it; runMeta.abPreference carries the same fields plus cohortsLeaning. The get_ad_performance pick follows the same rule: it names an ad only for a validated winner, and a no-call is never scored.
The two-version text record was scored on 1,500 held-out Upworthy news-headline A/B tests from 2013 to 2015, all of which had a real winner. Mimi named a winner on 44% and was right on 87% of those (scored October 2026). That test used one plain audience and no study questions, which is simpler than a full study, and other kinds of copy have not been scored on their own. Comparisons of three to six text versions have not passed their check, so they show a lean or a shortlist. On a 0–100 score question, versions are compared on the top two bands (61–100).
A question's lead names its leading option. rangesSeparate is true when the ranges shown for the lead and the runner-up (overall[].low and high) do not touch; decisive is the same value, kept for older callers.
Runs sample a fixed number of responses per cohort, set by depth. The adaptive sampling option has been removed: the simulation service never implemented it, so no run uses it.
OpenAPI
The full description is at /api/v1/openapi.json (OpenAPI 3.1, no key needed). Generate a typed client from it, import it into Postman or Insomnia, or hand it to an agent. It is written from the same definitions the API validates with, so it is always current.
Attach images, video, audio or PDFs
- Call
POST /media/uploadswithfilename,mimeTypeand the exactsizeBytes(40 MB at most). - POST the file's raw bytes to
uploadUrlwith its returned Content-Type. Wait for the response containingstorageId. - Include that storage id and the same file metadata in
POST /studies'smediaarray. Setversiontoafor the main creative,bfor the second version, and so on. Each file may appear once; at most four per version and twelve in total.
{
"media": [{
"storageId": "the-id-returned-by-the-upload",
"filename": "launch.mp4",
"mimeType": "video/mp4",
"sizeBytes": 8400000,
"version": "a"
}],
"idempotencyKey": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed"
}Media analysis and retries
Mimi describes each file before running the study. Set a five-minute request timeout to allow for analysis and saving, and check that every returned media item has analysis: ready. If analysis is unavailable, a request with run: true returns 503 without creating a study or run. Retry the same request with the same uploads and idempotency key. An exhausted allowance returns 429 with Retry-After.
run: false may save a draft with analysis: failed and a warning; it cannot run until its media are ready. Retry the same draft request and key to retry analysis, then call the run endpoint. Unused uploads expire after a day. An already registered file can only be reused by its owner with matching metadata.
