Parafé

Parafé A2A Extension, v2

How A2A agents declare Parafé trust requirements, authenticate each other through the Parafé broker, and carry scoped consent in every message.

Extension URI
https://parafe.ai/extensions/a2a/v2

Status
Draft
Last updated
2026-10-08

A2A versions
1.0 (and 0.3)
Reference implementation
@getparafe/a2a-extension 3.x (3.2.0)

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.

New to Parafé?

An agent needs a Parafé identity before it can use this extension. If you are an AI agent acting for a person and have no API key, this is the short path. The full, step-by-step guide for agents is at parafe.ai/llms.txt.

  1. Register. Generate an Ed25519 or P-256 key pair and send the public key to POST https://api.parafe.ai/agents/register with no API key. You receive a credential and a claim link. You start as self_registered and unverified.
  2. Get claimed. Show the claim link (https://platform.parafe.ai/claim?code=XXXX-XXXX-XX) to the person you act for. They sign in to Parafé and approve. You become their agent: identity assurance claimed and their verification tier. No secret passes through you, but whoever approves the link while signed in to Parafé becomes your principal, so show it only to the person you act for. It is single use and lasts 30 minutes; ask for a new one with POST /agents/{agent_id}/claim-link.
  3. Renew. Check GET /agents/{agent_id}/claim-status. When credential_current is false, renew with POST /agents/{agent_id}/renew so your credential shows who you act for.
  4. Handshake. Use this extension to authenticate with an agent. If it refuses you for identity or tier (identity_insufficient, tier_insufficient) while no person has claimed you, the broker's refusal includes a claim link and a hint: pass the link to your person.

Every request you make as the agent carries your credential and a proof of possession signed with your key (Parafe-PoP header). The @getparafe/sdk (0.5.0 and later) does all of this: register(), createClaimLink(), getClaimStatus(), renewCredential().

1. Overview

Parafé is a neutral trust broker for agent-to-agent interactions. This extension defines how Parafé's trust lifecycle travels over the A2A protocol:

  1. The receiving agent (the agent) declares its trust requirements, per scope, in its agent card.
  2. The calling agent (the client) starts a handshake with the Parafé broker and sends the broker's challenge to the agent in an A2A message.
  3. The agent answers the challenge with the broker, receives a broker-signed consent token, and returns it to the client.
  4. The client includes the consent token in every following message. The agent verifies it before acting, usually offline with the broker's public key.

All Parafé data rides in the A2A message's metadata, under the key https://parafe.ai/extensions/a2a/v2, and never in message parts. Agents commonly pass parts to a language model and store them in conversation logs. Consent tokens should be in neither.

2. Declaring the extension in an agent card

An agent that uses Parafé MUST list this extension in capabilities.extensions of its agent card (served at /.well-known/agent-card.json in A2A 1.0).

{
  "uri": "https://parafe.ai/extensions/a2a/v2",
  "description": "Parafé brokered trust: mutual authentication and scoped consent",
  "required": false,
  "params": {
    "agent_id": "prf_agent_donuts01",
    "broker_url": "https://api.parafe.ai",
    "minimum_identity_assurance": "self_registered",
    "scope_requirements": {
      "check-menu": {
        "permissions": ["read_menu", "read_availability"],
        "minimum_authorization_modality": "autonomous"
      },
      "order-donuts": {
        "permissions": ["read_menu", "create_order"],
        "minimum_authorization_modality": "attested"
      }
    }
  }
}
FieldTypeMeaning
params.agent_idstring, requiredThe agent's Parafé agent ID. Clients pass it to the broker as the handshake target.
params.broker_urlstring, requiredThe Parafé broker the agent trusts.
params.minimum_identity_assurance"self_registered" | "registered" | "claimed", requiredLowest identity assurance the agent accepts from clients. Order: self_registered < registered = claimed.
params.scope_requirementsobject, requiredOne entry per scope. permissions lists the actions the scope can grant. minimum_authorization_modality is "autonomous", "attested", "delegated" or "verified", weakest to strongest (section 5.3). Optional members below.

Optional members of each scope_requirements entry. They tell a client what it needs before it asks; the broker enforces each one that is in the agent's registered scope policy. An agent SHOULD build them from its scope policy (GET /agents/{id}/scope-policies; the package's scopeRequirementsFromPolicies()) so the card can't disagree with the broker.

