Per ora la documentazione per sviluppatori è solo in inglese. Tutto il resto del sito è in italiano.

A2A (Agent2Agent)

Another agent can find 50heads from its agent card, send it a question, and get back a task that completes with what the heads answered. It is the same account, sign-in, spend caps and tasks as the MCP server.

Being introduced: the agent card and the endpoint answer 404 until A2A is on for your account. Ask support to turn it on.

The agent card

https://mcp.50heads.com/.well-known/agent-card.json describes the agent: its skills, how to sign in and the endpoint, https://mcp.50heads.com/a2a (JSON-RPC 2.0 over HTTPS). It speaks A2A 1.0, and 0.3 for older peers: send A2A-Version: 0.3 for the 0.3 card, or read https://mcp.50heads.com/.well-known/agent.json. The endpoint takes both versions' method names (SendMessage and message/send, and so on) and answers in the shapes of the one used. Push notifications, streaming (SendStreamingMessage and SubscribeToTask, as Server-Sent Events) and the authenticated extended card (GetExtendedAgentCard) are supported. The same methods are also served over the HTTP+JSON binding at https://mcp.50heads.com/a2a/rest (for example POST /message:send and GET /tasks/{id}; 0.3 peers use the /v1 paths).

Signing in and scopes

The same credentials as the MCP server: an OAuth access token from https://auth.50heads.com (authorization code with PKCE, the device code flow, or client credentials for an organization's service account), or a 50heads API key as a bearer token. Tokens are for the resource https://mcp.50heads.com/mcp, which is the default when a client leaves the resource parameter out; any other resource is refused. A missing token is a 401 with a WWW-Authenticate challenge; a missing scope is a 403 that names it.

MethodScopes
SendMessage (estimate or ask), CancelTaskquestions:write
SendStreamingMessagequestions:write
GetTask, ListTasks, SubscribeToTaskquestions:read
CreateTaskPushNotificationConfig, DeleteTaskPushNotificationConfigquestions:read, webhooks:write
GetTaskPushNotificationConfig, ListTaskPushNotificationConfigsquestions:read, webhooks:read

Prices show in the account's currency with account:read. All scopes: questions:read, questions:write, templates:read, account:read, webhooks:read, webhooks:write, apps:read, apps:write, organization:read, organization:write, members:read, members:write, audit:read, events:read.

Estimate first

estimate is the default skill and never spends. Send text, a goal such as “Which of these two names sounds like a bakery”, and 50heads drafts a question; or send a data part {"question": {...}} with the same question object as the MCP ask tool. The reply is a message, not a task: the price, the credits and any fixes, and the question to send back to ask. Show a person the price before you ask when one is present.

Ask

Set "skill": "ask" in the message metadata (or in the data part) and send the question object. The reply is a task whose id is the question id: the same task the MCP ask tool returns, so you can read it from either protocol or the REST API. The messageId is the idempotency key: send the same message again and you get the same task, charged once. A contextId you send is kept as the question's own reference; without one, the context is the task id.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "6f1c0a52-8d1e-4f3b-9a51-0b8f1d2c7e44",
      "role": "ROLE_USER",
      "parts": [
        {
          "data": {
            "skill": "ask",
            "question": {
              "type": "single_choice",
              "text": "Which name sounds most like a bakery?",
              "language": "en",
              "options": [
                {
                  "label": "Crumb & Co"
                },
                {
                  "label": "Steelworks"
                }
              ],
              "n": 50
            }
          },
          "mediaType": "application/json"
        }
      ]
    },
    "configuration": {
      "returnImmediately": true
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "task": {
      "id": "q_8k2f",
      "contextId": "q_8k2f",
      "status": {
        "state": "TASK_STATE_WORKING",
        "message": {
          "messageId": "status-q_8k2f-working",
          "role": "ROLE_AGENT",
          "parts": [
            {
              "text": "0 of 50 answered"
            }
          ]
        },
        "timestamp": "2026-10-13T10:00:00.000Z"
      },
      "metadata": {
        "questionId": "q_8k2f",
        "status": "live",
        "answered": 0,
        "heads": 50,
        "creditsReserved": 1000,
        "creditsSpent": 0
      }
    }
  }
}

A2A 1.0 waits by default until the task finishes or needs a person, for up to ten minutes, then answers with the task as it is. Set returnImmediately to get the task at once, then poll GetTask or set a push notification config. A2A 0.3 returns at once unless blocking is true.

Task states

