#GhostUser API
Trigger ghost-user tests on any URL and get back usability findings plus a score, programmatically. Point GhostUser at a page, and opinionated AI "ghosts" with different personas try it like real users — then report what's confusing, broken, or annoying, with an overall score out of 10.
The API mirrors the product: kick off a run, poll until it's done, and read the structured report. Wire it into CI to catch regressions on every deploy, schedule nightly checks, or build it into your own dashboard.
https://ghostuser.io/v1
Bearer gu_live_…
JSON over HTTPS
#Authentication
Every request must include your API key as a Bearer token in the Authorization header.
Keys look like gu_live_xxx and are created in the dashboard under
Account → API keys at
ghostuser.io.
curl https://ghostuser.io/v1/runs \
-H "Authorization: Bearer gu_live_xxx"
gu_live_ keys like passwords —
use them server-side only, never in browser or mobile client code. If a key leaks, revoke it in the dashboard and issue a new one.
#Quickstart
Three steps: start a test, poll it,
and read the result. Runs are asynchronous — a real run drives a live browser and
can take a few minutes — so you start a run, then poll GET /v1/runs/{id} until status is done.
- 1Start a run —
POST /v1/runswith your URL. You get back a runidandstatus: "running". - 2Poll the run —
GET /v1/runs/{id}every few seconds untilstatusflips todone(orerror). - 3Read the result — pull
overall_score,counts, and theissuesarray. Openreport_urlfor the full visual report.
# 1. Start a run
curl -X POST https://ghostuser.io/v1/runs \
-H "Authorization: Bearer gu_live_xxx" \
-H "Content-Type: application/json" \
-d '{"url":"https://yourapp.com","tier":"economy"}'
# → { "id": "ab12cd34", "status": "running", ... }
# 2. Poll until status is "done"
curl https://ghostuser.io/v1/runs/ab12cd34 \
-H "Authorization: Bearer gu_live_xxx"
# → { "id": "ab12cd34", "status": "done", "overall_score": 7.5, ... }
#Runs
A run is the core resource: one ghost-user test of one URL.
Create a run to start a test, fetch a run to read its report, and list runs to see your recent history.
A run moves through running → done (or error).
/v1/runs
Starts a new ghost-user test on a URL. Returns immediately with 201 Created and a run in the
running state — poll GET /v1/runs/{id} for the result.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
url |
string | required | The URL the ghosts start from. Use a deep link to land them on a specific page or flow. |
tier |
string | optional | Depth/cost of the run. One of quick, economy, balanced, premium. Defaults to economy. |
personas |
string[] | optional | Persona keys to run, e.g. first_timer, mobile_first_user. Defaults to a sensible set for your product. See Personas. |
scenario |
string | optional | A focus for the run, e.g. "test the signup flow". Omit to let ghosts explore and find the core flow. |
Example request
curl -X POST https://ghostuser.io/v1/runs \
-H "Authorization: Bearer gu_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com",
"tier": "economy",
"personas": ["first_timer", "mobile_first_user"],
"scenario": "test the signup flow"
}'
Response · 201 Created
{
"id": "ab12cd34",
"status": "running",
"url": "https://yourapp.com",
"tier": "economy",
"created_at": "2026-06-23T14:00:00Z",
"report_url": "https://ghostuser.io/report/ab12cd34"
}
/v1/runs/{id}
Returns a run and, once finished, its full report. While running, status is running and
report fields may be absent. Poll this endpoint until status is done
(terminal states are done and error).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | required | The run id returned by POST /v1/runs, e.g. ab12cd34. |
Example request
curl https://ghostuser.io/v1/runs/ab12cd34 \
-H "Authorization: Bearer gu_live_xxx"
Response · 200 OK
{
"id": "ab12cd34",
"status": "done",
"url": "https://yourapp.com",
"tier": "economy",
"created_at": "2026-06-23T14:00:00Z",
"overall_score": 7.5,
"counts": { "critical": 0, "major": 1, "minor": 2 },
"num_ghosts": 2,
"headline": "Solid core flow, but the signup CTA is easy to miss on mobile.",
"issues": [
{
"severity": "major",
"title": "Signup button is below the fold on mobile",
"detail": "On a 375px viewport the primary CTA sits under three scrolls of copy.",
"quote": "Wait, where do I actually sign up? I had to scroll forever.",
"fix": "Pin a sticky 'Get started' button or move it above the fold on small screens."
}
],
"report_url": "https://ghostuser.io/report/ab12cd34"
}
Response fields
| Field | Type | Description |
|---|---|---|
status | string | One of running, done, error. Poll until done. |
overall_score | number | Usability score from 0–10. ≥7 is good, 4–7 mid, <4 needs work. |
counts | object | Issue counts by severity: critical, major, minor. |
num_ghosts | number | How many ghost personas tested the URL. |
headline | string | A one-line human summary of the run. |
issues | object[] | Findings, each with severity, title, detail, quote (a ghost's words), and a suggested fix. |
report_url | string | Link to the full visual report in the dashboard. |
/v1/runs
Returns a compact list of your most recent runs, newest first — handy for dashboards and history views.
Example request
curl https://ghostuser.io/v1/runs \
-H "Authorization: Bearer gu_live_xxx"
Response · 200 OK
{
"runs": [
{
"id": "ab12cd34",
"status": "done",
"url": "https://yourapp.com",
"overall_score": 7.5,
"created_at": "2026-06-23T14:00:00Z",
"report_url": "https://ghostuser.io/report/ab12cd34"
},
{
"id": "ef56gh78",
"status": "running",
"url": "https://yourapp.com/pricing",
"overall_score": null,
"created_at": "2026-06-23T14:08:00Z",
"report_url": "https://ghostuser.io/report/ef56gh78"
}
]
}
#Personas
Each ghost is a persona — a distinct kind of user with their own goals,
patience, and blind spots. Pass personas as an array of keys on
POST /v1/runs to choose who tests your app.
Omit it to use a sensible default set tuned for your product.
first_timerNever seen your product — judges onboarding cold.
mobile_first_userLives on a phone; flags anything that breaks on small screens.
skeptical_studentImpatient and unimpressed; bounces if value isn't obvious fast.
privacy_skepticReads the fine print; wary of data, tracking, and permissions.
accessibility_userTests contrast, labels, and keyboard/screen-reader friendliness.
busy_professionalTime-starved; wants the core action done in seconds, no fluff.
dev_security_consciousA developer's eye — scrutinizes API keys, scopes, docs, and integration safety.
#Tiers & credits
Runs cost credits, billed on completion by actual usage. The tier trades
depth for cost — higher tiers let each ghost take more steps and dig deeper, at a higher credit cost. Pick the tier that fits how
thorough you need the run to be.
| Tier | Relative cost | Best for |
|---|---|---|
quick | ¢ | A fast smoke test — does the core flow work at all? |
economy | $ | The default. Solid coverage at the lowest real cost. |
balanced | $$ | Deeper exploration for important flows and releases. |
premium | $$$ | Maximum depth — the most thorough, patient ghosts. |
#Errors
Errors use standard HTTP status codes and return a consistent JSON shape with a machine-readable
code and a human-readable message.
{
"error": {
"code": "invalid_api_key",
"message": "The API key provided is invalid or has been revoked."
}
}
| HTTP | Code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed request — missing url, bad JSON, or an unknown tier / persona key. |
| 401 | invalid_api_key | Missing, malformed, or revoked API key in the Authorization header. |
| 402 | insufficient_credits | Not enough credits to start the run. Top up in the dashboard. |
| 404 | not_found | No run exists with that id for your account. |
| 429 | rate_limited | Too many requests too quickly. Back off and retry. See Rate limits. |
| 500 | server_error | Something went wrong on our side. Safe to retry; if it persists, get in touch. |
#Rate limits
The API allows a few requests per second per key — plenty for normal use. Because runs are
asynchronous, you don't hold a connection open while a test runs: you start a run, then
poll GET /v1/runs/{id} on an interval.
A polling interval of every 5 seconds is a good default — runs take minutes, not milliseconds,
so faster polling just burns requests. If you exceed the limit you'll get 429 rate_limited; back off and retry.
Need higher limits? Reach out via the
dashboard.