AYA integration guide
AYA is a hub that fronts agent sessions and a queue of execution turns. Workers execute those turns on real agent CLIs and services (Claude Code, Codex, and others) running on a machine you control, pulling work rather than having it pushed at them. Dispatchers are the servers a worker pulls that work from — your own service, or the hub’s own built-in review and work-run queues. UIs read session history and steer turns — sending a message, watching it stream back, forking a conversation — over a browser session.
Those are the three ways to integrate. Pick the one that matches what you’re building:
Integrate as a dispatcher
You have your own service that decides what work exists, and you want a
worker to pull it from you rather than push a fixed request at the hub.
You implement the same small execution-pull contract the hub itself
implements — a worker configures your server as one of its own
[[execution_sources]] and polls you directly, never the other way
around — or, if you don’t want to run a full execution-pull server,
submit work runs directly to the hub instead and just serve its one
result-callback endpoint. See Integrate as a dispatcher.
Integrate as a worker
You have a machine that can run an agent CLI, and you want it to pull work
from AYA rather than have work pushed at it. You poll for a claimable turn,
drive it locally, and report events and a terminal outcome back. This is
what the reference worker (aya-helper) does. See
Integrate as a worker.
Integrate as a UI
You want to show someone their agent sessions — browse history, search across it, send a chat message naming a provider that’s currently being served (or a configured tier), fork a conversation where the harness supports it, and watch the reply arrive — without caring how any of it actually runs. You read a tree-shaped session/message model and drive turns through a session-cookie-gated chat-and-poll surface. See Integrate as a UI.
Five-line quickstart
# 1. Get a credential — an OIDC client-credentials grant (see
# Authentication). You end up with a bearer token.
export TOKEN="..."
# 2. Call an authenticated agent route with it — this one needs the
# agent:report scope.
curl -s https://hub.example.test/api/agent/turns \
-H "Authorization: Bearer $TOKEN"
# {"turns": [...], "total": 0, "limit": 50, "offset": 0}
# 3. Read the full contract — the worker-facing and service-integration
# surface, not the browser routes a UI uses (see Reference).
# https://hub.example.test/api/docs # interactive OpenAPI reference
curl -s https://hub.example.test/api/openapi.json # the spec itself
See Reference for the complete operation table and Authentication for how to get a credential in the first place.
Your first turn in ten minutes
The fastest way to see the whole loop — enqueue, claim, run, complete,
read the result — is to play both sides of it yourself against a real
deployment, using the hub’s own built-in review queue as the concrete
enqueue point (a real dispatcher’s own enqueue step is private to its own
storage, so there’s nothing to curl for it in general — this queue is
the one exception that’s fully self-contained and runnable end to end).
Get two credentials first: a review:run-scoped token ($REVIEW_TOKEN)
and an agent token ($TOKEN) — see Authentication. Step 2
claims WHATEVER’S next in the queue for the given provider, not
necessarily the turn you just made — run this against a deployment with
no other pending turns for that provider, or you may claim someone else’s.
Step 2 also assumes the deployment’s configured review profile uses
claude — ask your operator, or check
Run your own, and adjust
?providers= if it’s something else. Every step but the first captures
what it needs from the previous response with jq rather than hardcoding
it, so this is meant to be pasted and actually run, not just read.
# 1. Enqueue — stands in for "your own enqueue" in this walkthrough.
curl -s -X POST https://hub.example.test/api/integrations/turns \
-H "Authorization: Bearer $REVIEW_TOKEN" -H "Content-Type: application/json" \
-d '{"request_id": "first-turn-1", "purpose": "idea_review_v1", "input_version": 1,
"context": {"idea": {"id": "i1", "title": "Try AYA", "description": "A test idea."},
"messages": [], "omitted_message_count": 0,
"projects": [{"id": "p1", "name": "Test project", "description": ""}],
"linked_project_ids": ["p1"]}}'
# 202 {"request_id": "first-turn-1", "state": "queued", "replayed": false}
# (resubmitting this exact body is safe — same request_id, same input,
# replays as "replayed": true rather than queuing twice; changing the
# body under the same request_id instead gets a 409)
# 2. A worker claims it — this is the same call any worker polling this
# hub already makes on its own; run it yourself to see the shape.
# 204 with no body means nothing is pending for this provider right now.
CLAIM=$(curl -s "https://hub.example.test/api/agent/turns/next?providers=claude" \
-H "Authorization: Bearer $TOKEN")
echo "$CLAIM"
# {"turn_id": "101", "provider": "claude", "native_id": null, "text": "...",
# "model": null, "claim_token": "fence-1", "claim_generation": 1, ...}
TURN_ID=$(echo "$CLAIM" | jq -r .turn_id)
CLAIM_TOKEN=$(echo "$CLAIM" | jq -r .claim_token)
# 3. Post one event against that claim — a real review result, so step 5
# below actually reaches "ready" instead of "failed". The shape is
# exactly ReviewResult (see Integrate as a dispatcher's review-queue
# section): summary, questions, evidence, proposals. Built with jq so
# the nested JSON-as-a-string doesn't need hand escaping.
REVIEW_RESULT='{"summary":"Looks reasonable.","questions":[],"evidence":[],"proposals":[]}'
BODY=$(jq -n --arg token "$CLAIM_TOKEN" --arg delta "$REVIEW_RESULT" \
'{claim_token: $token, batch_id: "b1", events: [{type: "text", delta: $delta}]}')
curl -s -X POST "https://hub.example.test/api/agent/turns/$TURN_ID/events" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "$BODY"
# 200 {"ok": true}
# 4. Complete it.
curl -s -X POST "https://hub.example.test/api/agent/turns/$TURN_ID/done" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"claim_token\": \"$CLAIM_TOKEN\", \"ok\": true}"
# 200 {"ok": true}
# 5. Read the result back through the review queue you enqueued into.
curl -s https://hub.example.test/api/integrations/turns/first-turn-1 \
-H "Authorization: Bearer $REVIEW_TOKEN"
# {"request_id": "first-turn-1", "state": "ready",
# "result": {"summary": "Looks reasonable.", "questions": [], "evidence": [], "proposals": []}}
Steps 2-4 are exactly what Integrate as a worker documents in full — a real worker just does them in a loop, automatically, against whatever’s actually pending. Step 1 and step 5 are the one hub-hosted shortcut Integrate as a dispatcher describes; building your own is the rest of that page.
The two hosts
A deployment normally answers on two hostnames:
- The app host serves the product itself: the browser UI, OIDC login,
and every
/api/*route — the agent-facing surface this guide covers, and the browser-facing surface a UI integrates against. - The docs host (optional) serves this site at its own root, in
addition to — not instead of —
/docson the app host, which keeps working exactly as before whether or not a docs host is configured. Either way,/api/openapi.jsonand/api/docs(the interactive reference) are reachable from both hostnames, so links in this guide use root-relative paths that work no matter which host served the page you’re reading. See Run your own for how an operator configures this split.