MemberTypeMeaning
minimum_identity_assurance"self_registered" | "registered" | "claimed"The initiator's identity assurance for this scope. claimed: a person or org signed in to Parafé approved the agent. The broker ranks claimed equal to registered, so requiring either admits both.
minimum_verification_tier"unverified" | "email_verified" | "domain_verified" | "org_verified"How well Parafé knows the person or org behind the initiator, weakest to strongest.
exclusionsstring[]Actions never granted in this scope.
minimum_initiator_proof"pop" | "credential"pop: the initiator proved it holds its key when the token was issued.
trusted_issuersarray of { name?, iss?, kid?, jkt }The AP2 mandate issuers this scope trusts for delegated and verified. jkt is the issuer key's RFC 7638 thumbprint; the keys are in the broker's scope policy. The broker may also trust issuers for every agent (mandate_issuer_source: broker); those aren't listed here.
minimum_tenure_days, minimum_session_completion_rate, maximum_denied_requests_30d, minimum_unique_counterparties, minimum_handshake_success_ratenumberFloors on the initiator's reputation signals.

Readers MUST refuse an entry whose minimum_identity_assurance or minimum_verification_tier is not one of the values above, or whose trusted_issuers entry has no jkt, and MUST ignore members they don't recognize.

2.1 required

required: true means the agent serves no request without Parafé. A2A 1.0 agents MUST reject a request that does not activate a required extension with ExtensionSupportRequiredError (JSON-RPC -32008). The official A2A SDKs do this before the agent's own code runs.

required: false means the agent also serves callers without Parafé, for example to answer public questions, and requires a valid consent token only for actions inside a Parafé scope. An agent using required: false MUST enforce the consent check itself for every scoped action.

3. Activating the extension

A client MUST activate the extension on every request that carries Parafé data, and SHOULD activate it on every request to an agent that declares it:

The message itself MUST also list the URI in message.extensions. An agent that processed Parafé data for a request SHOULD list the URI in the A2A-Extensions response header.

4. Message data

Parafé data is an object at message.metadata["https://parafe.ai/extensions/a2a/v2"]. It contains exactly one of the members below or an error member (section 6), optionally with action_receipts beside it (section 4.5), or action_receipts alone. Readers MUST ignore members they don't recognize, and treat Parafé data with no member they recognize as absent, so that later revisions can add members.

4.1 handshake_challenge (client → agent)

Sent in the client's first message after it has called the broker's handshake initiation.

{
  "messageId": "6f1c…",
  "role": "ROLE_USER",
  "parts": [{ "text": "I'd like to place an order." }],
  "extensions": ["https://parafe.ai/extensions/a2a/v2"],
  "metadata": {
    "https://parafe.ai/extensions/a2a/v2": {
      "handshake_challenge": {
        "handshake_id": "hs_abc123",
        "challenge": "9f4a2c8e…(64 hex characters)",
        "initiator_agent_id": "prf_agent_sofia01",
        "broker_url": "https://api.parafe.ai",
        "requested_scope": "order-donuts",
        "requested_permissions": ["read_menu", "create_order"]
      }
    }
  }
}
FieldRequiredMeaning
handshake_idyesHandshake ID issued by the broker.
challengeyesThe broker's challenge nonce for the agent: 64 hexadecimal characters.
initiator_agent_idyesThe client's Parafé agent ID.
broker_urlyesThe broker that issued the challenge. The agent MUST NOT complete a handshake with a broker it does not trust.
requested_scopeyesA scope from the agent's scope_requirements.
requested_permissionsnoSubset of the scope's permissions. Defaults to all of them.

4.2 handshake_complete (agent → client)

Carried in the agent's response to the challenge: on the returned Message, or on status.message of the returned Task.

"https://parafe.ai/extensions/a2a/v2": {
  "handshake_complete": {
    "handshake_id": "hs_abc123",
    "status": "authenticated",
    "session_id": "sess_7d2…",
    "consent_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6…"
  }
}
FieldRequiredMeaning
handshake_idyesEchoes the challenge.
statusyes"authenticated", "rejected" or "error".
session_idwhen authenticatedThe Parafé session ID.
consent_tokenwhen authenticatedBroker-signed consent token (section 5).
error_code, error_messagewhen rejected or errorWhy the handshake did not complete.

4.3 consent (client → agent, every message after the handshake)

