Agents Agent-to-agent (A2A) Agent cards & errors

Agent cards & errors

Agent cards (discovery)

Every non-retired agent has an A2A 1.0 discovery document, generated from the agent's current state rather than hand-maintained — there's nothing to edit and nothing that can drift out of sync with the agent it describes:

curl https://api.forgebench.ai/v1/agents/invoice-classifier/.well-known/agent-card.json \
-H "Authorization: Bearer $ANY_TENANT_KEY"

# the tenant's whole registry (no name/id — every live agent's card)
curl https://api.forgebench.ai/v1/agents/cards \
-H "Authorization: Bearer $ANY_TENANT_KEY"
  • url always points at /v1/agents/{id}/tasks on the control plane, not at wherever the agent actually runs — so a caller that discovers this card, whether that's another tenant's agent, a partner's orchestrator, or an enterprise agent registry crawling for cards, can only ever reach the agent through the gate.
  • skills mirror the agent's live MCP tool bindings. Revoking a tool drops it from the card on the next read, the same way it drops off the allowlist.
  • The card is signed (EdDSA, canonical JSON, the same key that signs usage reports). Verify against GET /v1/billing/export/pubkey if you're consuming cards from a deployment you don't control.

Errors specific to A2A

Beyond the general refusal codes on Errors and refusals, opening, reading, or answering a task has its own shapes:

StatusCauseBody
403not_permitted — returned for every reason a caller can be refused: unknown callee, another tenant's callee, no binding, a revoked binding, a binding pending approval, or a paused/retired callee. All of it collapses to the same response on purpose — telling a caller which of those is true would leak facts about an agent it has no relationship with. The precise reason still lands on the audit log.{"detail":{"code":"not_permitted","message":"not permitted to call that agent"}}
404The task doesn't exist, or exists but you're not its caller or callee.{"detail":"task not found"}
409task_not_working — a reply came in for a task that isn't currently working (already finished, or canceled while a callee was mid-handler).{"detail":{"code":"task_not_working","state":"canceled"}}
422A pull result posted "state": "working". Only a push endpoint's immediate response may say working; a pull reply must be completed, failed, or input_required.{"detail":"a pull result must be completed, failed, or input_required"}

Since not_permitted collapses every cause into one code, the fastest way to tell them apart while you're setting things up is checking GET /v1/agents/{agent_id}/agents on the caller, or its Calls tab in the console:

  • no entry for the callee under outbound → no binding was ever granted
  • entry present but showing pending approval → waiting on the callee's owner
  • entry present but shown revoked → the binding was cut

Next