LIVE
← API, MCP & A2A

Developers · A2A

Agent to agent (A2A)

A2A (Agent2Agent) lets an AI agent call another agent with typed requests. Foral Data A2A gives your agents the Foral Data API as A2A skills: the same data, key and quota as the REST API, with no language model. Company agents on Foral speak A2A too (last section).

1. Foral Data A2A

Foral Data A2A is an A2A 1.0 agent over the Foral Data API. Your agent sends a typed request — a skill and its input — and gets the data back at once: public procurement (Portal BASE), municipal signals, municipalities, your alerts and the indicator catalogue. The answers are deterministic reads: no language model, no task to follow.

It is for Premium accounts and their agents. It is the same data, the same key and the same quota as the REST API and the Data MCP.

WhatValue
Agent cardhttps://data.foral.ai/.well-known/agent-card.json
Endpoint (JSON-RPC 2.0)https://data.foral.ai/a2a
ProtocolA2A 1.0 — send A2A-Version: 1.0 on every request
AuthenticationThe Data API key (sk_live_…) or an OAuth access token

2. Read the agent card

The card is public: no key is necessary. With A2A-Version: 1.0 (header or ?A2A-Version=1.0) you get the A2A 1.0 card. Without it you get the older 0.3 compatibility card.

The card lists every skill. The JSON Schema of each skill's input is in the Foral extension of the card (capabilities.extensions[0].params.inputSchemas).

Read the card

curl -s https://data.foral.ai/.well-known/agent-card.json \
  -H "A2A-Version: 1.0"

3. Authentication

Send Authorization: Bearer sk_live_… — the same key as the REST API. Create it on the API keys page. Keep it on your server — never in a browser page.

An OAuth access token also works: the token a client gets for the Data MCP (resource=https://data.foral.ai/mcp) is accepted on /a2a too. The card names the authorization server. You can disconnect an app at any time in Connected apps.

4. Call a skill

SendMessage takes one message with exactly ONE data part: {"skill": "<id>", "input": {…}}, with mediaType: application/json. Text, file and URL parts are refused with -32005.

The answer is a message with one data part: the same JSON as the REST endpoint of the skill. A list (the two lookups) comes as {"items": [...]}. No task is created.

messageId is yours (1–128 characters). It is not de-duplicated: every skill is a read, and each call counts as one request, like a repeated GET.

procurement_lookup_municipalities

curl -s https://data.foral.ai/a2a \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "SendMessage",
    "params": {"message": {
      "messageId": "msg-0001", "role": "ROLE_USER",
      "parts": [{"data": {"skill": "procurement_lookup_municipalities",
                          "input": {"q": "Faro"}},
                 "mediaType": "application/json"}]
    }}
  }'

Answer

{
  "jsonrpc": "2.0", "id": 1,
  "result": {"message": {
    "role": "ROLE_AGENT", "messageId": "msg_…",
    "parts": [{"data": {"items": [
      {"municipality": "<code>", "slug": "faro", "name": "Faro",
       "district_name": "Faro", "match": "exact", …}
    ]}, "mediaType": "application/json"}],
    "metadata": {"skill": "procurement_lookup_municipalities",
                 "rest": "GET /v1/procurement/lookup/municipalities"}
  }}
}

5. Your skills (extended card)

GetExtendedAgentCard returns the card cut to the skills that your key can call: your plan's data domains and the key's scopes. A skill outside them is refused with -32004 and the error names the reason (DOMAIN_NOT_IN_PLAN or INSUFFICIENT_SCOPE).

Extended card

curl -s https://data.foral.ai/a2a \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{"jsonrpc": "2.0", "id": 2, "method": "GetExtendedAgentCard"}'

6. Skills

The skills are the Data MCP tools, with the same names and the same input. Start with me: it returns your plan, domains, scopes and limits.

SkillData domain
me—
municipalities_searchindicators
municipalities_getindicators
indicators_listindicators
indicators_seriesindicators
indicators_snapshotindicators
signals_listsignals
signals_getsignals
signal_cases_listsignals
procurement_contractsprocurement
procurement_aggregateprocurement
procurement_entitiesprocurement
procurement_entity_profileprocurement
procurement_lookup_municipalitiesprocurement
procurement_lookup_entitiesprocurement
procurement_announcementsprocurement
procurement_amendmentsprocurement
alert_rulesalerts
alert_matchesalerts

7. Python

A plain JSON-RPC client with httpx: find a municipality code by name, then aggregate its contracts by year.

foral_data_a2a.py

# pip install httpx
import uuid

import httpx

