Answer a pending chat approval
List the approvals a Chat's latest turn is waiting on, answer one, and confirm the resumed turn.
A Chat turn can pause when it needs a decision: a tool call the Project policy routes to a person, or an action the automation policy gates. This guide starts from an existing Chat, lists the approvals its latest turn is waiting on, answers one, and confirms the turn continued with the Developer API. The Developer API has no chat-creation route, so create the Chat in the product UI first.
Before you start
You need:
- a Project Chat whose latest turn is waiting on an approval;
- a personal API key with the
automationmanagecapability, which also covers theautomationreadcapability every Chat read requires; - the
chat.approval.answerandchat.readaction grants. Every Chat-scopedGETroute requireschat.read, so the list-then-answer sequence needs both; - the
projectsreadcapability to read the Project list.
Every Chat route acts as the key's issuing user, so the key must be personal: a
bot or service key is refused with 403 actor_not_available before the request
is processed.
See API keys to create a personal key, and API overview for capability levels, action grants, and the error contract.
Set the origin and key once for the examples:
export API_ORIGIN="https://api.factorai.ai"
export API_KEY="<your API key>"Steps
1. Resolve the team, Project, and Chat
Every chat route carries teamId and projectId in its path, so resolve them
from the Project list before reading chats:
PROJECT=$(curl -sS "$API_ORIGIN/v1/projects" \
-H "Authorization: Bearer $API_KEY" |
jq -c '.projects[] | select(.slug == "my-project")')
TEAM_ID=$(printf '%s' "$PROJECT" | jq -r '.teamId')
PROJECT_ID=$(printf '%s' "$PROJECT" | jq -r '.id')Then list the Project's chats and select the one waiting on the approval:
CHATS=$(curl -sS \
"$API_ORIGIN/v1/teams/$TEAM_ID/projects/$PROJECT_ID/chats" \
-H "Authorization: Bearer $API_KEY")
CHAT_ID=$(printf '%s' "$CHATS" |
jq -r '.chats[] | select(.title == "Release checklist") | .id')The response is { chats, offset, totalCount }, newest first. Each chat carries
id, title, updatedAt, and sometimes createdAt. Pass limit and offset
to page through it.
2. List the pending approvals
GET /v1/teams/{teamId}/projects/{projectId}/chats/{chatId}/approvals returns
the approvals the Chat's latest turn is waiting on. It requires the automation
read capability and the chat.read action grant.
APPROVALS=$(curl -sS \
"$API_ORIGIN/v1/teams/$TEAM_ID/projects/$PROJECT_ID/chats/$CHAT_ID/approvals" \
-H "Authorization: Bearer $API_KEY")
printf '%s' "$APPROVALS" | jq '.approvals'approvals is a union of two shapes:
{
"approvals": [
{
"kind": "chat_control",
"requestId": "req_01",
"title": "Write the release notes",
"body": "Create docs/releases/v2.md with the changelog.",
"toolUseId": "toolu_01",
"draft": true
},
{
"kind": "permission_request",
"requestId": "req_02"
}
]
}A chat_control approval carries display text: title, body, the
toolUseId it controls, and an optional draft flag. A permission_request
carries only kind and requestId; the recorded action it gates is resolved by
the same canonical approvals owner the product's approval inbox uses, so it has
no title, body, or toolUseId.
3. Choose a decision
The answer body accepts one decision:
| Decision | Effect |
|---|---|
approve | Allow this request once. |
always | Allow it and remember the grant for the same resource. |
deny | Reject the request. |
always records a durable grant for the same resource, so a later request that
the same policy gates on that resource is approved without another prompt. Use
it only for a resource you trust going forward.
4. Answer the approval
POST /v1/teams/{teamId}/projects/{projectId}/chats/{chatId}/approvals/{requestId}/answer
requires the automation manage capability and the chat.approval.answer
action grant, and the decision is audited under that action.
REQUEST_ID="req_01"
ANSWER=$(curl -sS -X POST \
"$API_ORIGIN/v1/teams/$TEAM_ID/projects/$PROJECT_ID/chats/$CHAT_ID/approvals/$REQUEST_ID/answer" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "decision": "approve" }')
printf '%s' "$ANSWER" | jq .A 201 response carries chatId and, for a chat_control approval,
turnExecutionId:
{
"chatId": "chat_01",
"turnExecutionId": "turn_01"
}A permission_request answer returns { "chatId": "chat_01" } with no
turnExecutionId: the decision is recorded, and the worker replays the gated
write and resumes the turn, so no turn id exists at answer time.
5. Confirm the turn continued
For a chat_control answer, turnExecutionId names the resumed turn. Track it
through Workflow and Run lifecycle, or read the
Chat's messages for new assistant output:
curl -sS \
"$API_ORIGIN/v1/teams/$TEAM_ID/projects/$PROJECT_ID/chats/$CHAT_ID/messages" \
-H "Authorization: Bearer $API_KEY" |
jq '.messages[-3:]'GET /v1/teams/{teamId}/projects/{projectId}/chats/{chatId}/messages returns
{ messages, offset, totalCount }, oldest first. Output that appears after the
approval is the resumed turn. For a permission_request, wait for the worker's
replay: the resumed output appears in the messages read, not in the answer
response.
Errors
Both routes return the same { "error": "<value>" } failure body. The list
route produces only the shared read errors; the answer route produces those plus
its own answer-only values.
| Status | Error value | Route | When |
|---|---|---|---|
401 | - | both | The request is missing a valid API key. |
403 | actor_not_available | both | The key is not a personal key that can act as a user. |
403 | - | both | The key lacks a capability, an action grant, or Project access. |
404 | project_not_found | both | The Project does not exist for the key. |
404 | chat_not_found | both | The Chat does not exist for the key. |
404 | approval_not_found | answer | The request is no longer pending on the Chat's latest turn. |
409 | chat_scope_conflict | both | The chat's stored scope conflicts with the resolved scope. |
409 | approval_already_answered | answer | The request was already answered with a different decision. |
409 | approval_dependency_unapproved | answer | A dependency approval this request needs is still unapproved. |
422 | chat_turn_rejected | answer | The resumed turn was rejected before it ran. |
503 | chat_storage_unavailable | both | Chat storage or the Chat route's configuration is unavailable. |
503 | chat_execution_unavailable | answer | No durable worker is available to resume the turn. |
A - marks a status that comes from the shared authentication or capability
layer rather than a chat-route error value. The list route returns 200; the
answer route returns 201 and adds 422, and its 404 also covers an approval
that is no longer pending. A failed list request never returns
approval_not_found, approval_already_answered,
approval_dependency_unapproved, chat_turn_rejected, or
chat_execution_unavailable, because only the answer route examines a specific
request or resumes a turn.
Next steps
- API overview - authentication, capabilities, and endpoint families
- API keys - create, scope, and revoke a key
- Start a project Run and read its result - dispatch a Project's agent and follow the Run
- Workflow and Run lifecycle - how Runs, revisions, and Attempts relate
- Command-line interface - install, authenticate, and automate the CLI