StateWhen
TASK_STATE_SUBMITTEDScheduled, not live yet.
TASK_STATE_WORKINGLive: heads are answering. metadata.answered counts the answers so far.
TASK_STATE_INPUT_REQUIREDAn organization's spend approval policy holds the ask until a person decides.
TASK_STATE_COMPLETEDComplete, or closed with fewer answers than asked for (the rest is refunded). Carries the results.
TASK_STATE_CANCELEDCanceled. The answers not given are refunded; the results so far are attached.
TASK_STATE_REJECTEDReview refused the question, or an approver denied it, or nobody approved it in time. Nothing is spent.

Results

A completed or canceled task carries one artifact, results: a data part with the results object (the same one the MCP results tool returns: how many chose each option, the leader and how strong its lead is, written reasons) and a text part summarizing it. Results never identify the people who answered.

Spend approvals

When an organization's policy holds an ask, the task is TASK_STATE_INPUT_REQUIRED. The credits are held and nothing goes to heads. metadata.approval.approveUrl is where an approver decides, in the 50heads app or on the web; a message to the task does not approve it. Approved, the task goes on working; denied or expired, it is rejected and the credits are released.

Cancel and list

CancelTask stops a task that has not finished and reports metadata.refundCredits; a finished task is TaskNotCancelableError. ListTasks lists the account's tasks, newest first, and filters by status, contextId and statusTimestampAfter; artifacts come only with includeArtifacts, 20 tasks a page. totalSize counts the tasks on the page.

Push notifications

Set a push notification config on a task (in SendMessage's configuration, or with CreateTaskPushNotificationConfig) and 50heads POSTs each change of state to your https URL: live, waiting for approval, approved, completed, canceled or rejected. Up to five configs on a task. Each push carries:

  • Authorization: <scheme> <credentials> when the config has authentication;
  • X-A2A-Notification-Token, the config's token (made up and returned when you leave it out);
  • webhook-id, webhook-timestamp and webhook-signature (Standard Webhooks), signed with the secret whsec_ + base64(token).
{
  "statusUpdate": {
    "taskId": "q_8k2f",
    "contextId": "q_8k2f",
    "status": {
      "state": "TASK_STATE_COMPLETED",
      "message": {
        "messageId": "msg_evt_3kd9",
        "role": "ROLE_AGENT",
        "parts": [
          {
            "text": "Complete. Call GetTask for the results."
          }
        ]
      },
      "timestamp": "2026-10-13T10:12:00.000Z"
    },
    "metadata": {
      "event": "question.completed",
      "questionId": "q_8k2f",
      "creditsSpent": 1000,
      "refundCredits": 0
    }
  }
}

A 0.3 config gets the task object instead. Then call GetTask for the results. Deliveries are retried for about a day with backoff, like webhooks. You can also manage configs with a key under /v1/a2a/tasks/{taskId}/push-configs (see the API reference).

import { createHmac, timingSafeEqual } from "node:crypto";

// Every push is signed with Standard Webhooks headers. The secret is "whsec_" + base64(token):
// the token you set on the push notification config (or the one 50heads made and returned).
export function verifyPush(rawBody: string, headers: Headers, token: string, toleranceSec = 300): boolean {
  const id = headers.get("webhook-id");
  const timestamp = headers.get("webhook-timestamp");
  const signatures = headers.get("webhook-signature");
  if (!id || !timestamp || !signatures) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSec) return false;
  const expected = createHmac("sha256", Buffer.from(token, "utf8"))
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest();
  return signatures.split(" ").some((entry) => {
    const [version, sig] = entry.split(",");
    const given = Buffer.from(sig ?? "", "base64");
    return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
  });
}

Errors

JSON-RPC errors with A2A's codes: -32001 task not found (including another account's), -32002 not cancelable, -32004 unsupported (a message to, or a subscription on, a finished task, or a streaming method sent without a stream), -32005 a file part (put image URLs in the question object), -32009 an A2A version other than 1.0 or 0.3, and -32602 invalid parameters. 50heads' own refusals (not enough credits, a spend cap, refused content, a pause) are -32000. error.data holds google.rpc.ErrorInfo objects whose reason names the case, such as SPEND_CAP_EXCEEDED, and google.rpc.BadRequest field violations.

Limits

1 MB request bodies, one request at a time (no batches). 60 requests a minute per connection and 10 asks a minute, shared with the MCP server.

Keyboard shortcuts

Press ? on any page to see these. They are off while you type in a box.

General

Show keyboard shortcuts
?
Search help
/
Ask a question
n
Close menus
Esc

Go to

Home
gthenh
Pricing
gthenp
How it works
gthenw
How we verify
gthenv
For AI agents
gthena
For teams
gthent
Earn
gthene
Blog
gthenb
API docs
gthend
Your account
gtheno