Every endpoint is under /v1. All responses are JSON unless noted. Every endpoint except
/v1/health and /v1/auth/* requires Authorization: Bearer <github_token> (PRD §9.1).
These commands are copy-pasteable. They are the same calls exercised by test/e2e.sh, which
runs the whole lifecycle against mock GitHub + mock model services and asserts every response.
export BASE=http://localhost:8080/v1curl -s "$BASE/health"
# {"status":"ok","service":"equalify-iris"}GitHub OAuth is the only auth mechanism, and the same token opens PRs on close, so the consent
screen requests repo scope. By default the service uses a bundled OAuth App — you don't
create or configure anything; just run the device flow below and approve in your browser.
# Begin: returns a code to type into the browser.
dev=$(curl -s -X POST "$BASE/auth/github/device")
echo "$dev"
# {"device_code":"...","user_code":"WXYZ-1234","verification_uri":"https://github.com/login/device","expires_in":900,"interval":5}
# Open the verification_uri in a browser and enter the user_code, then poll:
DEVICE_CODE=$(echo "$dev" | jq -r .device_code)
curl -s -X POST "$BASE/auth/github/device/poll" \
-H 'content-type: application/json' \
-d "{\"device_code\":\"$DEVICE_CODE\"}"
# while pending -> 202 {"status":"pending","error":"authorization_pending"}
# once approved -> 200 {"access_token":"gho_...","token_type":"bearer"}
export TOKEN=gho_xxx # paste the access_token
export AUTH="Authorization: Bearer $TOKEN"GET /v1/auth/github/start -> 302 redirect to the GitHub consent screen
GET /v1/auth/github/callback -> 200 {"access_token":"gho_...","token_type":"bearer"}
/start issues a state value and redirects to GitHub; after the user approves, GitHub calls
/callback?code=...&state=... and the service returns the access token.
curl -s -H "$AUTH" "$BASE/me"{
"github_login": "iris-tester",
"github_user_id": 4242,
"upstream_repo": "https://github.com/example/iris",
"fork_repo": null,
"defaults": { "max_review_iterations": 3 }
}fork_repo is null until the first /close (the fork is created lazily).
multipart/form-data. Repeat images once per file; the order of the parts is the
processing order (not the filename). config is an optional JSON part.
create=$(curl -s -X POST -H "$AUTH" "$BASE/sessions" \
-F "images=@page-001.png" \
-F "images=@page-002.png" \
-F 'config={"max_review_iterations":3}')
echo "$create"
export SID=$(echo "$create" | jq -r .session_id){ "session_id": "ses_01HXYZ...", "status": "queued", "image_count": 2, "created_at": "..." }Accepted file types: PNG, JPEG, TIFF, WebP, and PDF. A PDF is rasterized server-side into one image per page (in page order) and processed like any other page sequence. Total pages (across all parts) are capped per deployment.
The pipeline runs asynchronously; poll until status is ready_for_review (or failed).
curl -s -H "$AUTH" "$BASE/sessions/$SID" | jq{
"session_id": "ses_01HXYZ...",
"status": "running",
"phase": "extraction",
"iterations_completed": 0,
"iterations_max": 3,
"image_count": 2,
"created_at": "...",
"updated_at": "..."
}A simple wait loop:
until [ "$(curl -s -H "$AUTH" "$BASE/sessions/$SID" | jq -r .status)" = "ready_for_review" ]; do
sleep 2
donecurl -s -H "$AUTH" "$BASE/sessions/$SID/output" -o output.htmltext/html with provenance comments intact (@source, @agent, @fragment, @reconciled).
Returns 409 while the session is still running.
Triggers a new run within the same session, with the feedback injected as a top-level
instruction to every agent (PRD §7.12). The prior output is snapshotted to
sessions/<id>/history/ so it can be reverted to.
curl -s -X POST -H "$AUTH" "$BASE/sessions/$SID/feedback" \
-H 'content-type: application/json' \
-d '{"feedback":"The footnote on page 4 was inlined as body text. Keep footnotes distinct."}'
# 202 {"session_id":"ses_...","status":"running","phase":"triage"}Then poll status again as in step 4.
curl -s -H "$AUTH" "$BASE/sessions/$SID/logs"application/x-ndjson — one JSON object per line (agent calls with git-SHA / inline-content
version pinning, model-call timing, no-content signals, phase transitions).
A machine-readable health summary distilled from the run log — built for maintainers, human or AI, to spot what's slow or stuck.
curl -s -H "$AUTH" "$BASE/sessions/$SID/diagnostics" | jq{
"session_id": "ses_...",
"status": "running",
"phase": "extraction",
"started_at": "2026-05-22T16:25:01Z",
"elapsed_ms": 92000,
"in_flight": {
"agent": "table", "model": "us.anthropic.claude-sonnet-4-6",
"provider": "bedrock", "capability": "vision",
"since": "2026-05-22T16:26:12Z", "waiting_ms": 41000
},
"phase_durations_ms": { "triage": 8200, "extraction": 60100 },
"model_calls": { "count": 7, "failed": 0, "total_ms": 51000, "avg_ms": 7285, "max_ms": 14300 },
"by_agent": { "image_analysis": { "count": 1, "total_ms": 8200, "max_ms": 8200 } },
"slowest_calls": [ { "agent": "table", "model": "...", "capability": "vision", "duration_ms": 14300, "ok": true } ],
"errors": []
}The key field for "is it hung?" is in_flight: a non-null value with a large waiting_ms
means a model call started and hasn't returned (the likely culprit). slowest_calls and
phase_durations_ms show where time goes; errors lists failed calls.
curl -s -H "$AUTH" "$BASE/sessions?limit=20"
curl -s -H "$AUTH" "$BASE/sessions?status=ready_for_review"{ "sessions": [ { "session_id": "ses_...", "status": "ready_for_review",
"image_count": 2, "created_at": "...", "updated_at": "..." } ], "next_cursor": null }Paginate by passing cursor=<next_cursor>.
Locks the output and deletes tmp/<id>/. Requires status = ready_for_review (else 409).
Contributions are handled automatically during the run (see below), so close does not open PRs.
curl -s -X POST -H "$AUTH" "$BASE/sessions/$SID/close"{ "session_id": "ses_...", "status": "closed" }When the extractor encounters content a dedicated specialist agent would handle better than the
general pass, Iris drafts that agent and automatically files a labeled GitHub issue
(iris-agent-suggestion) on the upstream repo containing the agent code + context. This is
server-side and requires a configured service token (IRIS_GITHUB_TOKEN); it never publishes
under end users' identities, and it is a no-op when no token is set. There is no PR/fork flow.
All errors share one shape:
{ "error": { "code": "invalid_state", "message": "Human-readable description", "details": {} } }Common codes: unauthorized (401), session_not_found (404), invalid_state (409),
invalid_request (400), pr_failed (502).
./test/e2e.sh # boots mocks + Iris, runs all of the above via curl, asserts each step