"https://parafe.ai/extensions/a2a/v2": {
  "consent": {
    "token": "eyJhbGciOiJFUzI1NiIsImtpZCI6…",
    "session_id": "sess_7d2…",
    "proof": "eyJhbGciOiJFZERTQSIsInR5cCI6InBhcmFmZS1wb3Arand0In0…"
  }
}

The client MUST include consent in every message sent under the session, and MUST keep the same contextId for the session's messages.

proof is a presentation proof (section 5.1): it shows the token is presented by the key it is bound to. A client SHOULD send one with every key-bound token (every token issued since 2026-09-30), a fresh one per message.

4.4 session_closed (either side, after closing)

The participant that closed the session at the broker MAY tell the other one, and hand it the receipt:

"https://parafe.ai/extensions/a2a/v2": {
  "session_closed": {
    "session_id": "sess_7d2…",
    "receipt": "eyJhbGciOiJFUzI1NiIsImtpZCI6…"
  }
}

receipt is the session receipt, a compact JWS signed by the broker (typ: parafe-session-receipt+jwt). The recipient SHOULD verify it against the broker's keys before relying on it; it can also fetch the same receipt from the broker (GET {broker_url}/sessions/{session_id}/receipt).

4.5 action_receipts (either side, beside any member or alone)

An agent returns the action receipts it signed for what it did or refused in this turn (section 5.2): a list of compact JWS strings, beside the message's member (for example error or handshake_complete), or alone:

"https://parafe.ai/extensions/a2a/v2": {
  "error": { "code": "SCOPE_VIOLATION", "message": "Not permitted by your consent token: issue_refund." },
  "action_receipts": [
    "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6…(read_menu, success)",
    "eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6…(issue_refund, error: excluded)"
  ]
}

At most 50 receipts per message. The client SHOULD file its copies with the broker (section 5.2), so they are indexed even if the agent never files them.

5. Verifying a consent token

The consent token is a JWT signed by the broker. It is signed with an ES256 key named by kid in its header; agents SHOULD verify offline with the broker's keys, fetched once from GET {broker_url}/.well-known/jwks.json and cached. Agents MAY instead verify online with POST {broker_url}/consent/verify. Offline verification can't see that an agent was revoked or suspended, or that the session is over: a token stays valid offline until exp (5 minutes after issue). The online check refuses it at once; agents SHOULD use it where that gap matters.

Before performing any action inside a Parafé scope, the agent MUST check all of the following, and MUST NOT perform the action if any check fails:

  1. The signature verifies (ES256) with the broker key named by kid, iss is "parafe-trust-broker", and token_type is "consent".
  2. The token has not expired (exp).
  3. target_agent_id equals the agent's own Parafé agent ID. A token issued for another agent MUST be rejected.
  4. session_id in the token equals consent.session_id in the message.
  5. The action is listed in permissions and not listed in exclusions.
  6. The message carries a proof that passes section 5.1. An agent SHOULD refuse a token sent without one (INVALID_PROOF); the reference implementation does by default since 3.0.

The agent SHOULD also check that scope is one it declares, that every entry in permissions belongs to that scope's declared permissions, and that authorization_modality meets the scope's minimum. The broker enforces these at handshake time; checking them again guards against a misconfigured broker or policy.

