A read-only REST API over your plans, features, tasks and progress — for dashboards, internal
tooling and integrations. Five endpoints, all GET, all JSON.
This is not the agent API. /api/cli/v1/* is the released contract of the CLI and the MCP
server: it writes task status, evidence and checkpoints, and it requires an editor role. This
surface only reads, so any role on a plan is enough.
/api/cli/v1/* | /api/v1/* | |
|---|---|---|
| Audience | coding agents | integrations, dashboards |
| Verbs | GET, PATCH, POST | GET, OPTIONS |
| Role needed | editor | any role, including viewer |
| Rate bucket | its own | its own |
| CORS | none | *, no credentials |
The same iprd_… access tokens as the CLI (Settings → Access tokens):
Authorization: Bearer iprd_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
A session cookie is never accepted — the Authorization header is the only credential these routes
read, so a signed-in browser cannot be steered into calling them with an ambient session.
A revoked token, an unknown token and a malformed one are all 401, indistinguishable from each
other. Scope is enforced: an account token reaches every plan its owner has a role on; a plan-scoped
token reaches only its own plan.
Every response is served Cache-Control: no-store, and every path answers OPTIONS.
GET /api/v1/mecurl -s https://instantprd.web.id/api/v1/me \
-H "Authorization: Bearer $INSTANTPRD_TOKEN"
{
"user": { "id": "…uuid…", "name": "Yeris", "email": "yeris@example.com" },
"token": { "name": "ci-dashboard", "prefix": "iprd_A1b2", "scope": "account", "plan_id": null },
"tier": "pro"
}
token.scope is account or plan, and plan_id is set only for a plan-scoped token. tier is
the billing tier: free, starter or pro.
GET /api/v1/plansEvery plan the token may read — owned plans plus active shared memberships. A plan-scoped token returns exactly one row. Ordered newest first.
curl -s "https://instantprd.web.id/api/v1/plans?limit=50" \
-H "Authorization: Bearer $INSTANTPRD_TOKEN"
{
"plans": [
{
"id": "…uuid…",
"title": "Bookly",
"status": "tasks_ready",
"language": "en",
"role": "owner",
"created_at": "2026-08-23T09:14:02.113Z",
"updated_at": "2026-08-23T10:02:55.900Z"
}
],
"next_cursor": "eyJrIjoiMjAyNi0wOC0yM…",
"limit": 50
}
role is your role on that plan: owner, editor or viewer.
GET /api/v1/plans/:planIdThe plan, its feature structure, and the full Markdown of the latest PRD version.
{
"plan": {
"id": "…uuid…", "title": "Bookly", "status": "tasks_ready", "language": "en",
"role": "owner", "idea": "A booking app for…", "is_public": false, "public_slug": null,
"created_at": "2026-08-23T09:14:02.113Z", "updated_at": "2026-08-23T10:02:55.900Z"
},
"structure": {
"features": [
{ "ref": "F-01", "title": "Booking", "description": "…", "phase": 1, "icon": "calendar",
"sort": 0, "subfeatures": ["Pick a slot", "Confirm"] }
]
},
"prd": { "version": 3, "content": "# 1. Overview\n…" }
}
prd is null until the PRD step has run.
GET /api/v1/plans/:planId/tasksThe full task specification — the same shape the CLI and the MCP server receive — plus the read-only
board flags id, verified, needs_review, files and updated_at. Returned in server execution
order, never re-sorted.
Filters, all optional and combinable:
curl -s "https://instantprd.web.id/api/v1/plans/$PLAN_ID/tasks?status=todo&layer=backend&limit=100" \
-H "Authorization: Bearer $INSTANTPRD_TOKEN"
{
"tasks": [
{
"id": "…uuid…",
"ref": "T-014",
"title": "Create the booking endpoint",
"description": "…",
"acceptance_criteria": [{ "text": "POST /bookings returns 201", "done": false }],
"test_plan": ["pnpm test bookings"],
"layer": "backend",
"priority": "core",
"page": null,
"estimate_hours": 2,
"depends_on": ["T-011"],
"status": "todo",
"feature": { "ref": "F-02", "title": "Booking", "phase": 1 },
"rejection_note": null,
"verified": false,
"needs_review": false,
"files": [],
"updated_at": "2026-08-23T10:02:55.900Z"
}
],
"next_cursor": null,
"limit": 100
}
An unknown status or layer is 422, with the allowed set in details.allowed.
GET /api/v1/plans/:planId/progress{
"total": 42,
"by_status": { "todo": 20, "doing": 1, "needs_review": 3, "done": 18, "failed": 0 },
"by_phase": [
{ "phase": 1, "total": 25, "done": 18, "verified": 15, "needs_review": 1 },
{ "phase": 2, "total": 17, "done": 0, "verified": 0, "needs_review": 2 }
],
"verified": 15,
"needs_review": 3
}
by_status always carries all five keys, zero-filled. verified counts the tasks whose evidence
was accepted — the quality number to chart next to done.
GET /api/v1/plans and GET /api/v1/plans/:planId/tasks are paginated.
CURSOR=""
while :; do
page=$(curl -s "https://instantprd.web.id/api/v1/plans?limit=100${CURSOR:+&cursor=$CURSOR}" \
-H "Authorization: Bearer $INSTANTPRD_TOKEN")
echo "$page" | jq -r '.plans[].id'
CURSOR=$(echo "$page" | jq -r '.next_cursor // empty')
[ -z "$CURSOR" ] && break
done
120 requests per minute per token, counted in a bucket of its own — separate from the agent API, so a busy dashboard can never starve a running coding agent.
Every response whose token was verified — including a 403, 404 or 422 — carries
X-RateLimit-Limit and X-RateLimit-Remaining. A 429 adds Retry-After, in seconds.
One envelope everywhere:
{ "error": "validation", "message": "Invalid `status`", "details": { "allowed": ["todo", "…"] } }
details appears only where it helps a client correct itself.
403 and 404 differA plan-scoped token used off its plan is always 403, decided before any database read — so a real
foreign plan id and a random UUID produce byte-identical responses, and the token cannot be used to
probe which plans exist. Everything else you have no role on is 404, identical to a plan that was
never created.
OPTIONS /api/v1/… → 204
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Max-Age: 86400
A wide origin is safe here and only here: these routes authenticate with a bearer token a browser
never attaches on its own, they never read cookies, Access-Control-Allow-Credentials is
deliberately absent, and only GET is allowed — so no origin can turn this into a state change or
ride someone's session.
The v1 in the path is the contract. Additive changes — new endpoints, new fields — land in v1; a
breaking change would ship as /api/v2/*. Ignore JSON fields you do not recognise.
To have an agent write code against a plan rather than read it, see Agent handoff for the CLI, or MCP server for the tool interface.
| Query | Values |
|---|
status | todo, doing, needs_review, done, failed |
layer | frontend, backend, db, integration |
feature | a feature ref, e.g. F-02 |
limit is 1–100, default 25. An out-of-range value is clamped (0 becomes 1, 5000 becomes
100); a non-integer is 422. The applied value is echoed back as limit.cursor is the next_cursor of the previous page, passed back verbatim (URL-encode it).
next_cursor is null on the last page, and there is never an empty trailing page.422, never a silent
full-table scan.error | HTTP | When |
|---|
unauthorized | 401 | no bearer header, or a malformed, unknown or revoked token |
forbidden | 403 | a plan-scoped token used on a different plan |
not_found | 404 | the plan does not exist, or you have no role on it — deliberately the same answer |
validation | 422 | bad limit, bad cursor, unknown filter value |
rate_limited | 429 | over the limit for this token |
server_error | 500 | unexpected; never carries internal detail |