{"openapi":"3.0.3","info":{"title":"Parafé Trust Broker API","version":"0.2.0","description":"The neutral trust broker for agent-to-agent interactions. Parafé brokers trust between agents by providing cryptographic identity, mutual authentication, scoped consent, and signed receipts. AI agents: start with the agent guide, https://parafe.ai/llms.txt (register without an API key, get claimed by the person you act for, handshake). Field reference: https://parafe.ai/docs/reference. This description: GET /openapi.json (also /docs.json). Authentication: an agent acting as itself sends its credential (`Authorization: Bearer <credential>`, or in the body where the route says so: `initiator_credential`, `target_credential`, `credential`) plus a `Parafe-PoP` header: a JWT signed with its registered key (header `typ: parafe-pop+jwt`, `alg` EdDSA or ES256 to match the key; claims `htm`, `htu` (the full URL, no query), `iat` (within 5 minutes), a random single-use `jti` of 16+ characters, and the route's own claims, named on each route). Production requires the proof (`401 proof_required`; `proof_invalid`, `proof_replayed`). An operator sends its API key (`Authorization: Bearer prf_key_live_…`) or a portal session token the same way. Errors are JSON with an `error` code and usually a `message` (validation errors may add `details`); `/consent/verify` answers `{ valid: false, reason }`, with `error` for some refusals.","contact":{"name":"Parafé"}},"servers":[{"url":"https://api.parafe.ai","description":"Production"},{"url":"https://parafe-staging.up.railway.app","description":"Staging (test data; reset without notice)"}],"tags":[{"name":"Health","description":"Front door, service health and broker keys"},{"name":"Agents","description":"Agent registration and identity"},{"name":"Handshake","description":"Mutual authentication handshake"},{"name":"Consent","description":"Scoped consent verification"},{"name":"Interaction","description":"Retired (410): replaced by action receipts"},{"name":"Session","description":"Session lifecycle management"},{"name":"Receipt","description":"Signed receipt generation and verification"},{"name":"AP2","description":"AP2 v0.2 mandates: verification for merchants"},{"name":"Registry","description":"The public agent registry"}],"components":{"schemas":{"HealthResponse":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"version":{"type":"string","example":"0.2.0"}}},"PublicKeyResponse":{"type":"object","properties":{"public_key":{"type":"string","example":"MCowBQYDK2VwAyEAz3x2JJw70VUGr2LXIZ8/AcA+..."},"algorithm":{"type":"string","example":"Ed25519"},"key_id":{"type":"string","example":"parafe-signing-key-v1"},"status":{"type":"string","example":"retired"},"jwks_uri":{"type":"string","example":"/.well-known/jwks.json"},"active_kid":{"type":"string","description":"The kid of the active ES256 key"},"note":{"type":"string"}}},"RegisterRequest":{"type":"object","required":["public_key"],"example":{"agent_name":"assistant-user-8f3a","principal_name":"Platform user","acts_for":{"ref":"user-8f3a"},"public_key":"MCowBQYDK2VwAyEA..."},"properties":{"agent_name":{"type":"string","description":"3-100 chars, lowercase alphanumeric + hyphens. Required when an operator registers the agent (API key or portal session): unique per operator (per (operator, ref) for acts_for), never reused. Optional without one (self-registration, SPEC-002 decision 10): then it is only what the agent calls itself, shown to the person on the claim page; the agent's public name (registry, credentials, receipts) is its agent_id, and nothing is checked or reserved.","example":"travel-assistant"},"principal_name":{"type":"string","description":"Who the agent acts for, as free text (at most 100 characters), optional. With an API key or portal session, the account's own name replaces it (for acts_for, it is what you say for your user). Without one (decision 10, FRICTION #88) it is only what the agent says it acts for, shown on the claim page; the agent has no principal name (credentials, registry) until a person or org claims it.","example":"Example Travel Co"},"principal_type":{"type":"string","enum":["personal","org"],"description":"Register for a specific principal: your user (personal) or an org you belong to; send principal_id with it."},"principal_id":{"type":"string","description":"The user or org ID for principal_type."},"acts_for":{"type":"object","required":["ref"],"additionalProperties":false,"description":"SPEC-002: the operator (the API key's or portal session's account) registers an agent acting for one of its users. `ref` is the operator's own opaque reference for the user (1-128 characters: letters, digits, . _ : -; not an email: counterparties see it). The agent gets `principal_type: external`, `principal_ref`, `verification_tier: unverified` (a platform's word about its user carries no tier) and `identity_assurance: registered`; the person can claim it to give it their own tier. Needs an API key or portal session (401 operator_required); not with principal_type (400). The agent name is unique per (operator, ref), not globally.","properties":{"ref":{"type":"string","minLength":1,"maxLength":128,"example":"user-8f3a"}}},"public_key":{"type":"string","description":"The agent's public key: Ed25519 or EC P-256 (A2), as base64 SPKI DER or a JWK object. Stored as base64 SPKI. Other curves and RSA are refused.","example":"MCowBQYDK2VwAyEA..."},"scope_policies":{"type":"object","description":"Optional: what this agent requires of agents that come to it, per scope (permissions, exclusions, minimum_* fields, reputation floors, ap2_trusted_issuers). Unknown fields: 400 unknown_policy_field."},"self_registered":{"type":"boolean","description":"Only matters with an API key or portal session: true registers the agent as self_registered instead of registered (ignored with acts_for). Without an API key or portal session the agent is always self_registered.","example":false}}},"Authorization":{"type":"object","required":["modality"],"properties":{"modality":{"type":"string","enum":["autonomous","attested","delegated","verified"],"description":"Authorization level (weakest to strongest). autonomous = no human. attested = a human instruction given in an authenticated session (the broker checks its shape, not its truth). delegated = an AP2 open-mandate chain the broker checked: the limits (the first open mandate) are signed by the trusted issuer or the holder of a credential it issued, not by a registered agent's key, and the initiator's own key closed the mandate (human not present). verified = an AP2 closed mandate the user signed, checked by the broker (human present). delegated and verified need evidence.ap2_mandate from an issuer the target's scope trusts (ap2_trusted_issuers, or the broker's AP2_TRUSTED_ISSUERS); the mandate's merchant or payee must be the target (agent ID or DID, or a website on its org's verified domain) and a hop's aud the target; each mandate and checkout is redeemed once. The consent token and the receipt carry mandate_refs ({ family, closed_jwt, sd_hash }). Errors: 400 verified_evidence_unverifiable (no mandate), mandate_invalid (with ap2_error, reason), mandate_mode_mismatch; 403 no_trusted_issuers, invalid_trusted_issuers, mandate_agent_mismatch, mandate_signed_by_agent (a verified mandate, or a delegated chain's first open mandate, signed by a registered agent's key), mandate_payee_mismatch (for delegated: the user-signed allow-list entry must name the target); 409 mandate_already_redeemed.","example":"attested"},"evidence":{"type":"object","description":"attested: instruction, platform, timestamp. delegated/verified: ap2_mandate plus checkout_jwt, checkout_hash or checkout_mandate as the mandate needs (nothing else).","properties":{"instruction":{"type":"string","description":"The user instruction","example":"Find me flights to Denver"},"session_type":{"type":"string","example":"authenticated_messaging"},"platform":{"type":"string","example":"example-assistant-app"},"timestamp":{"type":"string","format":"date-time"},"ap2_mandate":{"type":"string","description":"The AP2 mandate as presented (~~-joined Delegate SD-JWT chain)"},"checkout_jwt":{"type":"string"},"checkout_hash":{"type":"string"},"checkout_mandate":{"type":"string"}}}}},"RegisterResponse":{"type":"object","properties":{"agent_id":{"type":"string","example":"prf_agent_a1b2c3d4"},"did":{"type":"string","example":"did:web:api.parafe.ai:agents:prf_agent_a1b2c3d4"},"agent_name":{"type":"string","example":"travel-assistant","description":"Self-registered agents: the agent_id."},"principal_name":{"type":"string","nullable":true,"example":"Example Travel Co","description":"Null when there is no principal (self-registered)."},"principal_type":{"type":"string","enum":["personal","org","external"],"nullable":true,"description":"Who the agent acts for. external: a platform user (acts_for). Null when self-registered."},"principal_id":{"type":"string","nullable":true,"description":"The user or org ID (personal or org principals)"},"principal_ref":{"type":"string","nullable":true,"description":"The operator's reference for an external principal"},"operator_type":{"type":"string","enum":["personal","org"],"nullable":true,"description":"SPEC-002: who runs the agent and answers for it: the account that registered it. Null when self-registered."},"operator_id":{"type":"string","nullable":true},"identity_assurance":{"type":"string","enum":["registered","self_registered","claimed"],"description":"How the identity was established. 'claimed': self-registered, then approved by a signed-in person through a claim link","example":"registered"},"verification_tier":{"type":"string","enum":["unverified","email_verified","domain_verified","org_verified"],"example":"email_verified"},"org_id":{"type":"string","nullable":true},"scope_policies":{"type":"object","nullable":true},"credential":{"type":"string","description":"Signed JWT credential (agent passport), ES256","example":"eyJhbGciOiJFUzI1NiIs..."},"credential_sd_jwt":{"type":"string","description":"The same identity as an SD-JWT VC (typ dc+sd-jwt, ES256)"},"issued_at":{"type":"string","format":"date-time","example":"2026-02-25T10:00:00.000Z"},"expires_at":{"type":"string","format":"date-time","example":"2026-03-25T10:00:00.000Z"},"claim":{"type":"object","description":"Self-registration only: a claim link for the person the agent acts for. Show them claim_url and tell them its code: the claim page shows the code first, so they can match it.","properties":{"claim_url":{"type":"string","example":"https://platform.parafe.ai/claim?code=7KQ2-M9XD-4H"},"code":{"type":"string","example":"7KQ2-M9XD-4H"},"expires_at":{"type":"string","format":"date-time"}}}}},"HandshakeInitiateRequest":{"type":"object","required":["initiator_credential","target_agent_id","requested_scope"],"properties":{"initiator_credential":{"type":"string","description":"JWT credential from registration","example":"eyJhbGciOiJFUzI1NiIs..."},"target_agent_id":{"type":"string","description":"ID of the agent to handshake with","example":"prf_agent_delta01"},"requested_scope":{"type":"string","description":"Intended interaction scope (e.g., flight-search, booking-lookup, booking-modify)","example":"flight-search"},"requested_permissions":{"type":"array","items":{"type":"string"},"description":"Specific permissions requested (optional; omit to receive every permission in the scope). If present, must be a non-empty subset of the target scope's declared permissions, otherwise 403 permissions_not_in_scope.","example":["search_flights","read_schedules","read_fares"]},"authorization":{"$ref":"#/components/schemas/Authorization"},"session_id":{"type":"string","description":"For scope escalation: reference an existing authenticated session. Skips challenge-response, issues a new consent token for the new scope.","example":"sess_x1y2z3w4a5b6"},"context":{"type":"object","description":"Freeform metadata for this interaction","example":{"user_ref":"user-8f3a","account_ref":"ACCT-1001"}}}},"HandshakeInitiateResponse":{"type":"object","properties":{"handshake_id":{"type":"string","example":"hs_abc123def456"},"status":{"type":"string","example":"pending_target_auth"},"initiator_verified":{"type":"boolean","example":true},"initiator_agent_id":{"type":"string","example":"prf_agent_a1b2c3d4"},"target_agent_id":{"type":"string","example":"prf_agent_e5f6a7b8"},"challenge_for_target":{"type":"string","description":"32-byte hex nonce the target must sign","example":"7f3a...64 hex chars"},"requested_scope":{"type":"string","example":"flight-search"},"expires_at":{"type":"string","format":"date-time","example":"2026-02-25T10:05:00.000Z"}}},"HandshakeCompleteRequest":{"type":"object","required":["handshake_id","target_credential","challenge_response"],"properties":{"handshake_id":{"type":"string","description":"The handshake to complete","example":"hs_abc123def456"},"target_credential":{"type":"string","description":"Target agent JWT credential from registration","example":"eyJhbGciOiJFUzI1NiIs..."},"challenge_response":{"type":"string","description":"Signature of the challenge nonce bytes by the target's registered key, base64: Ed25519, or P-256 ECDSA/SHA-256 (raw r||s or DER)","example":"NK3vZjz5Cr31mHYz..."}}},"HandshakeCompleteResponse":{"type":"object","properties":{"handshake_id":{"type":"string","example":"hs_abc123def456"},"status":{"type":"string","example":"authenticated"},"mutual_auth":{"type":"boolean","example":true},"session":{"type":"object","properties":{"session_id":{"type":"string","example":"sess_x1y2z3w4a5b6"},"initiator":{"type":"object","properties":{"agent_id":{"type":"string","example":"prf_agent_a1b2c3d4"},"identity_assurance":{"type":"string"},"parties":{"type":"object","description":"{ operator, principal } (SPEC-002)"}}},"target":{"type":"object","properties":{"agent_id":{"type":"string","example":"prf_agent_e5f6a7b8"},"identity_assurance":{"type":"string"},"parties":{"type":"object"}}},"established_at":{"type":"string","format":"date-time"}}},"consent_token":{"type":"object","properties":{"token":{"type":"string","description":"Signed consent JWT","example":"eyJhbGciOiJFUzI1NiIs..."},"scope":{"type":"string","example":"flight-search"},"permissions":{"type":"array","items":{"type":"string"},"example":["read_bookings","search_alternatives","request_rebooking","charge_payment_on_file"]},"exclusions":{"type":"array","items":{"type":"string"},"example":["loyalty_transfers","personal_documents","e_credit_access","account_changes"]},"authorization":{"type":"object","description":"{ modality, evidence, mandate_refs? }"},"session_id":{"type":"string","example":"sess_x1y2z3w4a5b6"},"issued_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time","description":"5 minutes after issue"},"initiator_proof":{"type":"string","enum":["pop","credential"],"nullable":true,"description":"B14: how the initiator authenticated"},"initiator_proof_at":{"type":"string","format":"date-time","nullable":true}}}}},"ConsentVerifyRequest":{"type":"object","required":["consent_token","action","session_id"],"properties":{"consent_token":{"type":"string","description":"The consent JWT to verify","example":"eyJhbGciOiJFUzI1NiIs..."},"action":{"type":"string","description":"The action to check","example":"read_bookings"},"session_id":{"type":"string","description":"Session this consent belongs to","example":"sess_x1y2z3w4a5b6"},"proof":{"type":"string","description":"Optional: the presentation proof the initiator attached to the token (B7, typ parafe-pop+jwt), checked against the token's cnf.jkt. 401 error proof_invalid if it fails."}}},"ConsentVerifyPermitted":{"type":"object","properties":{"valid":{"type":"boolean","example":true},"action":{"type":"string","example":"read_bookings"},"permitted":{"type":"boolean","example":true},"session_id":{"type":"string","example":"sess_x1y2z3w4a5b6"},"expires_at":{"type":"string","format":"date-time"},"key_bound":{"type":"boolean","description":"The token carries cnf.jkt"},"proof_verified":{"type":"boolean","description":"A proof was sent and checked"}}},"ConsentVerifyDenied":{"type":"object","properties":{"valid":{"type":"boolean","example":true},"action":{"type":"string","example":"loyalty_transfers"},"permitted":{"type":"boolean","example":false},"reason":{"type":"string","example":"Action 'loyalty_transfers' is in the excluded list for this consent token"},"session_id":{"type":"string","example":"sess_x1y2z3w4a5b6"},"key_bound":{"type":"boolean"},"proof_verified":{"type":"boolean"}}},"ReceiptGenerateRequest":{"type":"object","required":["session_id"],"properties":{"session_id":{"type":"string","example":"sess_x1y2z3w4a5b6"}}},"ReceiptVerifyRequest":{"type":"object","required":["receipt"],"properties":{"receipt":{"oneOf":[{"type":"string","description":"v2: the session receipt JWS"},{"type":"object","description":"v2: the /session/close response (its `receipt`); or v1: the signed receipt object"}]},"receipt_signature":{"type":"string","description":"v1 only, when the signature is not inside the receipt (`signature`)","example":"NK3vZjz5Cr31mHYz8Q1W..."}}},"ReceiptVerifyResponse":{"type":"object","properties":{"valid":{"type":"boolean","example":true},"format_version":{"type":"integer","example":2},"signed_by":{"type":"string","example":"did:web:api.parafe.ai","nullable":true,"description":"v2: the receipt iss (the broker DID); v1: parafe-broker"},"kid":{"type":"string","nullable":true,"description":"v2: the key that signed it"},"receipt_id":{"type":"string","example":"rcpt_abc123def456","nullable":true},"tamper_detected":{"type":"boolean","example":false},"claims":{"type":"object","description":"v2: the verified payload"},"error":{"type":"string","description":"v2: why verification failed"}}},"ValidationError":{"type":"object","properties":{"error":{"type":"string","example":"validation_error"},"details":{"type":"array","items":{"type":"string"}}}},"AuthError":{"type":"object","properties":{"error":{"type":"string","example":"invalid_credential"},"message":{"type":"string","example":"Invalid or expired credential"}}},"NotFoundError":{"type":"object","properties":{"error":{"type":"string","example":"not_found"},"message":{"type":"string","example":"Target agent not found"}}},"ConflictError":{"type":"object","properties":{"error":{"type":"string","example":"agent_name_taken"},"message":{"type":"string","example":"Agent name 'travel-assistant' is already registered"}}},"ExpiredError":{"type":"object","properties":{"error":{"type":"string","example":"expired"},"message":{"type":"string","example":"Handshake has expired"}}}}},"paths":{"/":{"get":{"tags":["Health"],"summary":"Front door: where everything is","description":"JSON naming the agent guide (start there), this API description, the A2A extension spec, the field reference, the broker keys and DID, the registry, the portal and the SDK. Unknown routes answer `404 { error: \"not_found\", front_door, agent_guide }`.","responses":{"200":{"description":"The front door","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"version":{"type":"string"},"start_here":{"type":"string"},"links":{"type":"object","properties":{"agent_guide":{"type":"string","example":"https://parafe.ai/llms.txt"},"api_description":{"type":"string","example":"https://api.parafe.ai/openapi.json"},"api_docs":{"type":"string"},"a2a_extension_spec":{"type":"string","example":"https://parafe.ai/extensions/a2a/v2"},"field_reference":{"type":"string","example":"https://parafe.ai/docs/reference"},"broker_keys":{"type":"string","example":"https://api.parafe.ai/.well-known/jwks.json"},"broker_did":{"type":"string"},"registry":{"type":"string","example":"https://api.parafe.ai/registry/agents"},"portal":{"type":"string"},"sdk":{"type":"string"}}}}}}}}}}},"/.well-known/parafe.json":{"get":{"tags":["Health"],"summary":"Front door (well-known location)","description":"The same body as `GET /`.","responses":{"200":{"description":"The front door"}}}},"/openapi.json":{"get":{"tags":["Health"],"summary":"This API description (OpenAPI 3)","description":"Also at `/docs.json`; browsable at `/docs`. `GET /llms.txt` redirects to the agent guide.","responses":{"200":{"description":"OpenAPI document"}}}},"/.well-known/did.json":{"get":{"tags":["Health"],"summary":"The broker's DID document (did:web)","description":"The `iss` of session receipts v2. Its verification methods are the JWKS keys.","responses":{"200":{"description":"DID document","content":{"application/did+json":{"schema":{"type":"object"}}}}}}},"/health":{"get":{"tags":["Health"],"summary":"Health check","description":"Whether the broker is up, and its version. Nothing else (SPEC-003 part 2).","responses":{"200":{"description":"Service is healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/.well-known/jwks.json":{"get":{"tags":["Health"],"summary":"The broker's signing keys (JWKS)","description":"Every key the broker has signed with, as a JSON Web Key Set. Everything the broker signs names its key in the JWS `kid` header; look it up here. The active key is ES256 (P-256). Retired keys (the Ed25519 key that signed everything before 2026-09-30) stay published indefinitely so old artifacts keep verifying. Each `kid` is the key's RFC 7638 thumbprint.","responses":{"200":{"description":"JWKS","content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"object","properties":{"kty":{"type":"string","example":"EC"},"crv":{"type":"string","example":"P-256"},"x":{"type":"string"},"y":{"type":"string"},"kid":{"type":"string"},"alg":{"type":"string","example":"ES256"},"use":{"type":"string","example":"sig"},"status":{"type":"string","enum":["active","retired"]},"created_at":{"type":"string"}}}}}}}}}}}},"/public-key":{"get":{"tags":["Health"],"summary":"Get the broker's legacy Ed25519 public key","description":"Legacy. Returns the Ed25519 key that signed v1 receipts and every token issued before 2026-09-30. New artifacts are signed with ES256: use /.well-known/jwks.json and the `kid` in the JWS header.","responses":{"200":{"description":"Broker's public signing key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicKeyResponse"}}}}}}},"/agents/register":{"post":{"tags":["Agents"],"summary":"Register an agent","description":"Registers a new agent identity with Parafé. The agent provides its public key (Ed25519, or P-256 since A2) and, when an operator registers it, a name and who it acts for (principal_name, principal_type, or acts_for); a self-registered agent's public name is its agent_id, and nothing it says about itself goes into its credentials (SPEC-002 decision 10). Parafé returns a signed JWT credential (the agent's \"digital passport\") and, since B13, the same identity as an SD-JWT VC (`credential_sd_jwt`: typ `dc+sd-jwt`, ES256, `iss` = broker DID, `vct` https://parafe.ai/vct/agent-identity/1, `cnf.jwk` = the agent's registered key, selectively disclosable `principal_name`, `principal_id` and `principal_ref`, `principal_type`, `operator_type` and `operator_id`, and `operator_domain` / `operator_domain_verified_at` when the operator is an org that has verified its domain). SPEC-002: every agent has an operator (who runs it and answers for it: the account that registered it; none when self-registered or claimed) and a principal (who it acts for). The JWT credential carries the same parties (`principal_name`, `principal_type`, `principal_id`, `principal_ref`, `operator_type`, `operator_id`). The JWT credential is kept for one release.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}}},"responses":{"201":{"description":"Agent registered successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterResponse"}}}},"400":{"description":"validation_error (missing fields, bad public key format; `details` lists them), unknown_policy_field","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"invalid_api_key; operator_required (acts_for without an API key or portal session)"},"403":{"description":"insufficient_scope, org_auth_required, not_member, ownership_mismatch: the key or session may not register for that org or principal"},"409":{"description":"agent_name_taken (unique per operator; per operator and user reference with acts_for), name_burned (a revoked agent's name is never reused)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConflictError"}}}}}}},"/agents/{agent_id}/claim-link":{"post":{"tags":["Agents"],"summary":"Get a claim link (agent with no principal, or an external one)","description":"For an agent that registered itself with no API key. Returns a link to show the person the agent acts for: signed in to the portal, they approve it and the agent becomes theirs (`identity_assurance: claimed`, their verification tier). Auth: the agent's credential (`Authorization: Bearer`) and a `Parafe-PoP` proof with claims `{ agent_id }`, required even during the proof grace period. The code is 10 Crockford base32 characters shown `XXXX-XXXX-XX`, single use, valid 30 minutes; a new link replaces the pending one. Decision 10: tell the person the code with the link; the claim page shows it first, so they can match the page to your chat. Keyless `POST /agents/register` returns one as `claim`, and a `403 tier_insufficient` / `identity_insufficient` from `/handshake/initiate` carries one with a `hint`.","parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"The claim link","content":{"application/json":{"schema":{"type":"object","properties":{"claim_url":{"type":"string","example":"https://platform.parafe.ai/claim?code=7KQ2-M9XD-4H"},"code":{"type":"string","example":"7KQ2-M9XD-4H"},"expires_at":{"type":"string","format":"date-time"}}}}}},"401":{"description":"No valid credential for this agent, or no proof (proof_required)"},"409":{"description":"The agent already has a principal (already_claimed), or is not active"},"429":{"description":"Too many links for this agent (10 per hour)"}}}},"/agents/{agent_id}/claim-status":{"get":{"tags":["Agents"],"summary":"Claim status (agent with no principal, an external one, or claimed)","description":"Whether a person or org has claimed the agent, who runs it (operator) and who it acts for (principal), and whether its credential shows it yet. `principal_tier` is the principal's tier (null for an external principal or none: never the operator's). `credential_current` is false when the credential's tier, identity assurance, principal or operator differ from the agent record (including every credential issued before SPEC-002): renew (`POST /agents/{agent_id}/renew` with the agent's credential and a proof; reason `identity_changed`). Handshakes read the record, so a claim applies from the moment it is approved. Auth as claim-link (proof claims `{ agent_id }`). To wait for the person's approval, send `?wait=25` in a loop (a new proof each time): the broker answers as soon as the claim is approved, or after `wait` seconds with `claimed: false`. Stop after 30 minutes (the claim link's life) and get a new link.","parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string"}},{"name":"wait","in":"query","required":false,"description":"Seconds to wait for the claim to be approved before answering (0-60, default 0). Answers at once when the agent is already claimed.","schema":{"type":"integer","minimum":0,"maximum":60}}],"responses":{"200":{"description":"Claim status","content":{"application/json":{"schema":{"type":"object","properties":{"claimed":{"type":"boolean"},"operator_type":{"type":"string","nullable":true},"operator_id":{"type":"string","nullable":true},"principal_type":{"type":"string","nullable":true},"principal_ref":{"type":"string","nullable":true},"identity_assurance":{"type":"string"},"verification_tier":{"type":"string"},"principal_tier":{"type":"string","nullable":true},"credential_current":{"type":"boolean"},"registered_at":{"type":"string","format":"date-time"},"principal_email":{"type":"string","description":"Only when the principal chose to share it with the operator (opt-in at the claim or in the portal). Never in the credential."},"principal_email_verified":{"type":"boolean"}}}}}},"400":{"description":"wait is not a whole number from 0 to 60 (validation_error)"},"401":{"description":"No valid credential for this agent, or no proof"},"409":{"description":"The agent is not active (agent_inactive), including when it was revoked during the wait"}}}},"/agents/{agent_id}/renew":{"post":{"tags":["Agents"],"summary":"Renew the agent's credential","description":"Issues a new credential (`credential` and `credential_sd_jwt`) when the record changed since the credential was issued (`identity_changed`: claimed, or operator or principal changed), the principal's tier changed (`tier_changed`), it has expired (`expired`), or it expires within 7 days (`near_expiry`). Otherwise `renewed: false` and nothing changes. The old credential stops working. Auth: the agent's credential (`Authorization: Bearer`) with a `Parafe-PoP` proof (claims `{ agent_id }`), or an API key with `agents:register` for an agent you manage, or a portal session. No body.","parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Renewed, or nothing to renew","content":{"application/json":{"schema":{"type":"object","properties":{"agent_id":{"type":"string"},"renewed":{"type":"boolean"},"reason":{"type":"string","enum":["identity_changed","tier_changed","expired","near_expiry"]},"previous_tier":{"type":"string"},"current_tier":{"type":"string"},"credential":{"type":"string"},"credential_sd_jwt":{"type":"string"},"issued_at":{"type":"string"},"expires_at":{"type":"string"}}}}}},"401":{"description":"No valid credential, API key or session; or no proof with the credential (proof_required)"},"403":{"description":"The agent is not yours (forbidden), or the key lacks the scope (insufficient_scope)"},"404":{"description":"Agent not found"},"409":{"description":"The agent is revoked (agent_revoked)"}}}},"/agents/{agent_id}/revoke":{"post":{"tags":["Agents"],"summary":"Revoke an agent (permanent)","description":"Ends the agent: status `revoked`, its sessions end, its credential goes on the revoked list. Permanent. Auth: an API key with `agents:revoke` for an agent your org or you manage, or the agent's own credential with a `Parafe-PoP` proof (claims `{ agent_id }`). People revoke from the portal.","parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object","properties":{"agent_id":{"type":"string"},"status":{"type":"string","example":"revoked"},"revoked_at":{"type":"string","format":"date-time"},"sessions_revoked":{"type":"integer"}}}}}},"401":{"description":"No valid API key or credential, or no proof"},"403":{"description":"Not your agent, or the key lacks agents:revoke"},"404":{"description":"Agent not found"},"409":{"description":"Already revoked (already_revoked)"}}}},"/agents/{agent_id}/scope-policies":{"get":{"tags":["Agents"],"summary":"An agent's scope policies (what each scope requires)","description":"Public (an API key is optional). What a target requires per scope: `permissions`, `exclusions`, `minimum_authorization_modality`, `minimum_identity_assurance`, `minimum_verification_tier`, `minimum_initiator_proof`, reputation floors, and `ap2_trusted_issuers` (public keys of the AP2 mandate issuers it trusts). Read it before a handshake to know what you need.","parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Scope policies","content":{"application/json":{"schema":{"type":"object","properties":{"agent_id":{"type":"string"},"scope_policies":{"type":"object","additionalProperties":{"type":"object"}}}}}}},"404":{"description":"Agent not found"}}},"put":{"tags":["Agents"],"summary":"Set the agent's own scope policies","description":"Replaces the agent's scope policies (`null` or `{}` removes them: the agent accepts any scope). Unknown policy fields are refused (`unknown_policy_field`), because the broker wouldn't enforce them. Auth: `credential` in the body and a `Parafe-PoP` proof (claims `{ agent_id }`).","parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["credential"],"properties":{"credential":{"type":"string"},"scope_policies":{"type":"object","nullable":true,"additionalProperties":{"type":"object"}}}}}}},"responses":{"200":{"description":"Updated"},"400":{"description":"Invalid policy (validation_error, unknown_policy_field)"},"401":{"description":"Invalid credential or no proof"},"403":{"description":"The credential isn't this agent's"},"404":{"description":"Agent not found"}}}},"/agents/{agent_id}/metrics":{"get":{"tags":["Agents"],"summary":"An agent's reputation signals","description":"The signals scope policies can set floors on (tenure, session completion rate, denied requests in 30 days, unique counterparties, handshake success rate). Public; with an API key, only for your own org's agents.","parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Metrics"},"403":{"description":"Not your org's agent (with an API key)"},"404":{"description":"Agent not found"}}}},"/agents/{agent_id}/did.json":{"get":{"tags":["Agents"],"summary":"The agent's DID document (did:web)","description":"Resolves `did:web:api.parafe.ai:agents:<agent_id>` to the agent's registered public key.","parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"DID document","content":{"application/did+json":{"schema":{"type":"object"}}}},"404":{"description":"Agent not found"}}}},"/registry/agents":{"get":{"tags":["Registry"],"summary":"Search the public agent registry","description":"Public agents, newest first. A self-registered agent is listed once a person claims it (before that, look it up by ID). People are never shown: not a personal principal's name, nor a platform user's reference.","parameters":[{"name":"search","in":"query","schema":{"type":"string"}},{"name":"category","in":"query","schema":{"type":"string"}},{"name":"verification_tier","in":"query","description":"Minimum tier","schema":{"type":"string","enum":["unverified","email_verified","domain_verified","org_verified"]}},{"name":"identity_assurance","in":"query","schema":{"type":"string","enum":["registered","claimed","both"]}}],"responses":{"200":{"description":"Agents","content":{"application/json":{"schema":{"type":"object","properties":{"agents":{"type":"array","items":{"type":"object"}},"total":{"type":"integer"}}}}}},"400":{"description":"Unknown category (invalid_category)"}}}},"/registry/agents/{agent_id}":{"get":{"tags":["Registry"],"summary":"One agent in the public registry","description":"Any agent by ID, listed or not. A revoked or suspended agent answers with its status and dates only, so a verifier holding its ID sees it is no longer active.","parameters":[{"name":"agent_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The agent"},"404":{"description":"Agent not found"}}}},"/handshake/initiate":{"post":{"tags":["Handshake"],"summary":"Initiate a trust handshake or escalate scope","description":"Two modes: (1) New handshake — presents credential, gets challenge for target. (2) Scope escalation — includes session_id to get a new consent token within an existing session without re-challenge. Authorization modality and identity assurance are enforced against scope requirements. Proof of possession (B7): send a `Parafe-PoP` header, a JWT signed with the initiator's registered key (`typ: parafe-pop+jwt`; claims `htm`, `htu`, `iat`, a single-use `jti`, `target_agent_id`, `requested_scope`, plus `session_id` when escalating). The same proof, bound to `session_id` (or `agent_id` on revoke/renew/scope-policies), is required wherever an agent authenticates with its credential. During the grace period a request without a proof is accepted and labeled credential-only; with PARAFE_POP_MODE=required it gets 401 proof_required. Idempotency-Key replays are scoped to the authenticated initiator; reusing a key with a different body is 422 (S-42). The consent token (v2) carries `sub`, `aud` (target DID), `cnf.jkt` (initiator key thumbprint), `jti`, `ver: 2` and `exclusions`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandshakeInitiateRequest"}}}},"responses":{"200":{"description":"New handshake: the challenge for the target (below). Scope escalation: `{ status: \"scope_escalated\", session_id, consent_token }`. A practice agent as target completes at once: the /handshake/complete body plus `auto_completed` and `practice_agent`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandshakeInitiateResponse"}}}},"400":{"description":"validation_error (missing or malformed fields), verified_evidence_unverifiable, mandate_invalid, mandate_mode_mismatch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"401":{"description":"Invalid or expired initiator credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"403":{"description":"The initiator doesn't meet the scope's policy. `identity_insufficient` and `tier_insufficient` carry a `claim` link and a `hint` when no person has claimed the agent. `authorization_insufficient` carries `required_modality`, `provided_modality` and a `hint`; for `delegated` and `verified` also `trusted_issuers` ([{ name?, iss?, kid?, jkt }]: the AP2 mandate issuers the scope trusts, by key thumbprint) and `mandate_requirements` ({ evidence_field, merchant_or_payee: [agent ID, DID], merchant_website_domain?, aud: [DID, agent ID], max_age_seconds, redeemed_once }). Others: initiator_proof_insufficient, the reputation floors, permissions_not_in_scope, scope_not_found.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"authorization_insufficient"},"message":{"type":"string","example":"Scope 'booking-modify' requires authorization modality 'verified', provided 'attested'"}}}}}},"404":{"description":"Target agent (or, when escalating, the session) not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}},"409":{"description":"invalid_state (escalating a session that is not active), mandate_already_redeemed"},"422":{"description":"idempotency_key_reused: the same Idempotency-Key with a different body"}}}},"/handshake/complete":{"post":{"tags":["Handshake"],"summary":"Complete a trust handshake","description":"The target agent proves its identity by signing the challenge nonce with its private key. Parafé verifies the signature against the registered public key. On success, a session and consent token are created. SPEC-002: `session.initiator.parties` and `session.target.parties`, and the consent token claims `initiator_parties` / `target_parties`, name each agent's operator and principal: `{ operator: { type, id } | null, principal: { type, id?, ref? } | null }`. A person's user ID is never shown (a personal operator or principal shows its type only); an org shows its ID; an external principal shows the operator's `ref`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandshakeCompleteRequest"}}}},"responses":{"200":{"description":"Handshake complete — session established with consent token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandshakeCompleteResponse"}}}},"400":{"description":"validation_error: handshake_id, target_credential or challenge_response missing"},"401":{"description":"invalid_credential (target credential invalid, expired or not this handshake's target), or invalid_challenge_response (the signature failed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthError"}}}},"404":{"description":"Handshake not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}},"409":{"description":"already_completed, invalid_state, or agent_inactive (an agent was revoked or suspended since the handshake started)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"already_completed"},"message":{"type":"string"}}}}}},"410":{"description":"Handshake expired (5-minute window elapsed)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpiredError"}}}}}}},"/consent/verify":{"post":{"tags":["Consent"],"summary":"Verify a consent token","description":"Checks whether a specific action is permitted by a consent token. Returns whether the action is in the permissions list (permitted) or exclusions list (denied). Unlike an offline check, it refuses tokens of a revoked or suspended agent and of a session that is over. Agents can also verify the token offline against /.well-known/jwks.json (match the JWS `kid`; ES256).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentVerifyRequest"}}}},"responses":{"200":{"description":"Consent verified — action is permitted or denied","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ConsentVerifyPermitted"},{"$ref":"#/components/schemas/ConsentVerifyDenied"}]},"examples":{"permitted":{"summary":"Action is permitted","value":{"valid":true,"action":"read_bookings","permitted":true,"session_id":"sess_x1y2z3w4a5b6","expires_at":"2026-02-25T10:05:30.000Z"}},"denied":{"summary":"Action is excluded","value":{"valid":true,"action":"loyalty_transfers","permitted":false,"reason":"Action 'loyalty_transfers' is in the excluded list for this consent token","session_id":"sess_x1y2z3w4a5b6"}}}}}},"400":{"description":"validation_error: consent_token, action or session_id missing, or proof not a string"},"401":{"description":"Invalid or expired consent token, or the session is over. `error: 'agent_revoked'` when an agent in the session has been revoked or suspended (B15): revocation ends the agent's sessions at once.","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","example":false},"error":{"type":"string","example":"agent_revoked","description":"agent_revoked, or proof_invalid when a sent proof fails; absent otherwise"},"reason":{"type":"string","example":"Consent token signature invalid or token expired"}}}}}}}}},"/interaction/record":{"post":{"tags":["Interaction"],"summary":"Retired: use action receipts","deprecated":true,"description":"Replaced by action receipts (AP2 change request B6): the agent that performs or refuses an action signs a receipt and either participant files it at `POST /sessions/{session_id}/action-receipts`. Always answers 410.","responses":{"410":{"description":"Gone (`error: gone`)"}}}},"/session/close":{"post":{"tags":["Session"],"summary":"Close session and generate receipt","description":"Closes an active session and issues the session receipt (v2, B4): a compact JWS signed by the broker (ES256, `kid` in the header, `typ: parafe-session-receipt+jwt`, `iss` = the broker DID). It records participant identities (agent ID, DID, assurance, tier, and `parties`: operator and principal as in the consent token, SPEC-002), mutual authentication and a hash of the handshake context, every consent token issued in the session (a hash of the token, scope, permissions, exclusions, authorization modality and a hash of the human's instruction, never the text), and the session lifecycle (who closed it; `revoked` if an agent was revoked). `actions` lists every action receipt and AP2 receipt filed in the session index (B6), and `chain_head` is the index's last entry hash. Response: `{ format_version: 2, receipt_id, session_id, receipt: <JWS>, claims: <decoded payload> }`. The JWS is the receipt; `claims` is a convenience copy. Verify with any JOSE library against /.well-known/jwks.json, or POST /receipt/verify. Authentication (S-47): send the calling agent's credential as `Authorization: Bearer <credential>` with a `Parafe-PoP` proof (claims `{ session_id }`; required on production), or its operator's API key (with the agents:register scope) or portal session. The caller must be a participant in the session. Either participant may close.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptGenerateRequest"}}}},"responses":{"200":{"description":"Session closed, receipt generated with full consent token history","content":{"application/json":{"schema":{"type":"object","properties":{"format_version":{"type":"integer","example":2},"receipt_id":{"type":"string"},"session_id":{"type":"string"},"receipt":{"type":"string","description":"The session receipt: compact JWS (ES256)"},"claims":{"type":"object","description":"Decoded JWS payload (read-only view)"}}}}}},"401":{"description":"No credential, or not a valid agent credential, API key or portal session; or the proof is missing or wrong (proof_required, proof_invalid, proof_replayed)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"unauthorized"},"message":{"type":"string"}}}}}},"403":{"description":"The caller is not a participant in this session (not_participant)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"not_participant"},"message":{"type":"string"}}}}}},"404":{"description":"Session not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}},"409":{"description":"Session already closed (already_closed)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"already_closed"},"message":{"type":"string"}}}}}}}}},"/sessions/{session_id}/receipt":{"get":{"tags":["Session"],"summary":"Fetch the session receipt (either participant)","description":"Returns the receipt of a closed session to either participant, not only the one that closed it (B5). Same authentication as /session/close: the agent's credential with a `Parafe-PoP` proof (claims `{ session_id }`), or its operator's API key or portal session. Response as /session/close; a v1 receipt (closed before 2026-09-30) comes back as `{ format_version: 1, receipt: <signed object> }`. The operator (or principal) of an agent revoked since can still fetch it.","parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The receipt","content":{"application/json":{"schema":{"type":"object","properties":{"format_version":{"type":"integer"},"receipt_id":{"type":"string"},"session_id":{"type":"string"},"receipt":{"type":"string"},"claims":{"type":"object"}}}}}},"401":{"description":"No valid credential and proof, API key or portal session"},"403":{"description":"Not a participant (not_participant)"},"404":{"description":"Session not found"},"409":{"description":"Session not closed yet (session_not_closed)"}}}},"/sessions/{session_id}/action-receipts":{"post":{"tags":["Session"],"summary":"File an action receipt in the session index (B6)","description":"The agent that performs or refuses an action signs an **action receipt**: a compact JWS signed with its registered key (header `alg` EdDSA or ES256, `kid` = `<agent DID>#keys-1`, `typ: parafe-action-receipt+jwt`; claims `iss` (agent DID), `iat`, `jti`, `ver: 1`, `session_id`, `consent_ref` = base64url(SHA-256(consent token JWS)), `action`, `result` (`success` | `error`), `error` (`not_permitted`, `excluded`, `consent_invalid`, `consent_expired`, `proof_invalid`, `failed`; null on success), `error_description`, `request_ref`, `details_hash`, `business_ref`, `mandate_ref`). Either participant files it (credential + proof bound to the session, or the operator's API key or portal session). The broker checks the signature against the issuer's registered key, that the issuer is a participant, that `consent_ref` names a consent token of this session and that `iat` falls within the session; then appends it to the session's hash chain (`entry_hash` = base64url(SHA-256(\"<seq>|<receipt_hash>|<prev>\")), `prev` \"\" for the first) and returns a broker-signed acknowledgment (`typ: parafe-index-ack+jwt`). The session receipt lists every filed receipt and the chain head. `kind: ap2.checkout_receipt` / `ap2.payment_receipt` files an AP2 receipt unchanged; its signature is checked when a participant's registered P-256 key verifies it, otherwise it is indexed with `issuer_verified: false`. A3: its `reference` (either form: SHA-256 of the closed mandate JWT, or the spec's sd_hash), and an action receipt's `mandate_ref`, are checked against the AP2 mandates verified in this session (POST /ap2/mandates/verify with `session_id`, or a verified/delegated handshake): the entry, acknowledgment and session receipt carry `reference_verified` and `mandate_ref`, plus `mandate_verified_by` and `mandate_issuer_source`. A mandate checked at a handshake (the target verifies it, under its scope policy or the broker's list) counts for both participants' receipts; one verified at POST /ap2/mandates/verify counts only for the verifying agent's own receipts (`mandate_issuer_source: request`). `mandate_issuer_source` is `scope_policy`, `broker` or `request`: read `reference_verified` as strong as that list. Filing must finish before the session is closed.","parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["receipt"],"properties":{"receipt":{"type":"string","description":"The receipt JWS, exactly as signed"},"kind":{"type":"string","enum":["parafe.action_receipt","ap2.checkout_receipt","ap2.payment_receipt"],"default":"parafe.action_receipt"}}}}}},"responses":{"201":{"description":"Indexed","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string"},"seq":{"type":"integer"},"receipt_hash":{"type":"string"},"entry_hash":{"type":"string"},"acknowledgment":{"type":"string","description":"Broker-signed JWS"},"claims":{"type":"object"}}}}}},"400":{"description":"invalid_action_receipt, invalid_ap2_receipt, consent_not_in_session, validation_error"},"401":{"description":"No valid credential and proof, API key or portal session"},"403":{"description":"The filer (not_participant) or the receipt's issuer (issuer_not_participant) is not a participant"},"404":{"description":"Session not found"},"409":{"description":"duplicate_receipt (with the original acknowledgment), session_closed, session_revoked, index_full"}}},"get":{"tags":["Session"],"summary":"The session index (either participant)","description":"Every receipt filed for the session, exactly as filed, with its acknowledgment, in chain order, and `chain_head`. Same authentication as the session receipt.","parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ session_id, chain_head, entries }"},"401":{"description":"No valid credential and proof, API key or portal session"},"403":{"description":"Not a participant (not_participant)"},"404":{"description":"Session not found"}}}},"/ap2/mandates/verify":{"post":{"tags":["AP2"],"summary":"Verify an AP2 mandate for a merchant (A1)","description":"A Parafé-protected merchant (or credential provider) hands the broker an AP2 v0.2 Checkout or Payment Mandate as presented, the `~~`-joined Delegate SD-JWT chain, and gets AP2's verdict. Checks (with `@getparafe/verify`): the root against the trusted issuers (the broker-wide `AP2_TRUSTED_ISSUERS` plus `trusted_issuers` in the request), every hop against the previous `cnf.jwk`, `sd_hash`/`issuer_jwt_hash`, required terminal `aud` and `nonce` (and `expected_audience`/`expected_nonce`), the terminal hop no older than 5 minutes, the exact `vct`, claims carried unchanged, no withheld constraint, every v0.2 constraint (unknown ones fail), `checkout_hash` against the Checkout JWT, and a payment's `transaction_id` against its checkout (`checkout_jwt`, `checkout_hash`, or `checkout_mandate`, the checkout chain, which also supplies `payment.reference`). A failure returns 200 with `valid: false`, the AP2 `error` code for the receipt (`invalid_credential`, `unresolved_constraint`, `invalid_mandate`), a `reason`, and `references` (both ways) for a rejection receipt. On success the broker records the redemption (unless `redeem: false`): the same closed mandate, or another mandate of the same family for the same checkout, is refused with 409 (`valid: false`, `error: invalid_mandate`, `reason: already_redeemed`, and the earlier `redemption`) for the same verifier (its org, or the agent itself; AP2 #346). `agent` names the registered Parafé agent whose key is the mandate's agent key (human not present), with `is_counterparty` when `session_id` is given. `closed_by` says who signed the closed mandate (`issuer`, `credential_holder`, `open_mandate_key`); `opened_by` says who signed the first open mandate, the user's limits (`issuer`, `credential_holder`). A human-present mandate, or a first open mandate, signed by a registered agent's key is refused (`reason: agent_signed_for_user`). Trusted issuers must be P-256 or Ed25519 public keys, never the broker's own. Authentication: the verifying agent's credential plus a proof bound to `agent_id` (or to `session_id` when given), or its operator's API key or portal session with `agent_id`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["mandate"],"properties":{"mandate":{"type":"string","description":"The AP2 mandate as presented (~~-joined Delegate SD-JWT chain)"},"agent_id":{"type":"string","description":"The verifying agent (needed with an operator API key or portal session)"},"session_id":{"type":"string","description":"Record the mandate in this session (the verifier must be a participant)"},"checkout_jwt":{"type":"string","description":"The merchant-signed Checkout JWT, when the checkout mandate does not disclose it; for a payment mandate, the checkout it pays"},"checkout_hash":{"type":"string","description":"Payment mandate: the expected transaction_id, if you don't hold the Checkout JWT"},"checkout_mandate":{"type":"string","description":"Payment mandate: the checkout mandate chain it belongs to (verified too)"},"expected_audience":{"type":"string"},"expected_nonce":{"type":"string"},"trusted_issuers":{"type":"array","items":{"type":"object","properties":{"jwk":{"type":"object"},"kid":{"type":"string"},"iss":{"type":"string"},"name":{"type":"string"}}},"description":"Credential Providers or Agent Providers you accept (public JWKs), or a JWKS"},"context":{"type":"object","properties":{"total_amount":{"type":"number"},"total_uses":{"type":"number"},"last_used_at":{"type":"number"}},"description":"For payment.budget and payment.agent_recurrence"},"redeem":{"type":"boolean","default":true}}}}}},"responses":{"200":{"description":"{ valid, family, mode, issuer, audience, nonce, presented_at, checkout_hash, transaction_id, references: { sd_hash, closed_jwt }, mandate_hash, closed_mandate, open_mandates, agent_key_thumbprint, agent, redemption } or { valid: false, error, reason, message, violations, references }"},"400":{"description":"validation_error, no_trusted_issuers"},"401":{"description":"No valid credential and proof, API key or portal session"},"403":{"description":"Not an agent you may act for, or not a participant in session_id"},"409":{"description":"Already redeemed: `error: invalid_mandate`, `reason: already_redeemed`, with the earlier redemption; or session_closed"}}}},"/receipt/generate":{"post":{"tags":["Receipt"],"summary":"Generate a signed receipt","description":"Legacy alias of POST /session/close: same v2 receipt response. Authentication (S-47): send the calling agent's credential as `Authorization: Bearer <credential>` with a `Parafe-PoP` proof (claims `{ session_id }`; required on production), or its operator's API key (with the agents:register scope) or portal session. The caller must be a participant in the session.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptGenerateRequest"}}}},"responses":{"200":{"description":"Session closed; the v2 receipt, as /session/close","content":{"application/json":{"schema":{"type":"object","properties":{"format_version":{"type":"integer","example":2},"receipt_id":{"type":"string"},"session_id":{"type":"string"},"receipt":{"type":"string","description":"The session receipt: compact JWS (ES256)"},"claims":{"type":"object","description":"Decoded JWS payload (read-only view)"}}}}}},"401":{"description":"No valid credential and proof, API key or portal session (unauthorized, proof_required, proof_invalid)"},"403":{"description":"Not a participant (not_participant)"},"404":{"description":"Session not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}},"409":{"description":"Session already closed (already_closed)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"already_closed"},"message":{"type":"string"}}}}}}}}},"/receipt/verify":{"post":{"tags":["Receipt"],"summary":"Verify a receipt signature","description":"Verifies a receipt is authentic and has not been tampered with. No authentication required. Send `{ receipt: <v2 JWS> }` (or the whole /session/close response) for v2 receipts; the response includes the verified `claims`. v1 receipts (issued before 2026-09-30: signed JSON, Ed25519) still verify: send the receipt object with its `signature`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptVerifyRequest"}}}},"responses":{"200":{"description":"Verification result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiptVerifyResponse"},"examples":{"valid":{"summary":"Receipt is authentic","value":{"valid":true,"format_version":2,"signed_by":"did:web:api.parafe.ai","kid":"-Abd1cZVBdP8uubhTpqRMHCoCgjDLubW7obdn6OKmmo","receipt_id":"rcpt_abc123def456","tamper_detected":false,"claims":{}}},"tampered":{"summary":"Tamper detected (v2)","value":{"valid":false,"format_version":2,"signed_by":null,"kid":null,"receipt_id":null,"tamper_detected":true,"error":"signature verification failed"}}}}}},"400":{"description":"validation_error: no v2 JWS, and no v1 receipt with its signature"}}}}}}