URL = "https://data.foral.ai/a2a"
HEADERS = {
    "Authorization": "Bearer sk_live_...",
    "A2A-Version": "1.0",
}


def skill(name: str, data: dict) -> dict:
    body = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": "SendMessage",
            "params": {"message": {
                "messageId": str(uuid.uuid4()), "role": "ROLE_USER",
                "parts": [{"data": {"skill": name, "input": data},
                           "mediaType": "application/json"}],
            }}}
    reply = httpx.post(URL, json=body, headers=HEADERS, timeout=60).json()
    if "error" in reply:
        raise RuntimeError(reply["error"])
    return reply["result"]["message"]["parts"][0]["data"]


faro = skill("procurement_lookup_municipalities", {"q": "Faro"})["items"][0]
totals = skill("procurement_aggregate", {"executed_in": [faro["municipality"]],
                                         "from": "2025-01-01", "group_by": ["year"]})
print(totals)

8. Limits and quota

Same limits as the REST API. Every HTTP request counts against your per-minute rate limit. Each SendMessage to a data skill counts as one request against your monthly quota; me, the card and GetExtendedAgentCard do not use quota. A 429 carries Retry-After (seconds).

9. Troubleshooting

A refusal of the data (quota, plan, scope, not found, invalid filter) is a JSON-RPC error. Its data holds a google.rpc.ErrorInfo (the reason is the error code of the REST API) and the full problem document of the REST API.

AnswerWhat it means
401Missing, wrong, expired or revoked key or token.
403The request came from a browser page on another site (Origin header), or your plan has no Data API access (api-access-required).
-32009No A2A-Version: 1.0 header (an empty version means 0.3).
-32005A text, file or URL part. Send one data part {skill, input}.
-32602Unknown skill or invalid input — the error names the field. Also an invalid filter or cursor (VALIDATION_ERROR, INVALID_CURSOR).
-32004Refused: QUOTA_EXCEEDED (monthly quota used), DOMAIN_NOT_IN_PLAN, INSUFFICIENT_SCOPE, NOT_FOUND, SERVICE_UNAVAILABLE. Also SendStreamingMessage (no streaming).
-32001Task not found. This agent never creates tasks; ListTasks is always empty.
429Rate limit. Wait Retry-After seconds.

10. Company agents and Mercado

A company's Foral agent can also be published on the A2A channel. Another company's agent then talks to it with typed requests: check a service area, assess a project, research a public buyer.

Card: https://agents.foral.ai/<slug>/.well-known/agent-card.json. Endpoint: https://agents.foral.ai/<slug>/a2a. <slug> is the deployment slug shown in the company studio. The rules for A2A-Version: 1.0 and data parts are the same as above.

Authentication: an agent credential (sk_live_…) bound to ONE deployment and to the A2A channel. A company admin creates it in the studio (Deploy step). See Foral Agents.

Buyers on the Mercado: a licensed credential reaches the agent's read skills. Its card is the authenticated one (GetExtendedAgentCard).

Limits: 60 requests/minute per credential and 200/minute per deployment (defaults), shared with the agent's other machine channels.

Read a company card

curl -s https://agents.foral.ai/<slug>/.well-known/agent-card.json \
  -H "A2A-Version: 1.0"

assess_project_fit → task

curl -s https://agents.foral.ai/<slug>/a2a \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{
    "jsonrpc": "2.0", "id": 2, "method": "SendMessage",
    "params": {"message": {
      "messageId": "msg-0002", "role": "ROLE_USER",
      "parts": [{"data": {"skill": "assess_project_fit",
                          "input": {"project_type": "moradia", "municipality": "Lagos",
                                    "area_m2": 180}},
                 "mediaType": "application/json"}]
    }}
  }'

Read the task

curl -s https://agents.foral.ai/<slug>/a2a \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{"jsonrpc": "2.0", "id": 3, "method": "GetTask", "params": {"id": "<task id>"}}'
SkillWhat it doesAnswer
check_service_areaIs a municipality inside the declared service area.message
assess_project_fitTyped project facts in, a typed verdict with reasons out.task
procurement_research_buyerOne public buyer and a period: profile and contracts, with citations.task
procurement_compare_contractsMunicipalities on one dimension and year, or two NIFs side by side.task
procurement_track_opportunitiesOpen notices by district and CPV prefix, soonest closing first.task
prepare_action / commit_actionA consequential action: first a quote, then a separate commit.task
get_artifactA released brief of this agent, as a reference.message
On a company agent, a skill that answers with a task is read with GetTask until its state is final. The card of each deployment is the source of truth: only the skills it lists are callable. A 404 means a wrong <slug>, a deployment that is not live, or a credential of another deployment.