Token claims: scope, permissions, exclusions, session_id, token_type, authorization_modality, initiator_agent_id, target_agent_id, parent_token_id, iss, iat, exp, ver (2), sub (initiator agent ID), aud (target agent DID), jti, cnf.jkt (RFC 7638 thumbprint of the initiator's registered key), and initiator_proof with initiator_proof_at: "pop" when the initiator proved it holds its key when the token was issued, "credential" when it only showed its credential. A scope's minimum_initiator_proof MAY require "pop". Since the fifth revision, initiator_parties and target_parties name who runs each agent (its operator) and who it acts for (its principal): { "operator": { "type", "id"? } | null, "principal": { "type", "id"?, "ref"? } | null }, where a personal operator or principal shows its type only (never a person's user ID), an org its id, and a platform's user (external) the platform's opaque ref.

5.1 Presentation proofs (key-bound tokens)

A key-bound token is only valid in the hands of the key it names. The proof is a compact JWS signed with the initiator's registered key (Ed25519 → EdDSA, P-256 → ES256), header typ: "parafe-pop+jwt", with claims:

ClaimMeaning
athbase64url(SHA-256(consent token string)): the token this proof presents.
audThe token's aud: the agent's DID.
iatIssued at. A proof older than 5 minutes MUST be rejected.
jtiRandom, at least 16 characters. The agent MUST reject a jti it has seen in the last 5 minutes.
midOptional: the A2A messageId it travels with. If present it MUST match.

The agent verifies the proof with the initiator's public key (from the initiator's DID document, GET {broker_url}/agents/{sub}/did.json), and MUST check that the key's RFC 7638 thumbprint equals the token's cnf.jkt. A failed proof MUST be rejected with INVALID_PROOF, even if the token alone would pass.

5.2 Action receipts

The agent that performs or refuses an action signs an action receipt: a compact JWS signed with its registered key (EdDSA or ES256), header typ: "parafe-action-receipt+jwt", kid = {its DID}#keys-1, with claims:

ClaimMeaning
issThe acting agent's DID.
iat, jti, verIssued at; random ID; 1.
session_idThe Parafé session.
consent_refbase64url(SHA-256(consent token string)): the token the action was requested under.
actionThe action, e.g. a permission name.
result"success" or "error".
errorWith "error": not_permitted, excluded, consent_invalid, consent_expired, proof_invalid (a consent check failed) or failed (attempted and failed). Otherwise null.
error_descriptionOptional, human-readable.
request_ref, details_hashOptional hashes of the request and of what was done (JCS, RFC 8785). Never the content itself.
business_refOptional reference for the outcome, e.g. an order ID.
mandate_refOptional: the AP2 closed-mandate hash, when the action was AP2-authorized.

An agent that uses this extension SHOULD sign an action receipt for every action it performs inside a Parafé scope, and for every action it refuses because a check in section 5 failed. Either participant files it with the broker: POST {broker_url}/sessions/{session_id}/action-receipts with { "receipt": "…" }, authenticated with its credential and a proof of possession. The broker checks the signature against the issuer's registered key, that the issuer is a participant and that consent_ref names a token of the session, adds it to the session's hash chain and returns an acknowledgment it signs (typ: parafe-index-ack+jwt). Filing the same receipt again returns the original acknowledgment. Receipts MUST be filed before the session is closed; the session receipt then lists every one (actions, with chain_head). The broker learns action names, results and business references, never message content.

5.3 AP2 mandates behind a token

Two modalities mean the broker checked a user-signed AP2 v0.2 mandate, against the mandate issuers the agent's scope policy trusts, plus any the broker trusts for every agent (never the broker's own keys), and that the mandate's merchant or payee is this agent (for delegated, the merchant the user allowed): "verified", a mandate the user signed for this exact purchase (human present; it shows the user's approval, not which agent carries it, and an agent's own key can't sign one), and "delegated", an open mandate (the user's limits) that the initiator closed with its own registered key (human not present). Order, weakest to strongest: autonomous < attested < delegated < verified. Such tokens carry mandate_refs, one entry per mandate: { "family": "checkout" | "payment", "closed_jwt": …, "sd_hash": … }, the mandate's hash in the two forms AP2's specification and its SDK use.

An agent that is an AP2 merchant MUST answer a Checkout Mandate with an AP2 Checkout Receipt (a payment processor: a Payment Receipt), accepted or not. It SHOULD file the receipt unchanged in the session's index: POST {broker_url}/sessions/{session_id}/action-receipts with { "receipt": "…", "kind": "ap2.checkout_receipt" } (or "ap2.payment_receipt"). The broker checks its reference, in either form, against the mandates verified in the session and marks the index entry reference_verified, with who verified the mandate (mandate_verified_by) and whose list of trusted issuers it passed (mandate_issuer_source: scope_policy, broker or request). A mandate the broker checked at a handshake (the target is the verifier, under its scope policy or the broker's list) counts for both participants' receipts. A mandate verified with POST /ap2/mandates/verify counts only for the receipts of the agent that verified it, against a list it passed itself (request); the other participant's verifications don't count. Read reference_verified together with these two fields: it means the mandate was checked against that verifier's list, so it is as strong as that list. The strong case is scope_policy, the risk-bearer's own list; request from the receipt's own issuer means that agent chose the issuers. A merchant MAY have the broker verify a mandate presented to it: POST {broker_url}/ap2/mandates/verify, which also refuses a second redemption of the same mandate or checkout.

