Agent cards & errors
Agent cards (discovery)
Every non-retired agent has an A2A 1.0 discovery document. It's generated from the agent's current state, so there's nothing to edit and nothing to drift out of sync:
import os
import httpx
headers = {"Authorization": f"Bearer {os.environ['ANY_TENANT_KEY']}"}
r = httpx.get(
"https://api.forgebench.ai/v1/agents/invoice-classifier/.well-known/agent-card.json",
headers=headers,
)
r.raise_for_status()
card = r.json()
# the tenant's whole registry (no name/id — every live agent's card)
r = httpx.get("https://api.forgebench.ai/v1/agents/cards", headers=headers)
r.raise_for_status()
cards = r.json()const headers = { Authorization: "Bearer " + process.env.ANY_TENANT_KEY };
let r = await fetch(
"https://api.forgebench.ai/v1/agents/invoice-classifier/.well-known/agent-card.json",
{ headers },
);
if (!r.ok) throw new Error("HTTP " + r.status + ": " + (await r.text()));
const card = await r.json();
// the tenant's whole registry (no name/id — every live agent's card)
r = await fetch("https://api.forgebench.ai/v1/agents/cards", { headers });
if (!r.ok) throw new Error("HTTP " + r.status + ": " + (await r.text()));
const cards = await r.json();import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient http = HttpClient.newHttpClient();
ObjectMapper json = new ObjectMapper();
String auth = "Bearer " + System.getenv("ANY_TENANT_KEY");
HttpResponse<String> r = http.send(
HttpRequest.newBuilder(URI.create(
"https://api.forgebench.ai/v1/agents/invoice-classifier/.well-known/agent-card.json"))
.header("Authorization", auth)
.GET()
.build(),
HttpResponse.BodyHandlers.ofString());
if (r.statusCode() / 100 != 2) throw new IllegalStateException("HTTP " + r.statusCode() + ": " + r.body());
Object card = json.readValue(r.body(), Object.class);
// the tenant's whole registry (no name/id — every live agent's card)
r = http.send(
HttpRequest.newBuilder(URI.create("https://api.forgebench.ai/v1/agents/cards"))
.header("Authorization", auth)
.GET()
.build(),
HttpResponse.BodyHandlers.ofString());
if (r.statusCode() / 100 != 2) throw new IllegalStateException("HTTP " + r.statusCode() + ": " + r.body());
Object cards = json.readValue(r.body(), Object.class);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"import os
import httpx
headers = {"Authorization": f"Bearer {os.environ['ANY_TENANT_KEY']}"}
r = httpx.get(
"https://api.forgebench.ai/v1/agents/706608c0-4160-452a-86ac-ded1186326ac/.well-known/agent-card.json",
headers=headers,
)
r.raise_for_status()
card = r.json()
# the tenant's whole registry (no name/id — every live agent's card)
r = httpx.get("https://api.forgebench.ai/v1/agents/cards", headers=headers)
r.raise_for_status()
cards = r.json()const headers = { Authorization: "Bearer " + process.env.ANY_TENANT_KEY };
let r = await fetch(
"https://api.forgebench.ai/v1/agents/706608c0-4160-452a-86ac-ded1186326ac/.well-known/agent-card.json",
{ headers },
);
if (!r.ok) throw new Error("HTTP " + r.status + ": " + (await r.text()));
const card = await r.json();
// the tenant's whole registry (no name/id — every live agent's card)
r = await fetch("https://api.forgebench.ai/v1/agents/cards", { headers });
if (!r.ok) throw new Error("HTTP " + r.status + ": " + (await r.text()));
const cards = await r.json();import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient http = HttpClient.newHttpClient();
ObjectMapper json = new ObjectMapper();
String auth = "Bearer " + System.getenv("ANY_TENANT_KEY");
HttpResponse<String> r = http.send(
HttpRequest.newBuilder(URI.create(
"https://api.forgebench.ai/v1/agents/706608c0-4160-452a-86ac-ded1186326ac/.well-known/agent-card.json"))
.header("Authorization", auth)
.GET()
.build(),
HttpResponse.BodyHandlers.ofString());
if (r.statusCode() / 100 != 2) throw new IllegalStateException("HTTP " + r.statusCode() + ": " + r.body());
Object card = json.readValue(r.body(), Object.class);
// the tenant's whole registry (no name/id — every live agent's card)
r = http.send(
HttpRequest.newBuilder(URI.create("https://api.forgebench.ai/v1/agents/cards"))
.header("Authorization", auth)
.GET()
.build(),
HttpResponse.BodyHandlers.ofString());
if (r.statusCode() / 100 != 2) throw new IllegalStateException("HTTP " + r.statusCode() + ": " + r.body());
Object cards = json.readValue(r.body(), Object.class);curl https://api.forgebench.ai/v1/agents/706608c0-4160-452a-86ac-ded1186326ac/.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"urlalways points at/v1/agents/{id}/taskson Forgebench, not at wherever the agent runs — so a caller that discovers this card, whether that's another workspace's agent, a partner's orchestrator, or an enterprise agent registry crawling for cards, can only ever reach the agent through the gate.skillsmirror 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 againstGET /v1/billing/export/pubkeyif 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:
| Status | Cause | Body |
|---|---|---|
403 | not_permitted — returned for every reason a caller
can be refused: unknown callee, another workspace's callee, no binding,
a revoked binding, a binding pending approval, or a paused/retired
callee. All of it collapses to the same response, because 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"}} |
404 | The task doesn't exist, or exists but you're not its caller or callee. | {"detail":"task not found"} |
409 | task_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"}} |
422 | A 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"} |
not_permitted covers every cause. To tell them apart during setup, check 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

