Early access: your sandbox is free, with $5 of AQ Composer credits every month. Your own subscriptions stay unmetered. Start free

aq.dev / docs/api

The AQ API

The AQ API is the programmatic interface to AQ workspaces. With it, your applications and agents can list, create, and close workspaces, read each workspace's event log, tracked pull requests, and preview URLs, start, steer, stop, and archive conversations with AQ Composer (the built-in agent), and react to workspace activity through signed webhooks instead of polling. It comes as three production surfaces that operate on the same workspaces and the same permission model: a versioned REST API for deterministic application workflows, a hosted MCP server so MCP-compatible agents can discover and call the same operations as tools, and signed outbound webhooks for event-driven systems. Every surface authenticates with scoped API keys that act as the person who created them, every REST write is retry-safe through idempotency keys, and the complete contract is machine-readable OpenAPI 3.1. This guide takes you from a new API key to a working integration, including copy-paste tests that do not change data.

Start here

SurfaceURLUse it when
REST APIhttps://aq.dev/v1Your application needs predictable requests, responses, pagination, and retry-safe writes.
MCP serverhttps://aq.dev/mcpAn MCP-compatible agent should discover and use AQ tools directly.
WebhooksConfigured through /v1/webhooksYour service should react to workspace, pull request, agent, Linear, comment, or runner events.
OpenAPI/v1/openapi.jsonYou 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

Quickstart: authenticate and make your first request

1. Create an API key

Open AQ 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": "[email protected]",
    "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.

ScopeAllowsTypical use
workspaces:readList and fetch workspaces and connected reposDashboards, inventory, MCP discovery
workspaces:writeCreate and close workspacesIssue intake, automation, workspace lifecycle
events:readRead workspace event logsActivity feeds and polling integrations
prs:readRead pull requests tracked for a workspaceRelease and review workflows
previews:readRead preview status and URLsQA links and review dashboards
webhooks:manageList, create, and delete webhook endpointsEvent-driven integrations
agent:runStart, inspect, steer, stop, and archive AQ Composer conversationsAgent orchestration from applications or other agents

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.

MethodPathScopePurpose
GET/v1/meAny valid keyInspect the acting user, team, key id, and scopes.
GET/v1/reposworkspaces:readList repositories connected to the team.
GET/v1/workspacesworkspaces:readList visible workspaces, newest first.
POST/v1/workspacesworkspaces:writeCreate a workspace.
GET/v1/workspaces/{id}workspaces:readFetch one visible workspace.
POST/v1/workspaces/{id}/closeworkspaces:writeClose a workspace and stop its terminals and previews.
GET/v1/workspaces/{id}/eventsevents:readRead the append-only workspace event log.
GET/v1/workspaces/{id}/prsprs:readList pull requests associated with a workspace.
GET/v1/workspaces/{id}/previewspreviews:readRead preview status, visibility, and URLs.
POST/v1/workspaces/{id}/agentagent:runStart an AQ Composer conversation with an initial task.
GET/v1/agent-sessions/{id}agent:runRead agent status, budget state, spend, and timestamps.
GET/v1/agent-sessions/{id}/eventsagent:runPoll ordered progress events using after_seq.
GET/v1/agent-sessions/{id}/messagesagent:runRead the conversation transcript.
POST/v1/agent-sessions/{id}/messagesagent:runQueue a follow-up or start the next turn now. deliver=now stops the current turn first.
DELETE/v1/agent-sessions/{id}/messages/{messageId}agent:runRetract a queued message before a turn claims it.
PATCH/v1/agent-sessions/{id}/messages/orderagent:runReorder the queued messages.
POST/v1/agent-sessions/{id}/messages/{messageId}/deliveragent:runRun a queued message next, stopping the current turn if one is in flight.
POST/v1/agent-sessions/{id}/interruptagent:runStop the current turn while keeping the conversation open.
POST/v1/agent-sessions/{id}/archiveagent:runClose the conversation tab while retaining its transcript.
GET/v1/webhookswebhooks:manageList endpoints and discover all supported event types.
POST/v1/webhookswebhooks:manageCreate an endpoint and receive its signing secret once.
DELETE/v1/webhooks/{id}webhooks:manageDelete an endpoint.

Request and response schemas, field types, enums, limits, and error responses are defined in the OpenAPI 3.1 document.

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
StatusMeaningWhat the client should do
queuedWaiting for a workerContinue polling.
runningThe current turn is activeRead events and messages; send a message only when you intend to steer.
waiting_for_userThe agent is resting or needs inputRead the transcript and send a reply to resume. If error_code is set, fix that first: workspace_not_ready means open the workspace once.
pausedThe run is paused by a control or budget gateInspect budget_state and error_code.
completed, failed, cancelledTerminal statesStop 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

AQ's MCP endpoint uses stateless Streamable HTTP with OAuth. In a client that supports remote MCP servers (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 toolRequired scopeEffect
list_workspacesworkspaces:readList visible workspaces with bounded cursor pagination.
get_workspaceworkspaces:readFetch one workspace.
list_reposworkspaces:readList connected repositories.
create_workspaceworkspaces:writeCreate a workspace.
close_workspaceworkspaces:writeClose a workspace; requires confirm=true.
get_workspace_eventsevents:readRead workspace activity.
list_workspace_prsprs:readList tracked pull requests.
get_preview_urlpreviews:readRead preview status and URLs.
start_agentagent:runStart an AQ Composer conversation.
get_agent_runagent:runRead status, the transcript tail, and recent events.
send_agent_messageagent:runSteer or resume the agent.
stop_agentagent:runStop the current turn while keeping the conversation open.

The MCP server only advertises tools allowed by the presented key's scopes. Webhook management is REST-only. REST idempotency headers do not apply to MCP tool calls, so a client that automatically retries a mutating tool should first check current state before repeating create_workspace or start_agent.

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_comment, 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 errorMeaningWhat to do
401 unauthorizedThe 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_scopeThe key is valid but cannot perform this operation.Create a least-privilege replacement key with the returned scope.
403 agent_disabledThe team has AQ Composer turned off.Ask AQ to enable the built-in agent for the team.
400 idempotency_key_requiredA POST or DELETE arrived without an Idempotency-Key header.Send the header on every REST write. See the idempotency section above.
404 not_foundThe 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_progressThe first request using this idempotency key has not finished.Wait briefly and retry the identical request with the same key.
409 session_archivedThe conversation tab was closed with the archive endpoint.The transcript stays readable by id. Start a new conversation for more work.
409 run_finishedThe conversation is already in a terminal state.Read its final transcript and start a new conversation for more work.
422 idempotency_key_reusedThe 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_limitedThe key exceeded the default 240 requests per minute, or an agent concurrency gate was reached.Honor Retry-After and add backoff with jitter.
MCP 405The client sent GET or DELETE to the stateless endpoint.Use a Streamable HTTP MCP client or POST JSON-RPC messages.
MCP 406The 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 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.

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 use start_agent, get_agent_run, send_agent_message, and stop_agent. 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 tool calls do not carry REST idempotency headers, so verify state before automatically retrying a mutating MCP tool.