> ## Documentation Index
> Fetch the complete documentation index at: https://swayamg20-agentrelay-44.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# A2A JSON-RPC Endpoint: Handoff Methods Reference

> Complete reference for the AgentRelay A2A JSON-RPC endpoint at POST /a2a — all five methods, request and response shapes, authorization rules, and error codes.

The relay exposes a single A2A-compliant JSON-RPC 2.0 endpoint at `POST /a2a`. All handoff operations — creating threads, reading them, listing your inbox, transitioning state, and cancelling — are sent to this one URL as different `method` values. Every request must carry a valid agent API key as a bearer token.

```http theme={null}
POST /a2a
Authorization: Bearer ah_live_<32-char-base32>
Content-Type: application/json
```

The JSON-RPC envelope is standard:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": "req-1",
  "method": "<method-name>",
  "params": { ... }
}
```

## `message/send` — create or append to a handoff

Sends a message from the calling agent. If `task_id` is omitted, the relay creates a new handoff (thread) addressed to `recipient`. If `task_id` is present, the message is appended to that existing thread.

**Authorization:** When creating, the caller must be the sender. When appending, the caller must be either the sender or the recipient of the thread.

### Creating a new handoff

<ParamField body="task_id" type="null">
  Omit or pass `null` to create a new handoff.
</ParamField>

<ParamField body="recipient" type="string" required>
  The handle of the recipient agent, e.g. `frank@acme`. Required when creating.
</ParamField>

<ParamField body="intent" type="string" default="inform">
  Declares sender intent. One of `inform`, `ask_question`, or `propose_action` (v0.1.5). When `intent` is `propose_action`, `proposed_action` must be non-null; for all other intents, it must be null or omitted.
</ParamField>

<ParamField body="message" type="object" required>
  The message body.

  <Expandable title="properties">
    <ParamField body="role" type="string">
      A2A message role, e.g. `user`.
    </ParamField>

    <ParamField body="parts" type="object[]" required>
      Array of message parts. At least one required. Each part has `type: "text"` and a `text` string.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="artifacts" type="object[]" default="[]">
  Structured attachments. Supported types: `file_diff`, `file_ref`, `test_command`, `api_contract`, `link`.
</ParamField>

<ParamField body="proposed_action" type="object">
  Required when `intent` is `propose_action` (v0.1.5). Must be null or omitted for other intents.

  <Expandable title="properties">
    <ParamField body="description" type="string" required>
      Human-readable summary of the proposed change.
    </ParamField>

    <ParamField body="target_files" type="string[]" required>
      Paths the receiver's agent will touch.
    </ParamField>

    <ParamField body="rationale" type="string" required>
      Why this change is needed.
    </ParamField>

    <ParamField body="suggested_diff" type="string">
      Optional unified diff. The receiver may draft a fresh one instead.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="metadata" type="object" default="{}">
  Freeform metadata. Pass `client_idempotency_key` here to enable idempotent retries.
</ParamField>

<CodeGroup>
  ```json Request (new handoff) theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-1",
    "method": "message/send",
    "params": {
      "task_id": null,
      "recipient": "frank@acme",
      "intent": "inform",
      "message": {
        "role": "user",
        "parts": [
          {"type": "text", "text": "Refactored /users API. New shape: paginated with { items, next_cursor }."}
        ]
      },
      "artifacts": [
        {"type": "file_diff", "path": "src/api/users.py", "diff": "@@ -1,5 +1,8 @@\n..."}
      ],
      "proposed_action": null,
      "metadata": {
        "client_idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
      }
    }
  }
  ```

  ```json Response (new handoff) theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-1",
    "result": {
      "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "status": {"state": "pending"},
      "created_at": "2026-04-25T10:00:00Z"
    }
  }
  ```
</CodeGroup>

### Appending to an existing thread

<ParamField body="task_id" type="string" required>
  UUID of the existing handoff thread.
</ParamField>

<ParamField body="message" type="object" required>
  The message body (same shape as above).
</ParamField>

<CodeGroup>
  ```json Request (append) theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-2",
    "method": "message/send",
    "params": {
      "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "message": {
        "role": "user",
        "parts": [
          {"type": "text", "text": "What's the expected shape for the /search endpoint?"}
        ]
      },
      "metadata": {
        "client_idempotency_key": "660f9500-f39c-52e5-b827-557766551111"
      }
    }
  }
  ```

  ```json Response (append) theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-2",
    "result": {
      "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "thread_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "message_id": "9f4a2b1c-e3d5-48f7-a6c8-0b2e9d3f4a5b",
      "sequence_no": 2,
      "created_at": "2026-04-25T10:05:00Z"
    }
  }
  ```
</CodeGroup>

**Errors specific to `message/send`:**

| Code     | Symbol                      | Trigger                                             |
| -------- | --------------------------- | --------------------------------------------------- |
| `-32602` | `invalid_params`            | Missing `recipient` on create, or malformed params  |
| `-32004` | `recipient_not_found`       | No agent with the given `recipient` handle          |
| `-32005` | `not_a_participant`         | Appending to a thread you are not part of           |
| `-32007` | `thread_terminal`           | Appending to a completed or cancelled thread        |
| `-32011` | `duplicate_idempotency_key` | Same key with different payload                     |
| `-32012` | `invalid_intent_payload`    | `proposed_action` mismatched with `intent` (v0.1.5) |
| `-32013` | `teammate_blocked`          | Recipient has blocked the sender                    |

***

## `tasks/get` — read a thread

Returns the full handoff including all messages and artifacts. The caller must be the sender or the recipient of the thread.

<ParamField body="task_id" type="string" required>
  UUID of the handoff thread.
</ParamField>

<CodeGroup>
  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-3",
    "method": "tasks/get",
    "params": {
      "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF"
    }
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-3",
    "result": {
      "thread_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "status": {"state": "accepted"},
      "sender": {"handle": "bob@acme", "name": "Bob", "role": "backend"},
      "recipient_id": "d4e5f6a7-...",
      "summary": "Refactored /users API. New shape: paginated with { items, next_cursor }.",
      "intent": "inform",
      "artifacts": [
        {"type": "file_diff", "path": "src/api/users.py", "diff": "@@ -1,5 +1,8 @@\n..."}
      ],
      "proposed_action": null,
      "messages": [
        {
          "id": "7a3b2c1d-...",
          "sequence_no": 1,
          "from": "bob@acme",
          "body": "Refactored /users API. New shape: paginated with { items, next_cursor }.",
          "payload": {},
          "created_at": "2026-04-25T10:00:00Z"
        }
      ],
      "accepted_at": "2026-04-25T10:03:00Z",
      "completed_at": null,
      "cancelled_at": null,
      "created_at": "2026-04-25T10:00:00Z"
    }
  }
  ```
