REST API Examples¶
The SynthOrg REST API is mounted at /api/v1 on the backend service (default port 3001). Every endpoint requires authentication; the JWT is delivered as an HttpOnly Set-Cookie header by /auth/login, so subsequent calls authenticate by carrying the cookie back, not by attaching an Authorization: Bearer header. The response envelope is a typed ApiResponse<T> or PaginatedResponse<T>. This guide shows the 10 most common operations.
The base URL placeholder $BASE defaults to http://localhost:3001. Examples assume jq is installed for response inspection.
Authenticate¶
curl¶
# Login. -c writes the session cookie to a jar; -b on every subsequent
# call reads it back. The response body carries only metadata
# (expires_in, must_change_password); the JWT is in Set-Cookie.
curl -s -c cookies.txt -X POST $BASE/api/v1/auth/login \
-H "Content-Type: application/json" \
--data '{"username":"admin","password":"admin"}' | jq
Python (httpx)¶
import httpx
# httpx.Client persists cookies on its ``.cookies`` jar between calls.
client = httpx.Client(base_url="http://localhost:3001")
resp = client.post("/api/v1/auth/login", json={"username": "admin", "password": "admin"})
resp.raise_for_status()
# Token is in client.cookies now; every subsequent client.get/post
# carries it back automatically.
JavaScript (fetch)¶
// credentials: 'include' both sends and accepts cookies. In a browser
// this works against same-origin or CORS-allowed targets; in Node 18+
// fetch use undici's cookie jar via dispatchers (see node docs).
const resp = await fetch('http://localhost:3001/api/v1/auth/login', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'admin' }),
})
const { data: session } = await resp.json()
console.log('session expires in', session.expires_in, 'seconds')
1. List agents¶
const r = await fetch(`${base}/api/v1/agents`, { credentials: 'include' })
const { data: agents } = await r.json()
Returns a paginated envelope; the meta.next_cursor field drives the next page.
2. Create a task¶
curl -s -b cookies.txt -X POST "$BASE/api/v1/tasks" \
-H "Content-Type: application/json" \
--data '{"title":"Build a sample","description":"Smoke test","type":"development","project":"'"$PROJECT_ID"'","created_by":"'"$AGENT_NAME"'"}'
resp = client.post(
"/api/v1/tasks",
json={
"title": "Build a sample",
"description": "Smoke test",
"type": "development",
"project": project_id,
"created_by": agent_name,
},
)
task = resp.json()["data"]
const r = await fetch(`${base}/api/v1/tasks`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Build a sample', description: 'Smoke test', type: 'development', project: projectId, created_by: agentName }),
})
const { data: task } = await r.json()
3. Get a task¶
4. List artifacts for a task¶
5. Submit a client request¶
curl -s -b cookies.txt -X POST "$BASE/api/v1/requests" \
-H "Content-Type: application/json" \
--data '{"client_id":"c-1","requirement":{"title":"Ship the thing","description":"Make it work","acceptance_criteria":["Tests pass"]}}'
resp = client.post(
"/api/v1/requests",
json={
"client_id": "c-1",
"requirement": {
"title": "Ship the thing",
"description": "Make it work",
"acceptance_criteria": ["Tests pass"],
},
},
)
6. Approve a client request¶
The approve endpoint walks the request through the intake engine (when in SUBMITTED status) or finalises a previously-scoped request.
7. Fetch budget utilisation¶
overview = client.get("/api/v1/analytics/overview").json()["data"]
print(f"Budget used: {overview['budget_used_percent']:.1f}%")
8. Approve a pending approval¶
Approvals are decided through dedicated /approve and /reject endpoints (there is
no combined /decide route). /approve accepts an optional comment; /reject
requires a mandatory reason.
curl -s -b cookies.txt -X POST "$BASE/api/v1/approvals/$APPROVAL_ID/approve" \
-H "Content-Type: application/json" \
--data '{"comment":"Canary signal clean."}'
resp = client.post(
f"/api/v1/approvals/{approval_id}/approve",
json={"comment": "Canary signal clean."},
)
# To reject instead (reason is mandatory):
# client.post(f"/api/v1/approvals/{approval_id}/reject", json={"reason": "Needs rework."})
9. Subscribe to the live event WebSocket¶
The WebSocket uses a two-step ticket handshake: exchange your session for a one-time ticket, then send it as the first frame after the socket opens.
// 1. Exchange the session cookie for a one-time WebSocket ticket.
const ticketResp = await fetch(`${base}/api/v1/auth/ws-ticket`, {
method: 'POST',
credentials: 'include',
})
const { data: { ticket } } = await ticketResp.json()
// 2. Open the socket and authenticate with the ticket on the first frame.
// Derive ws/wss from the API base so TLS is preserved: an https base
// yields wss://. Plain ws:// is acceptable only for a localhost base; in
// any deployment the ticket and event data travel in-band and ws:// would
// expose them to network observers.
const ws = new WebSocket(`${base.replace(/^http/, 'ws')}/api/v1/ws`)
ws.onopen = () => {
ws.send(JSON.stringify({ action: 'auth', ticket }))
}
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
if (msg.action === 'auth_ok') {
// 3. Once authenticated, subscribe to channels.
ws.send(JSON.stringify({ action: 'subscribe', channels: ['tasks', 'approvals'] }))
return
}
console.log('[event]', msg.event_type, msg.payload)
}
The server's auth acknowledgement frame is {"action":"auth_ok"}; once seen, the
channels you subscribed to deliver events in real time. See the
WebSocket Models section of the API reference for
the full handshake and event-type catalogue.
10. Query the project brain¶
The project brain records a project's decisions, open questions, blockers, risks, dependencies, and plan revisions. The read endpoints are available whenever persistence and a memory backend are wired.
# Current-state blockers for a project, newest first.
blockers = client.get(
"/api/v1/projects/proj-abc123/brain",
params={"entry_kind": "blocker", "status": "blocked", "limit": 20},
).json()
# Semantic search across all entries.
hits = client.get(
"/api/v1/projects/proj-abc123/brain/search",
params={"q": "payment integration risk", "limit": 5},
).json()
11. Steer a running project¶
Mid-flight steering injects a hint or redirect into a project without stopping it. A redirect forces affected agents to re-plan at their next safe boundary; supersede_mode controls how obsolete tasks are cancelled.
result = client.post(
"/api/v1/cockpit/steering",
json={
"project_id": "proj-abc123",
"kind": "redirect",
"text": "Use Postgres instead of MongoDB for all persistence work",
"supersede_task_ids": ["task_xyz789"],
"supersede_mode": "explicit",
},
).json()
directive_id = result["data"]["directive_id"]
12. Resume a pending interrupt¶
When an agent pauses for human input (a tool approval or a clarification request), it raises an interrupt. Poll for pending interrupts, then resume one with a decision or a response.
# List pending interrupts (optionally filter by ``session_id``).
pending = client.get("/api/v1/interrupts").json()
interrupt_id = pending["data"][0]["id"]
# Approve a tool-approval interrupt.
client.post(
f"/api/v1/interrupts/{interrupt_id}/resume",
json={"decision": "approve", "feedback": "Looks correct, proceed."},
)
# Or answer an information-request interrupt instead:
# client.post(
# f"/api/v1/interrupts/{interrupt_id}/resume",
# json={"response": "Target the EU region."},
# )
The streaming counterpart to this polling API is the Server-Sent Events stream at /api/v1/events/stream; the polling endpoints above are the fallback when a long-lived SSE connection is impractical.
Pagination¶
List endpoints return PaginatedResponse<T>:
{
"data": [...],
"meta": {
"limit": 50,
"next_cursor": "eyJsYXN0X2lkIjoidGFzay0xMjMifQ==",
"has_more": true
}
}
To fetch the next page: pass ?cursor=<value> to the same endpoint. Stop when has_more is false.
Error envelopes¶
Errors follow RFC 9457:
{
"type": "synthorg/not-found",
"title": "Task not found",
"status": 404,
"detail": "Task '123e4567-e89b-12d3-a456-426614174000' not found",
"code": "RESOURCE_NOT_FOUND",
"category": "client_error"
}
The code field is the typed ErrorCode enum (see docs/reference/errors.md). Clients can switch on the enum without parsing prose.