# The AQ API

> The AQ API is AQ's REST, MCP, and webhook surface: create workspaces, read events, PRs, and previews, run AQ Composer, and react via signed webhooks.

Updated October 20, 2026 (first published August 1, 2026) · by the AQ team · Canonical: https://aq.dev/docs/api/

The AQ API is the programmatic interface to AQ workspaces: a versioned REST API at `https://aq.dev/v1`, a hosted MCP server at `https://aq.dev/mcp`, and signed outbound webhooks, all operating on the same workspaces with the same permission model as the AQ dashboard. The REST API lists, creates, and closes workspaces, reads each workspace's event log, tracked pull requests, and preview URLs, and starts, steers, stops, and archives conversations with AQ Composer (the built-in agent). The MCP server exposes those same operations as tools, so any MCP-compatible agent can discover and call them directly, and webhooks push workspace activity to your service so it reacts without polling. The complete machine-readable contract is the OpenAPI 3.1 document at [undefined](https://aq.dev/v1/openapi.json).

Every surface authenticates with scoped API keys that act as the person who created them, and every REST write is retry-safe through idempotency keys. API keys, the REST API, webhooks, and the MCP server are part of the [Pro plan](https://aq.dev/pricing/). This guide takes you from a new API key to a working integration, including copy-paste tests that do not change data.

## Start here

| Surface | URL | Use it when |
| --- | --- | --- |
| REST API | `https://aq.dev/v1` | Your application needs predictable requests, responses, pagination, and retry-safe writes. |
| MCP server | `https://aq.dev/mcp` | An MCP-compatible agent should discover and use AQ tools directly. |
| Webhooks | Configured through `/v1/webhooks` | Your service should react to workspace, pull request, agent, Linear, comment, or runner events. |
| OpenAPI | [undefined](https://aq.dev/v1/openapi.json) | You need the complete machine-readable OpenAPI 3.1 contract or want to generate a client. |

The fastest safe test is: create a read-only key, call `GET /v1/me`, list workspaces, then ask the MCP server for its tool list. Those calls are all read-only.

## In this guide

- [Create a key and make the first read-only request](#quickstart)
- [Choose scopes and understand the permission model](#authentication)
- [Find the complete REST endpoint map](#rest-reference)
- [Implement workspace, event, and agent-event pagination](#pagination)
- [Retry REST writes safely with idempotency keys](#idempotency)
- [Create, inspect, and close a test workspace](#workspace-example)
- [Start, poll, steer, stop, and archive an AQ Composer run](#agent-runs)
- [Connect and test an MCP client](#mcp)
- [Register, verify, deduplicate, and troubleshoot webhooks](#webhooks)
- [Handle errors and rate limits](#errors)
- [Run the safe production smoke-test checklist](#smoke-test)

## Quickstart: authenticate and make your first request

### 1. Create an API key

Open [AQ Settings](https://aq.dev/settings), choose Connections, find API keys, and select Create key. Give the key a name that identifies its consumer, such as `release-bot` or `local-mcp-test`, then select only the scopes it needs.

Keys start with `aq_` and are shown exactly once. AQ stores only a SHA-256 hash of the key. Copy it into your password manager or secret store before leaving the creation screen.

### 2. Put the key in your shell without saving it in history

```
read -rsp "AQ API key: " AQ_API_KEY
export AQ_API_KEY
echo
```

Do not commit the key, put it in a query string, paste it into an issue, or expose it to browser-side JavaScript. Revoke a key immediately from Settings if it may have leaked.

### 3. Verify the key and its scopes

```
curl -fsS https://aq.dev/v1/me \
  -H "Authorization: Bearer $AQ_API_KEY" | jq
```

A successful response identifies the user and team the key acts as, along with the scopes attached to it:

```
{
  "user": {
    "id": "user-id",
    "email": "you@example.com",
    "name": "Your Name"
  },
  "tenant_id": "tenant-id",
  "account_id": "account-id",
  "scopes": ["workspaces:read", "events:read"],
  "key_id": "ak_..."
}
```

### 4. Make a read-only request

```
curl -fsS "https://aq.dev/v1/workspaces?limit=5" \
  -H "Authorization: Bearer $AQ_API_KEY" | jq
```

Workspace responses use stable, public field names such as `id`, `title`, `status`, `repo_id`, `branch_name`, `pr_url`, and timestamps. Runner identifiers, filesystem paths, credentials, and raw infrastructure errors are never included.

## Authentication, permissions, and scopes

Send the key as an HTTP Bearer token on every REST or MCP request:

```
Authorization: Bearer aq_your_key
```

An API key is personal, not a team-wide service account. It acts as the user who created it, sees only workspaces that user can access, and is re-checked against live team and account membership. Revoking the key, removing the user, or suspending the account stops access. API keys never inherit AQ host-operator privileges.

| Scope | Allows | Typical use |
| --- | --- | --- |
| `workspaces:read` | List and fetch workspaces and connected repos | Dashboards, inventory, MCP discovery |
| `workspaces:write` | Create and close workspaces | Issue intake, automation, workspace lifecycle |
| `events:read` | Read workspace event logs | Activity feeds and polling integrations |
| `prs:read` | Read pull requests tracked for a workspace | Release and review workflows |
| `previews:read` | Read preview status and URLs | QA links and review dashboards |
| `webhooks:manage` | List, create, and delete webhook endpoints | Event-driven integrations |
| `agent:run` | Start, inspect, steer, stop, and archive AQ Composer conversations | Agent orchestration from applications or other agents |
| `secrets:read` | List the names of the secrets the key's user can use (never values) | Checking which environment variables a work item will have |

Use separate keys for separate consumers. A read-only reporting job should not share a key with a system that creates workspaces or runs agents.

## REST API reference by task

Every endpoint except the public OpenAPI document requires a Bearer key. Methods and paths below are the current `v1` surface.

| Method | Path | Scope | Purpose |
| --- | --- | --- | --- |
| GET | `/v1/me` | Any valid key | Inspect the acting user, team, key id, and scopes. |
| GET | `/v1/secrets` | `secrets:read` | List secret names and scopes (organization and the user's personal secrets). Values are never returned. |
| GET | `/v1/repos` | `workspaces:read` | List repositories connected to the team. |
| GET | `/v1/workspaces` | `workspaces:read` | List visible workspaces, newest first, with filters (`status`, `repo_id`, `repo_name`, `query`). |
| GET | `/v1/workspaces/count` | `workspaces:read` | Count visible workspaces grouped by status, with the same filters. |
| POST | `/v1/workspaces` | `workspaces:write` | Create a workspace. |
| GET | `/v1/workspaces/{id}` | `workspaces:read` | Fetch one visible workspace. |
| POST | `/v1/workspaces/{id}/close` | `workspaces:write` | Close a workspace and stop its terminals and previews. |
| GET | `/v1/workspaces/{id}/events` | `events:read` | Read the append-only workspace event log. |
| GET | `/v1/workspaces/{id}/prs` | `prs:read` | List pull requests associated with a workspace. |
| GET | `/v1/workspaces/{id}/previews` | `previews:read` | Read preview status, visibility, and URLs. |
| POST | `/v1/workspaces/{id}/agent` | `agent:run` | Start an AQ Composer conversation with an initial task. |
| GET | `/v1/agent-sessions/{id}` | `agent:run` | Read agent status, budget state, spend, and timestamps. |
| GET | `/v1/agent-sessions/{id}/events` | `agent:run` | Poll ordered progress events using `after_seq`. |
| GET | `/v1/agent-sessions/{id}/messages` | `agent:run` | Read the conversation transcript. |
| POST | `/v1/agent-sessions/{id}/messages` | `agent:run` | Queue a follow-up or start the next turn now. `deliver=now` stops the current turn first. |
| DELETE | `/v1/agent-sessions/{id}/messages/{messageId}` | `agent:run` | Retract a queued message before a turn claims it. |
| PATCH | `/v1/agent-sessions/{id}/messages/order` | `agent:run` | Reorder the queued messages. |
| POST | `/v1/agent-sessions/{id}/messages/{messageId}/deliver` | `agent:run` | Run a queued message next, stopping the current turn if one is in flight. |
| POST | `/v1/agent-sessions/{id}/interrupt` | `agent:run` | Stop the current turn while keeping the conversation open. |
| POST | `/v1/agent-sessions/{id}/archive` | `agent:run` | Close the conversation tab while retaining its transcript. |
| POST | `/v1/agent-sessions/{id}/delegate` | `agent:run` | Hand a task from a conversation to a crew role thread (plan, build, review, test, research); the child's typed report reaches the parent. |
| GET | `/v1/delegations/{id}` | `agent:run` | Read a crew delegation: the child's state, what it runs on, its report, any open question. |
| GET | `/v1/webhooks` | `webhooks:manage` | List endpoints and discover all supported event types. |
| POST | `/v1/webhooks` | `webhooks:manage` | Create an endpoint and receive its signing secret once. |
| DELETE | `/v1/webhooks/{id}` | `webhooks:manage` | Delete an endpoint. |

Request and response schemas, field types, enums, limits, and error responses are defined in the [OpenAPI 3.1 document](https://aq.dev/v1/openapi.json).

## Pagination

Workspace lists use an opaque cursor. Pass the returned `next_cursor` unchanged to the next request:

```
curl -fsS \
  "https://aq.dev/v1/workspaces?limit=50&cursor=opaque-cursor" \
  -H "Authorization: Bearer $AQ_API_KEY"
```

Workspace events are returned newest first and use a timestamp cursor. Pass `next_before` back as the `before` query parameter. Agent progress events are different: they are ordered and carry a monotonic string `seq`. Pass the newest sequence you have seen as `after_seq` to fetch only later events.

## Retry-safe REST writes with idempotency keys

Every REST `POST` and `DELETE` requires an `Idempotency-Key` header of at most 255 characters. Generate one key for each logical action and keep it stable across retries. A UUID is a good default.

```
curl -fsS -X POST https://aq.dev/v1/workspaces \
  -H "Authorization: Bearer $AQ_API_KEY" \
  -H "Idempotency-Key: 9683499e-966d-4d4b-bc6c-05c9216da279" \
  -H "Content-Type: application/json" \
  -d '{"title":"Fix the auth bug","repo_id":"your-repo-id"}'
```

- A retry with the same method, path, body, key, and API key replays the stored response and adds `Idempotency-Replayed: true`.
- Reusing the key with a different body returns `422 idempotency_key_reused`.
- Retrying while the first request is still running returns `409 retry_in_progress`. Wait briefly and retry with the same key.
- Do not generate a fresh idempotency key merely because the client timed out. That would describe a new logical action and could create a duplicate.

## End-to-end REST example: create, inspect, and close a workspace

First choose a repository id:

```
curl -fsS https://aq.dev/v1/repos \
  -H "Authorization: Bearer $AQ_API_KEY" | jq '.repos[] | {id, name, default_branch}'
```

Create the workspace and save its id:

```
CREATE_RESPONSE=$(curl -fsS -X POST https://aq.dev/v1/workspaces \
  -H "Authorization: Bearer $AQ_API_KEY" \
  -H "Idempotency-Key: docs-smoke-create-1" \
  -H "Content-Type: application/json" \
  -d '{"title":"API smoke test","repo_id":"your-repo-id"}')

WORKSPACE_ID=$(printf '%s' "$CREATE_RESPONSE" | jq -r '.workspace.id')
printf '%s' "$CREATE_RESPONSE" | jq
```

Inspect its activity, pull requests, and previews:

```
curl -fsS "https://aq.dev/v1/workspaces/$WORKSPACE_ID/events" \
  -H "Authorization: Bearer $AQ_API_KEY" | jq

curl -fsS "https://aq.dev/v1/workspaces/$WORKSPACE_ID/prs" \
  -H "Authorization: Bearer $AQ_API_KEY" | jq

curl -fsS "https://aq.dev/v1/workspaces/$WORKSPACE_ID/previews" \
  -H "Authorization: Bearer $AQ_API_KEY" | jq
```

Close the test workspace when you are done:

```
curl -fsS -X POST \
  "https://aq.dev/v1/workspaces/$WORKSPACE_ID/close" \
  -H "Authorization: Bearer $AQ_API_KEY" \
  -H "Idempotency-Key: docs-smoke-close-1" | jq
```

## Run and steer AQ Composer over REST

AQ Composer (the built-in AQ agent) is a durable conversation attached to an active workspace. Starting a run queues work, and a worker normally begins it within seconds. The agent edits the workspace's worktree and reports progress through ordered events and transcript messages. It does not automatically push or open a pull request.

The key needs `agent:run`, the workspace must be active, and AQ Composer must be enabled for the team. It is included (beta) for every organization by default; a team can have it turned off. A disabled team receives `403 agent_disabled`.

The workspace also needs a prepared worktree, which happens the first time someone opens it in AQ. Starting a conversation in a never-opened workspace still succeeds, but the session immediately rests in `waiting_for_user` with `error_code` set to `workspace_not_ready` and a system message in the transcript explaining the fix. Open the workspace once, then send a message to resume. A temporarily unreachable runner behaves the same way with `error_code` set to `runner_unavailable`: nothing is lost, and the conversation resumes on the next message.

### Start a conversation

```
RUN_RESPONSE=$(curl -fsS -X POST \
  "https://aq.dev/v1/workspaces/$WORKSPACE_ID/agent" \
  -H "Authorization: Bearer $AQ_API_KEY" \
  -H "Idempotency-Key: agent-run-1" \
  -H "Content-Type: application/json" \
  -d '{"message":"Inspect the failing tests, explain the cause, and fix it."}')

AGENT_SESSION_ID=$(printf '%s' "$RUN_RESPONSE" | jq -r '.agent_session.id')
printf '%s' "$RUN_RESPONSE" | jq
```

Start, get, and interrupt respond with the session object. The fields your poller needs are `status`, `error_code`, and `credits_spent_micros`; `title` is the tab label the conversation gets in the workspace:

```
{
  "agent_session": {
    "id": "as_...",
    "workspace_id": "workspace-id",
    "title": "AQ",
    "status": "queued",
    "budget_state": "active",
    "credits_spent_micros": "0",
    "error_code": null,
    "created_at": "2026-08-02T18:20:00.000Z"
  }
}
```

### Poll progress incrementally

```
curl -fsS \
  "https://aq.dev/v1/agent-sessions/$AGENT_SESSION_ID/events?after_seq=0&limit=200" \
  -H "Authorization: Bearer $AQ_API_KEY" | jq
```

Persist the largest returned `seq` and use it as the next `after_seq`. The stream mirrors what the workspace tab shows: `message` entries, one `tool` event per tool call the agent makes while working, `checkpoint` markers for the worktree snapshot taken before each turn, and `status_changed` transitions. Read the conversation itself from the messages endpoint:

```
curl -fsS \
  "https://aq.dev/v1/agent-sessions/$AGENT_SESSION_ID/messages" \
  -H "Authorization: Bearer $AQ_API_KEY" | jq
```

| Status | Meaning | What the client should do |
| --- | --- | --- |
| `queued` | Waiting for a worker | Continue polling. |
| `running` | The current turn is active | Read events and messages; send a message only when you intend to steer. |
| `waiting_for_user` | The agent is resting or needs input | Read the transcript and send a reply to resume. If `error_code` is set, fix that first: `workspace_not_ready` means open the workspace once. |
| `paused` | The run is paused by a control or budget gate | Inspect `budget_state` and `error_code`. |
| `completed`, `failed`, `cancelled` | Terminal states | Stop polling. Start a new conversation for more work. |

### Reply, stop a turn, or archive the conversation

```
# Reply or steer. A reply to waiting_for_user resumes the run.
curl -fsS -X POST \
  "https://aq.dev/v1/agent-sessions/$AGENT_SESSION_ID/messages" \
  -H "Authorization: Bearer $AQ_API_KEY" \
  -H "Idempotency-Key: agent-reply-1" \
  -H "Content-Type: application/json" \
  -d '{"content":"Use the smaller fix and rerun the focused tests."}'

# Stop only the current turn. The conversation remains open.
curl -fsS -X POST \
  "https://aq.dev/v1/agent-sessions/$AGENT_SESSION_ID/interrupt" \
  -H "Authorization: Bearer $AQ_API_KEY" \
  -H "Idempotency-Key: agent-stop-1"

# Close the conversation tab. Its transcript remains readable by id.
curl -fsS -X POST \
  "https://aq.dev/v1/agent-sessions/$AGENT_SESSION_ID/archive" \
  -H "Authorization: Bearer $AQ_API_KEY" \
  -H "Idempotency-Key: agent-archive-1"
```

## Connect an MCP client

For the agent-facing quickstart (one-step connect for Claude Code, Claude and ChatGPT connectors, Cursor, and a paste-to-your-agent setup block), see [Connect your AI agent to AQ over MCP](https://aq.dev/docs/mcp/). The reference below covers the same surface in API terms.

AQ's MCP endpoint uses stateless Streamable HTTP with OAuth. In a client that supports remote MCP servers (as of October 2026 that includes Cursor, Claude, ChatGPT connectors, and others), add the URL and nothing else:

```
{
  "mcpServers": {
    "aq": {
      "url": "https://aq.dev/mcp"
    }
  }
}
```

The first time the client connects, your browser opens an AQ sign-in and consent screen. Approve, and the client receives its own access token, no key copying. Each connected app shows up in **Settings → Connections → API keys** (named `MCP: <client>`), and revoking it there disconnects the app immediately.

### Headless or key-based clients

If your client cannot run a browser OAuth flow, mint a key in Settings → Connections → API keys and send it as a Bearer header: the same key used by REST:

```
{
  "mcpServers": {
    "aq": {
      "url": "https://aq.dev/mcp",
      "headers": {
        "Authorization": "Bearer aq_your_key"
      }
    }
  }
}
```

Prefer a client feature that reads the token from an environment variable or secret store when one is available.

| MCP tool | Required scope | Effect |
| --- | --- | --- |
| `whoami` | *any key* | Identify the key: user, team, and scopes. |
| `list_workspaces` | `workspaces:read` | List visible workspaces with bounded cursor pagination and server-side filters (`status`, `repo_id`, `repo_name`, `query`). |
| `count_workspaces` | `workspaces:read` | Count workspaces grouped by status, with the same filters. The instant answer to "how many", with no paging. |
| `get_workspace` | `workspaces:read` | Fetch one workspace. |
| `list_repos` | `workspaces:read` | List connected repositories. |
| `create_workspace` | `workspaces:write` | Create a workspace (optionally from a specific `base_branch`). |
| `update_workspace` | `workspaces:write` | Edit a workspace's title and/or description. |
| `share_workspace` | `workspaces:write` | Set visibility and the share roster (owner-only). Omit `shared_with_emails` to keep the current roster; pass `[]` to revoke everyone. Newly-added teammates are notified. |
| `close_workspace` | `workspaces:write` | Close a workspace; requires `confirm=true`. |
| `reopen_workspace` | `workspaces:write` | Reopen a closed or archived workspace. |
| `get_workspace_events` | `events:read` | Read workspace activity. |
| `list_workspace_prs` | `prs:read` | List tracked pull requests. |
| `list_connections` | `agent:run` | List the connected CLI accounts of the person the key acts as, with the id to pass as `native_connection_id` when a harness has more than one. |
| `get_preview_url` | `previews:read` | Read preview status and URLs. |
| `list_agent_sessions` | `agent:run` | List a workspace's AQ Composer conversations. |
| `start_agent` | `agent:run` | Start an AQ Composer conversation. |
| `get_agent_run` | `agent:run` | Read status, the transcript tail, and recent events. |
| `send_agent_message` | `agent:run` | Steer or resume the agent. |
| `stop_agent` | `agent:run` | Stop the current turn while keeping the conversation open. |
| `archive_agent_session` | `agent:run` | Close a conversation tab; the transcript stays readable. |
| `delegate` | `agent:run` | Hand a task from a conversation to a crew role thread (plan, build, review, test, research) on the organization's role policy; the child's typed report reaches the parent. |
| `delegation_status` | `agent:run` | Read a crew delegation: the child's state, what it runs on, its report, any open question. |
| `list_webhooks` | `webhooks:manage` | List webhook endpoints and subscribable event types. |
| `create_webhook` | `webhooks:manage` | Create a webhook endpoint; the signing secret is returned exactly once. |
| `delete_webhook` | `webhooks:manage` | Delete a webhook endpoint; requires `confirm=true`. |

The MCP server only advertises tools allowed by the presented key's scopes, and annotates read-only versus destructive tools so clients can gate confirmations. Mutating tools accept an optional `idempotency_key` argument: a retry with the same key and identical arguments replays the first result instead of re-executing, so an agent that automatically retries can safely repeat `create_workspace` or `start_agent` without checking state first.

### Test MCP without installing a client

List the tools visible to your key:

```
curl -fsS https://aq.dev/mcp \
  -H "Authorization: Bearer $AQ_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  --data '{
    "jsonrpc":"2.0",
    "id":1,
    "method":"tools/list",
    "params":{}
  }' | jq
```

Call a read-only tool:

```
curl -fsS https://aq.dev/mcp \
  -H "Authorization: Bearer $AQ_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  --data '{
    "jsonrpc":"2.0",
    "id":2,
    "method":"tools/call",
    "params":{
      "name":"list_workspaces",
      "arguments":{"limit":5}
    }
  }' | jq
```

The server is stateless and POST-only. A browser visit or other `GET /mcp` request intentionally returns `405`. Raw MCP POSTs must accept both `application/json` and `text/event-stream`, even though AQ currently returns JSON responses.

## Receive signed webhooks

Webhook endpoints let a service react without polling. Production endpoints must be public HTTPS URLs. AQ rejects credentials in webhook URLs, private and link-local address ranges, internal hostnames, redirects, and hostnames that resolve to private addresses at delivery time.

### Create an endpoint

```
curl -fsS -X POST https://aq.dev/v1/webhooks \
  -H "Authorization: Bearer $AQ_API_KEY" \
  -H "Idempotency-Key: webhook-create-1" \
  -H "Content-Type: application/json" \
  -d '{
    "url":"https://example.com/webhooks/aq",
    "description":"Production activity feed",
    "event_types":["workspace.pr_opened","workspace.agent_error"]
  }' | jq
```

The response returns the endpoint and an `aqwh_` signing secret. The secret is shown once and is never retrievable again. Store it before discarding the response:

```
{
  "webhook": {
    "id": "whe_...",
    "url": "https://example.com/webhooks/aq",
    "event_types": ["workspace.pr_opened", "workspace.agent_error"],
    "created_at": "2026-08-02T18:20:00.000Z"
  },
  "secret": "aqwh_..."
}
```

Pass an empty `event_types` array, or omit it, to subscribe to every supported event. `GET /v1/webhooks` returns your endpoints as `webhooks` plus the complete supported list as `event_types`, so your integration can discover new event types without a docs check.

### Delivery format

```
POST /webhooks/aq
content-type: application/json
user-agent: AQ-Webhooks/1.0
aq-webhook-id: whd_...
aq-event-type: workspace.pr_opened
aq-signature: t=1785690000,v1=hex-hmac-sha256

{
  "id": "whd_...",
  "type": "workspace.pr_opened",
  "created_at": "2026-08-02T18:20:00.000Z",
  "data": {
    "id": "event-id",
    "workspace_id": "workspace-id",
    "type": "pr_opened",
    "payload": {
      "prNumber": 42,
      "prUrl": "https://github.com/org/repo/pull/42"
    },
    "created_at": "2026-08-02T18:20:00.000Z",
    "read_at": null
  }
}
```

Webhook `data` uses the same scrubbed public DTOs as REST. Treat event delivery as at least once: deduplicate using the stable `id` or `aq-webhook-id`.

### Verify the signature before parsing or processing

```
import crypto from "node:crypto";

export function verifyAqWebhook(rawBody, signature, secret) {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signature ?? "");
  if (!match) return false;

  const timestamp = Number(match[1]);
  const received = Buffer.from(match[2], "hex");
  const expected = Buffer.from(
    crypto.createHmac("sha256", secret)
      .update(String(timestamp) + "." + rawBody)
      .digest("hex"),
    "hex",
  );

  const fresh = Math.abs(Date.now() / 1000 - timestamp) <= 300;
  return fresh && received.length === expected.length
    && crypto.timingSafeEqual(received, expected);
}
```

Use the exact raw request bytes in the HMAC input, not parsed and re-serialized JSON. Reject stale timestamps to prevent replay, compare digests in constant time, and return a `2xx` only after your system has durably accepted the event.

### Event types

- **Lifecycle:** `workspace.created`, `workspace.closed`, `workspace.reopened`.
- **Agent:** `workspace.agent_error`, `workspace.agent_stopped`, `workspace.agent_stale`, `workspace.agent_idle`.
- **Pull requests:** `workspace.pr_opened`, `workspace.pr_merged`, `workspace.pr_closed`.
- **Linear and comments:** `workspace.linear_state_changed`, `workspace.comment_added`.
- **Runtime health:** `workspace.terminal_exited_unexpectedly`, `workspace.setup_hook_failed`, `workspace.prepare_failed`, `workspace.runner_lost`, `workspace.stranded`.

### Retries and endpoint health

AQ considers any `2xx` response successful and times out a delivery after 10 seconds. Failed deliveries receive up to six total attempts, with delays of 1 minute, 5 minutes, 30 minutes, 2 hours, and 12 hours. A delivery is dead-lettered after its final failed attempt. An endpoint is automatically disabled after 20 consecutive failed deliveries; create a new endpoint after fixing the receiver.

## Errors, limits, and troubleshooting

REST errors use a stable JSON envelope with a machine-readable snake_case code:

```
{"error":"insufficient_scope","scope":"events:read"}
```

| Status or error | Meaning | What to do |
| --- | --- | --- |
| `401 unauthorized` | The Bearer header is missing, malformed, revoked, or no longer backed by active membership. | Call `/v1/me`, check the header, then create a new key if needed. |
| `403 insufficient_scope` | The key is valid but cannot perform this operation. | Create a least-privilege replacement key with the returned scope. |
| `403 agent_disabled` | The team has AQ Composer turned off. | Ask AQ to enable the built-in agent for the team. |
| `400 idempotency_key_required` | A `POST` or `DELETE` arrived without an `Idempotency-Key` header. | Send the header on every REST write. See the idempotency section above. |
| `404 not_found` | The resource does not exist or is not visible to the acting user. | Check the id and the user's workspace access. AQ intentionally does not distinguish the two cases. |
| `409 retry_in_progress` | The first request using this idempotency key has not finished. | Wait briefly and retry the identical request with the same key. |
| `409 session_archived` | The conversation tab was closed with the archive endpoint. | The transcript stays readable by id. Start a new conversation for more work. |
| `409 run_finished` | The conversation is already in a terminal state. | Read its final transcript and start a new conversation for more work. |
| `422 idempotency_key_reused` | The same key was used with a different request body. | Use the original body, or use a new key for a genuinely new action. |
| `429 rate_limited` | The key exceeded the default 240 requests per minute, or an agent concurrency gate was reached. | Honor `Retry-After` and add backoff with jitter. |
| MCP `405` | The client sent GET or DELETE to the stateless endpoint. | Use a Streamable HTTP MCP client or POST JSON-RPC messages. |
| MCP `406` | The raw client did not accept both required media types. | Send `Accept: application/json, text/event-stream`. |

The default API and MCP limit is 240 requests per minute per key. Responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. A `429` also includes `Retry-After`.

## A safe production smoke-test checklist

1. Create a temporary key with only `workspaces:read`.
2. Call `GET /v1/me` and verify the user, team, and scopes.
3. Call `GET /v1/workspaces?limit=1`.
4. POST an MCP `tools/list` request and confirm only read tools are advertised.
5. Call the MCP `list_workspaces` tool.
6. Revoke the temporary key and confirm `GET /v1/me` returns `401`.

Only after those read-only checks pass should you create a write-scoped key and run the create-and-close workspace example. Agent tests change files and may consume credits, so run them in a disposable workspace and only after confirming the team has AQ Composer enabled.

## Complete reference

Import [https://aq.dev/v1/openapi.json](https://aq.dev/v1/openapi.json) into an OpenAPI client generator, API client, or contract test suite for the complete field-level reference. For MCP, `tools/list` is the live source of truth because it returns exactly the tools allowed by the presented key.

## Frequently asked questions

### What is the AQ API?

The AQ API is the programmatic interface to AQ workspaces: a versioned REST API at `https://aq.dev/v1`, a hosted MCP server at `https://aq.dev/mcp`, and signed outbound webhooks. All three operate on the same workspaces with the same permission model as the AQ dashboard, authenticate with scoped `aq_` API keys, and are fully described by the OpenAPI 3.1 document at `/v1/openapi.json`.

### What can you do with the AQ API?

List, create, and close workspaces; read workspace event logs, tracked pull requests, and preview status and URLs; start, poll, steer, stop, and archive AQ Composer conversations; and register webhook endpoints so your service reacts to workspace, agent, pull request, Linear, comment, and runner events without polling. Typical integrations are dashboards, issue intake automation, activity feeds, agent orchestration, and event-driven workflows.

### Does AQ have an API?

Yes. AQ exposes a versioned REST API at `https://aq.dev/v1`, signed webhooks for workspace events, and an MCP server at `https://aq.dev/mcp`. The machine-readable OpenAPI 3.1 reference lives at [/v1/openapi.json](https://aq.dev/v1/openapi.json).

### How do I get an AQ API key?

Create one in Settings under API keys. Keys start with `aq_`, are shown once at creation, and act as you inside your team: a key sees exactly the workspaces you can see, with the scopes you chose at creation.

### Does AQ have an MCP server?

Yes, at `https://aq.dev/mcp` (Streamable HTTP). Any compatible MCP client can list workspaces, create new ones, read event logs, fetch preview URLs, and use the AQ Composer tools allowed by its scopes. Clients that support OAuth connect with just the URL (your browser opens an AQ consent screen once), and headless clients can authenticate with the same `aq_` API key as REST.

### What are the AQ API rate limits?

240 requests per minute per key by default. When a key exceeds the limit, the API returns 429 with a `Retry-After` header. Responses also report the limit, remaining requests, and reset time in rate-limit headers.

### Does the AQ REST API use the same permissions as the dashboard?

Yes. An API key acts as the user who created it and can only see workspaces that user can access. AQ also checks the key's explicit scopes. Removing the user from the team, suspending the account, or revoking the key stops access.

### Can an MCP client start and steer an AQ coding agent?

Yes. A key with the agent:run scope can list_agent_sessions, start_agent, get_agent_run, send_agent_message, stop_agent, and archive_agent_session. AQ Composer must also be enabled for the team, and the target workspace must be active.

### Are AQ webhooks delivered exactly once?

No. Treat delivery as at least once. AQ retries failures, so receivers should verify the signature and deduplicate using the stable delivery id in the payload or aq-webhook-id header before applying side effects.

### How should I retry an AQ API mutation after a timeout?

Send the identical REST request with the same Idempotency-Key. AQ replays the original response instead of executing the mutation twice. Do not generate a new key for a timeout retry. MCP mutating tools offer the same guarantee through their optional idempotency_key argument, which shares the REST slot store.

---

About AQ: AQ is the multiplayer coding harness where engineering teams run AI coding agents like Claude Code and Codex together: shared live terminals, a code editor, and app previews, in your own cloud. Start at [aq.dev](https://aq.dev/).