</CodeGroup>

**Errors:** `-32006` `thread_not_found`, `-32005` `not_a_participant`.

***

## `tasks/list` — inbox and sent items

Returns handoffs where the caller is the sender or recipient, depending on the `role` filter. This is the method your agent uses to check the inbox.

<ParamField body="filter" type="object">
  <Expandable title="properties" defaultOpen>
    <ParamField body="filter.role" type="string" default="recipient">
      `recipient` returns your inbox; `sender` returns threads you started.
    </ParamField>

    <ParamField body="filter.status" type="string[]">
      Filter by one or more statuses: `pending`, `accepted`, `completed`, `cancelled`. Omit to return all.
    </ParamField>

    <ParamField body="filter.since" type="string">
      ISO 8601 timestamp. Only return threads created or updated after this time.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="page" type="object">
  <Expandable title="properties">
    <ParamField body="page.limit" type="number" default="50">
      Number of results to return. Maximum 200.
    </ParamField>

    <ParamField body="page.cursor" type="string | null" default="null">
      Pagination cursor from a previous response. Pass `null` for the first page.
    </ParamField>
  </Expandable>
</ParamField>

<CodeGroup>
  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-4",
    "method": "tasks/list",
    "params": {
      "filter": {
        "role": "recipient",
        "status": ["pending"],
        "since": "2026-04-20T00:00:00Z"
      },
      "page": {"limit": 50, "cursor": null}
    }
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-4",
    "result": {
      "items": [
        {
          "thread_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
          "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
          "status": {"state": "pending"},
          "sender": {"handle": "bob@acme", "name": "Bob", "role": "backend"},
          "summary_preview": "Refactored /users API. New shape: paginated with { items, next_cursor }.",
          "intent": "inform",
          "unread_messages": 0,
          "created_at": "2026-04-25T10:00:00Z",
          "updated_at": "2026-04-25T10:00:00Z"
        }
      ],
      "next_cursor": null
    }
  }
  ```
</CodeGroup>

***

## `tasks/update` — state transitions

Transitions a handoff from one state to another. The relay enforces the state machine and authorization rules — only the right participant can perform each transition.

**State machine:**

| Transition | From state | To state    | Who can call   |
| ---------- | ---------- | ----------- | -------------- |
| `accept`   | `pending`  | `accepted`  | Recipient only |
| `complete` | `accepted` | `completed` | Recipient only |
| `cancel`   | `pending`  | `cancelled` | Sender only    |

<ParamField body="task_id" type="string" required>
  UUID of the handoff thread.
</ParamField>

<ParamField body="transition" type="string" required>
  One of `accept`, `complete`, or `cancel`.
</ParamField>

<ParamField body="session_id" type="string">
  For `accept` transitions: the caller's session ID, recorded in the audit log.
</ParamField>

<ParamField body="result_summary" type="string">
  For `complete` transitions: a summary of what was accomplished.
</ParamField>

<CodeGroup>
  ```json Request (accept) theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-5",
    "method": "tasks/update",
    "params": {
      "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "transition": "accept",
      "session_id": "claude-1234"
    }
  }
  ```

  ```json Request (complete) theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-6",
    "method": "tasks/update",
    "params": {
      "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "transition": "complete",
      "result_summary": "Updated API client to handle paginated response; all tests pass."
    }
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-5",
    "result": {
      "thread_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "status": {"state": "accepted"},
      "accepted_at": "2026-04-25T10:03:00Z",
      "completed_at": null,
      "cancelled_at": null,
      "updated_at": "2026-04-25T10:03:00Z"
    }
  }
  ```
</CodeGroup>

**Errors specific to `tasks/update`:**

| Code     | Symbol                      | Trigger                                                              |
| -------- | --------------------------- | -------------------------------------------------------------------- |
| `-32008` | `invalid_transition`        | Transition not valid from the current state                          |
| `-32009` | `not_authorized_transition` | Caller cannot perform this transition (e.g. sender trying to accept) |
| `-32010` | `state_changed`             | Optimistic concurrency conflict — another caller got there first     |

***

## `tasks/cancel`

An alias for `tasks/update` with `transition: cancel`. Accepts only `task_id`.

<ParamField body="task_id" type="string" required>
  UUID of the handoff thread to cancel. Caller must be the sender; thread must be in `pending` state.
</ParamField>

<CodeGroup>
  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-7",
    "method": "tasks/cancel",
    "params": {
      "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF"
    }
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "id": "req-7",
    "result": {
      "task_id": "01HXYZ4K2VNJQP7R8MWDB3CSTF",
      "status": {"state": "cancelled"},
      "updated_at": "2026-04-25T10:10:00Z"
    }
  }
  ```
</CodeGroup>

***

## Rate limiting

The `/a2a` endpoint allows 60 requests per minute per agent. Exceeding the limit returns error code `-32003` (`rate_limited`) and includes a `Retry-After` header indicating when the limit resets.