What the mandate names. For the broker to accept a mandate at a handshake, the mandate's merchant (checkout.merchant.id) or payee (payment.payee.id) MUST be the target's agent ID or DID, or the merchant's website MUST be on the target org's verified domain; for delegated, the user's allow-list entry names it that way. The last key-binding hop's aud MUST be the target's DID or agent ID. Its nonce is the target's own (for example the quote ID it issued); the broker does not check it, so the target SHOULD check it when it verifies the mandate itself. A mandate the issuer signed directly (no key-binding hop, AP2's human-present model) has no aud or nonce: the broker accepts it only within 5 minutes of its iat, and the merchant binding and the one-time redemption stand in for them. AP2 receipts are ES256, so a merchant's receipt counts as signed by it (issuer_verified) only when its registered key is P-256.

AP2 data in A2A messages (provisional). AP2 v0.2 defines no binding to A2A. Its samples put each artifact in its own data part under a fixed key (ap2.mandates.CheckoutMandateSdJwt, ap2.mandates.PaymentMandateSdJwt, ap2.PaymentReceipt) and declare https://github.com/google-agentic-commerce/ap2/v1. Agents MAY carry AP2 artifacts that way beside Parafé data: Parafé data is in message.metadata, so the two never collide. The reference implementation also uses ap2.CheckoutReceipt for Checkout Receipts. This paragraph will follow whatever binding AP2 (now at the FIDO Alliance) publishes.

6. Errors

When an agent refuses an action for a Parafé reason, it SHOULD explain in the Parafé data of its response:

"https://parafe.ai/extensions/a2a/v2": {
  "error": { "code": "EXPIRED_CONSENT_TOKEN", "message": "Consent token expired at 2026-09-27T20:14:10Z." },
  "action_receipts": ["eyJhbGciOiJFUzI1NiIsImtpZCI6ImRpZDp3ZWI6…"]
}

action_receipts is optional here: the agent's signed receipt of the refusal (section 5.2), when the message carried a consent token to bind it to.

CodeWhen
MISSING_PARAFE_EXTENSIONA scoped action was requested without Parafé data.
MALFORMED_PARAFE_DATAParafé data is present but a required field is missing or invalid.
INVALID_CONSENT_TOKENBad signature, wrong issuer, wrong token type, or session mismatch.
WRONG_AUDIENCEThe token was issued for a different agent.
EXPIRED_CONSENT_TOKENThe token has expired.
SCOPE_VIOLATIONThe action is not permitted by the token.
INVALID_PROOFA presentation proof is missing (where required) or fails section 5.1.

If the agent returns a Task, it SHOULD use TASK_STATE_AUTH_REQUIRED when a new handshake could resolve the problem (missing, expired, or insufficient consent), and TASK_STATE_REJECTED otherwise.

7. Security considerations

8. Compatibility

A2A versions. This extension works over A2A 1.0 and 0.3. Both versions carry message.metadata and message.extensions, so the Parafé data is identical. Only the activation header name differs (section 3).

Earlier Parafé extension. https://github.com/getparafe/parafe-a2a-extension/v1 carried the same payloads as A2A data parts under the keys parafe.handshake.Challenge, parafe.handshake.Complete and parafe.trust.ConsentToken. That version is retired: agents ignore v1 data parts and the v1 URI, and clients MUST NOT send them.

9. Revision history

First revision (2026-09-30). Additive at the time; agents and clients following the earlier text kept working with tokens that carry excluded (until the fourth revision).

Second revision (2026-09-30), for action receipts. Additive: a reader of the earlier text ignores action_receipts beside a member, but rejects a message whose only Parafé data is action_receipts. Agents SHOULD send receipts alone only to clients that follow this revision (reference implementation 2.2 or later). This revision also asks readers to ignore members they don't recognize (section 4).

Third revision (2026-09-30, AP2 mandates). Additive: the delegated modality, mandate_refs, AP2 receipts in the index, and provisional AP2 data in messages (section 5.3). Readers of earlier text refuse a delegated token against a declared scope (reference implementation 2.3 or later understands it).

Fourth revision (2026-09-30, reference implementation 3.0). Not additive: consent tokens verify with ES256 only and verifiers read exclusions only (the broker still also sends the legacy excluded); agents SHOULD refuse a token without a proof; v1 data is no longer read; minimum_identity_assurance may be claimed.

Fifth revision (2026-10-01, reference implementation 3.1). Additive: consent tokens name both parties (initiator_parties, target_parties): each agent's operator and principal.

Sixth revision (2026-10-08, reference implementation 3.2). Additive: agent-card scope requirements may state minimum_identity_assurance, minimum_verification_tier, exclusions and trusted_issuers (section 2).

10. Changes from v1