# Agent API

Use the organization-scoped HTTP API to create, configure and message persistent agents, retrieve replies and manage lifecycle and triggers.

Use the versioned HTTP API directly from your application. The [OpenAPI document](https://composal.ai/api/v1/openapi) is the canonical source for request schemas, response fields and operation IDs. Generate a client from it if you need types; the planned `composalai/agent-sdk` is not required and is not available yet.

## Manage sharing

Creation accepts optional `visibility: "private"`; the default is `"organization"`. Read `agent.visibility` and `agent.sharing_version` from the agent response. An owner or manager can change sharing with:

```http
PATCH /api/v1/organizations/acme/agents/AGENT/sharing
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{"expected_version": 7, "visibility": "organization"}
```

Echo the current integer `sharing_version` as `expected_version`; it is distinct from Git configuration/lifecycle versions. A stale version returns `409`. After a timeout, GET the agent to confirm its audience before submitting another change. This endpoint does not use an idempotency key. An organization member cannot change sharing or configuration simply because they can message the agent. Membership removal or switching to private revokes ungranted access and prevents further task execution for that requester.

Shared messages use the owner's configured tools and selected accounts. Only the owner can edit account grants. Provider-event consent includes the audience; visibility changes can require the owner to renew that consent. Organization sharing includes future members; membership is checked live for access.

## Authenticate and select an agent

Send a Composal personal access token in `Authorization: Bearer …`. The token acts as its user, within that user's organization membership and agent access. New agents default to organization sharing: members can view and message them, while only the owner and explicit managers can change settings. Private agents require an explicit grant even for organization administrators. Existing agents also become organization-shared at rollout; owners/managers can switch them to private afterward.

The base collection is:

```text
https://composal.ai/api/v1/organizations/{organization_id}/agents
```

`organization_id` accepts an accessible organization slug or public ID. An agent selector accepts its readable `url_slug`, legacy slug or `agent_…` public ID. Browser links use `/{organization}/agent/{url_slug}`; aliases are reserved within that organization and stay stable when the display name changes. Save returned public IDs in your integration. Private Git heads and task IDs are opaque values, not SQL IDs.

The examples below use Bash, `curl` and `jq`. Load `COMPOSAL_API_TOKEN` through your secret manager. This helper passes the token through stdin rather than a command-line argument; do not enable shell tracing around credentials.

```sh
export ORG=your-organization
export BASE="https://composal.ai/api/v1/organizations/$ORG/agents"
api() {
  curl --fail-with-body --silent --show-error --config - "$@" <<EOF
header = "Authorization: Bearer ${COMPOSAL_API_TOKEN:?Set COMPOSAL_API_TOKEN}"
header = "Content-Type: application/json"
EOF
}
```

## Create and activate

Create an agent using a name and configuration. A slug is generated automatically. Choose a stable idempotency key for this creation and retain the exact request file until its outcome is known.

```sh
cat > create-agent.json <<'JSON'
{"name":"Research assistant","configuration":{"instructions":"Research the topics I send and report your sources."}}
JSON
api "$BASE" --request POST \
  --header 'Idempotency-Key: research-agent-create-001' \
  --data-binary @create-agent.json > created-agent.json
export AGENT="$(jq -r '.agent.id' created-agent.json)"
api "$BASE/$AGENT" > agent.json
```

Creation returns `201`, or `202` with `agent.setup_pending: true` while private state initialization recovers. Follow `Retry-After` and repeat the **GET** until setup is ready. Do not create a second agent to bypass pending setup.

Read `agent.lifecycle_actions` and the returned versions. When `activate` is available, build the activation request from that snapshot:

```sh
jq '{expected_version: .agent.lifecycle_version,
     configuration_version: .agent.configuration_version}' agent.json > activate-agent.json
api "$BASE/$AGENT/activate" --request POST \
  --header 'Idempotency-Key: research-agent-activate-001' \
  --data-binary @activate-agent.json
```

Keep this file and key unchanged when retrying an uncertain activation. Reloading state and choosing a different version is a new operation with a new key.

## Send work and read replies

`main` is the default conversation. Supply another identifier to keep separate work in a different conversation. Sending creates an input; the response is not the agent's answer.

```sh
cat > message.json <<'JSON'
{"conversation_id":"main","text":"Describe your instructions and the tools available to you."}
JSON
api "$BASE/$AGENT/inputs" --request POST \
  --header 'Idempotency-Key: research-message-001' \
  --data-binary @message.json
api "$BASE/$AGENT/conversations/main" > conversation.json
jq '.conversation.inputs[] | {input_id: .input.id, task: .task}' conversation.json
```

A ready conversation response has `setup_pending: false` and a `conversation` object. Each input's `task` can be `null` before admission, then includes its `id`, `status`, `reason` and `result`. A completed task has a saved result. Check failures, cancellations and blocked states explicitly. Responses also include pending questions/approvals and the task stop control where available.

Poll this GET with a bounded backoff until the relevant task has an outcome. `next_before` selects older messages: pass it as `?before=…`; it is not a cursor for new activity. To follow new work, reread the latest page. Use separate conversation IDs when your application needs separate work contexts; steering inputs can join an existing task.

`GET /{id}/updates` provides an agent-wide feed of completed, failed, cancelled and blocked outcomes. Paginate older entries with `before=next_before`. `POST /{id}/updates/read` acknowledges a displayed sequence for the authenticated viewer; it does not change task execution.

## Configuration and versions

`PATCH /{id}/configuration` accepts `{expected_version, configuration}`. Use `agent.configuration_version` as the expected version. Configuration is a replacement document: start with the returned configuration, change the fields you intend, and preserve selected work repositories, connections, triggers and limits. Activate the returned new revision before using it for new work.

Activation uses `lifecycle_version` plus `configuration_version`; pause, resume and archive use `lifecycle_version`. These versions are opaque Git commit identities. Use returned `lifecycle_actions` and capability fields rather than inferring allowed actions from a status string.

Configuration includes instructions, skills, tools, work repositories, `connection_grants`, `trigger_bindings`, destinations, timezone, model policy, budgets and memory policy. Defaults are filled when creating a configuration; nested policy objects must follow the complete schema. Secrets do not belong in configuration or the agent repository.

## Endpoint reference

Paths below are relative to the agent collection. Read the generated OpenAPI schemas for exact bodies and pagination.

| Method | Path | Purpose |
| --- | --- | --- |
| GET / POST | `/` | List authorized agents / create a draft |
| GET | `/{id}` | Configuration, versions and current capabilities |
| PATCH | `/{id}/sharing` | Change organization/private audience using integer `sharing_version` |
| PATCH | `/{id}/configuration` | Save a new configuration revision |
| GET | `/{id}/revisions` | Read configuration revision history |
| POST | `/{id}/activate`, `/{id}/pause`, `/{id}/resume`, `/{id}/archive` | Change lifecycle with an expected version |
| GET / POST | `/{id}/inputs` | Read accepted inputs / send a message or bound reply |
| GET | `/{id}/conversations/{conversation}` | Read messages, task status and saved replies |
| GET | `/{id}/updates` | Read durable task outcomes |
| POST | `/{id}/updates/read` | Mark displayed outcomes read |
| POST | `/{id}/tasks/stop` | Request stopping a task using `task_id` and the conversation's `version` |
| POST | `/{id}/approvals` | Answer a pending approval using its exact binding |
| GET / PATCH | `/{id}/memory`, `/{id}/files` | Read or update managed memory/files with their versioned contracts |
| POST | `/{id}/schedule_preview`, `/{id}/webhooks/preview` | Preview proposed triggers without delivering work |

## Apps and incoming events

Connect accounts through the organization-level `/api/v1/organizations/{organization_id}/agent_connections` API. Custom MCP registration is `POST …/agent_connections/custom_mcp`; authorization is a separate step. Reference the returned account and current authority generation in `configuration.connection_grants` as `agent_connection_…:GENERATION`. Select only accounts the controlling user owns. Revocation or replacement is checked again at tool use.

Agent webhook management uses `GET/POST /{id}/webhooks` and `POST /{id}/webhooks/{endpoint_id}/rotate` or `/revoke`. Public delivery goes to the endpoint URL returned by creation, under `/api/webhooks/agents/{endpoint_id}`. Follow its signing and event schema; store the returned signing secret securely. Endpoint creation alone does not activate a trigger: save its binding and activate the revision.

Webhook deliveries use a different authentication scheme from management API calls:

- `X-Composal-Event-Id`: a stable event ID (1–128 letters, digits, underscores or hyphens).
- `X-Composal-Timestamp`: Unix time in seconds, within five minutes of server time.
- `X-Composal-Signature`: `v1=` followed by the lowercase hex HMAC-SHA256 of `timestamp + "." + event_id + "." + raw_body`, using the endpoint's signing key.

Send UTF-8 JSON with this envelope, limited to 64 KiB. Sign the exact bytes you send, including whitespace:

```json
{"schema":1,"type":"resource.changed","subject":"item-42","data":{"id":"item-42"}}
```

Keep the same event ID and body when retrying delivery; refresh the timestamp and signature if needed. Reusing an event ID with different content conflicts. A `202` delivery receipt is acceptance, not a completed agent task.

Supported platform events use `POST /{id}/events` with a configured `binding_id` and published `event_id`. Provider-event consent uses `GET /{id}/provider-sources` and `GET/POST /{id}/provider-grants`, with `POST /{id}/provider-grants/{grant_id}/revoke` for revocation. Follow returned eligibility and consent requirements; app tool access does not imply event delivery support.

## Errors and retries

- **401 / 403 / 404:** authenticate again or check the user's current access. Private agent selection can return 404 rather than disclose its existence.
- **409:** state changed or requires reconciliation. For a definite version conflict, reload and make a new decision with a new key. An unknown external effect is not permission to submit it again.
- **422:** fix the request using returned validation errors. A changed request needs a new key.
- **429 / 503:** follow `Retry-After` and use bounded retries. Preserve the same key, body and expected versions for the same intended mutation.
- **Network timeout:** acceptance is uncertain. Keep the original request/key and reconcile or retry that exact operation. Never generate a new key automatically.

Task failure is reported in conversation/outcome data even when its GET succeeds. A `201` message receipt, successful HTTP response or running worker does not prove the task completed. Stopping a task does not undo effects already started; uncertain usage and effects can remain under reconciliation.
