Skip to content

Proto: FORA v1

Source: proto/fora/v1/fora.proto

The core protocol. Both AI agents and Brokers are valid clients; an Exchange tells their requests apart as Direct and relayed requests states. Every Exchange implements all of its transaction RPCs, DisputeTransaction included.

ExecuteTransaction answers every per-item denial in the body: an item the Exchange denies carries TransactionResultItem.denial_reason in a successful response, whatever the item count. A non-OK error carrying ErrorDetail.transaction_denial is used only when the Exchange refuses the whole request and decides no item.

RPCRequestResponseDescription
DiscoverResourcesResourceQueryResourceResponseDiscover available resource offers matching the query. Steps 2-3 in the FORA flow. Discovery has no side effects. It creates no transaction, no billing authorization and no reporting obligation; an agent commits to an offer only on the execute path (ExecuteTransaction). Discovery is therefore safe to retry. A transport failure, such as an unreachable Exchange or a timeout, is the client's concern: the protocol defines no transient-failure code for it.
ExecuteTransactionTransactionRequestTransactionResponseCommit to an offer and receive delivery information. Steps 4-5 in the FORA flow. Every per-item decision is answered in the body. The call returns OK with one TransactionResultItem per item, and an item the Exchange denies carries its TransactionResultItem.denial_reason. This holds whatever the item count: a one-item purchase that is denied is a successful response whose only item is denied, never a non-OK error. A non-OK answer carrying ErrorDetail.transaction_denial is used only when the Exchange refuses the whole request and decides no item (see TransactionDenial). Errors of other classes (a malformed request, a failed request signature, an internal fault) stay non-OK errors, as for every RPC.
ReportUsageUsageReportUsageReportResponseSubmit a post-usage report for a completed transaction. Step 7 in the FORA flow.
DisputeTransactionDisputeRequestDisputeResponseSignal a resource dispute for a completed transaction. Filed by the agent when delivered resource does not match what was promised (hash mismatch, resource unavailable, wrong resource). The Exchange records the dispute and initiates resolution. Resolution mechanics (refund, credit, re-delivery) are implementation- specific — this RPC standardizes the dispute signal, not the outcome. Every Exchange implements this RPC and accepts a filing that meets its checks: it is part of the core protocol, like ReportUsage, not an optional capability. A filing the Exchange accepts is answered with a DisputeResponse. A filing it refuses is a non-OK error carrying ErrorDetail.dispute_failure, whose DisputeFailureReason names the one check the filing failed.
RequestDomainVerificationDomainVerificationRequestDomainVerificationChallengeRequest a domain verification challenge for provider onboarding. Proves domain control before signing keys are pushed. Follows the ACME HTTP-01 pattern (Let's Encrypt).
ConfirmDomainVerificationDomainVerificationConfirmationDomainVerificationResultConfirm domain verification and register a signing key. Called after the challenge token is placed at the provider's domain.
RegisterRegisterRequestRegisterResponseCreate the calling agent's account with the Exchange. The caller's identity is proven by the request signature — the Exchange derives who is registering from the verified signature, never from the request body. Registering again for the same agent returns the same billing_ref (idempotent by design), which is why this RPC carries no idempotency_key; the repeat is answered from the stored account record and runs none of the account-creation gates — see "Repeat registration" in the Agent Account Registration section header. A refused registration travels as a non-OK transport error carrying ErrorDetail.registration_failure.
GetAccountStatusGetAccountStatusRequestGetAccountStatusResponseRead-only check of whether the calling agent's account is active. Identity comes from the request signature, so the request carries no identifying field. An agent the Exchange knows, its request signature verified, but that holds no account at this Exchange is answered with the Connect code NOT_FOUND. An agent whose signature does not verify is UNAUTHENTICATED, as on every RPC. A request whose ext_critical lists a key the Exchange does not understand is INVALID_ARGUMENT with no typed reason (see "Critical extensions" in the file header).

Optional RPC for providers/CMS/third-party intelligence providers to push content metadata. A push is all-or-nothing at both validation tiers: a hard rejection anywhere in the submission refuses the entire submission and persists nothing, and the refusal names the entries that failed so the publisher can fix and resubmit the whole set. That per-entry detail is reporting, not partial acceptance.

RPCRequestResponseDescription
PushResourcesPushResourcesRequestPushResourcesResponsePush or update resource entries in the Exchange catalog.
RemoveResourcesRemoveResourcesRequestRemoveResourcesResponseRemove resource entries. The request names each resource the way a push does, by domain and path (RemoveResourcesRequest.resources). For each one the Exchange resolves /.well-known/fora.json from its domain and checks that the signer of the request is authorized to push for that domain, exactly as for PushResources, and refuses the request as a push is refused when it is not. The caller is the party that signs the request.
RefreshCatalogRefreshCatalogRequestRefreshCatalogResponseTrigger a full catalog refresh from configured sources.

The Broker entry point. It has two methods.

Resolve(DiscoveryRequest) → DiscoveryResponse is discovery-only: a client sends the URIs/query it wants resolved and receives offer_groups (one OfferGroup per URI, each carrying the full signed Offer) or a typed absence_reason with empty offer_groups when nothing licensable was found. “No result” is a successful answer; malformed requests, auth failures, and internal faults are non-OK transport errors carrying an ErrorDetail. Resolve may apply the agent’s budget (RequestConstraints) as a filter. How the budget is computed is the Broker’s own behaviour; the protocol fixes how its effect is reported: a URI left with no offer is answered with OFFER_ABSENCE_REASON_BUDGET_EXCEEDED, never NOT_AUTHORIZED and never an error such as RESOURCE_EXHAUSTED. Resolve never charges. Resolve does not deliver inline. The per-transaction result (transaction id, billing id, retrieval endpoint, …) comes from the separate execute path, which the agent takes either directly, with ExchangeService.ExecuteTransaction at each offer’s Offer.exchange, or through the Broker.

ExecuteTransaction(TransactionRequest) → BrokerTransactionResponse buys offers from any number of Exchanges in one call. The Broker verifies the agent’s request, groups the items by each signed offer’s exchange, and sends one ExchangeService.ExecuteTransaction per Exchange, signed with its own key. The agent’s per-item AgentAcceptance and its AgentRequestAcceptance travel in each sub-request body, so every Exchange still verifies the agent’s consent. The Broker combines the answers in request order. Re-packaging is safe because each item is atomic and its integrity is per resource: the offer carries its Exchange’s signature, the acceptance binds the agent to that one offer, and the result comes from the Exchange that owns the resource. The Broker’s own refusals (a bad agent signature, a requester.domain that is not the agent’s signing directory, a malformed request, an Exchange it cannot route to or does not approve) are non-OK errors and buy nothing. An Exchange’s per-item denial, including DENIAL_REASON_RELAY_NOT_ACCEPTED (decided per item by each offer’s provider), is that Exchange’s result item unchanged. An Exchange’s refusal of its whole sub-request is never turned into a Broker error: it rides in the body, on each affected item’s refusal. The purchase is the agent’s, billed to and owned by the agent’s account at each Exchange, and the agent’s idempotency_key is scoped to the agent whatever the route (see Accounts).

RPCRequestResponseDescription
ResolveDiscoveryRequestDiscoveryResponseResolve runs the broker discovery flow for the requested URIs/query: it fans out to one or more Exchanges and returns the merged offers. It is pure discovery — it selects and returns offers, never executes a transaction, so it neither charges nor produces transaction denials. A denial is raised only when the agent later buys a selected offer — either directly, calling ExchangeService.ExecuteTransaction at the offer's Exchange, or through BrokerService.ExecuteTransaction — and rides there on the result item's denial_reason. A result returns OK with offers populated on DiscoveryResponse.offer_groups (one OfferGroup per requested URI). A request that ran but yielded nothing licensable (not in catalog, no offers, entitlement/budget absence, upstream temporarily unavailable) returns OK with DiscoveryResponse.absence_reason set and empty offer_groups — "no result" is a successful answer, mirroring DiscoverResources (ADR-019 §2). Here "authz" means resource entitlement (→ OK + absence); transport authentication failures are a different axis and, like malformed requests and internal faults, are non-OK transport errors carrying an ErrorDetail. Budget. Resolve may apply the agent's budget (RequestConstraints) as a filter over the offers it returns. How the budget is computed is the Broker's own behaviour, and the protocol does not specify it. What the protocol fixes is how its effect is reported: an offer the budget leaves out is not returned, a URI the budget leaves with no offer is answered with OFFER_ABSENCE_REASON_BUDGET_EXCEEDED, and so is the whole resolve when the budget leaves no offer at all. A budget is never reported as OFFER_ABSENCE_REASON_NOT_AUTHORIZED and never as a non-OK error such as RESOURCE_EXHAUSTED. Resolve never charges: returning an offer reserves nothing.
ExecuteTransactionTransactionRequestBrokerTransactionResponseExecuteTransaction buys one or more offers, issued by one or more Exchanges, in one call. The request is the TransactionRequest an agent would send an Exchange, with every item's AgentAcceptance and the AgentRequestAcceptance over all items, signed (RFC 9421) by the agent for the Broker's URL. The request terminates at the Broker; it is not forwarded. The Broker RE-PACKAGES the purchase. It verifies the agent's request, groups the items by each signed offer's exchange, and sends one ExchangeService.ExecuteTransaction per Exchange. Each sub-request is a new request the Broker authors and signs with ITS OWN key; it carries that Exchange's items in request order, each with its reflected Offer and the agent's AgentAcceptance, plus the agent's AgentRequestAcceptance unchanged, the agent's requester and the agent's idempotency_key unchanged. So each Exchange still verifies the agent's consent: the acceptances are detached signatures over the body, and they do not depend on who signed the request. The Broker then combines the answers into one BrokerTransactionResponse. Why re-packaging and combining are safe: each item is atomic, and its integrity is per resource. Each offer carries the signature of the Exchange that issued it, covering price, terms, expiry and exchange; each item carries the agent's acceptance of that one offer, bound to the requester and the idempotency key; and each item's result comes from the Exchange that owns that resource. The Broker therefore assembles independent per-resource purchases. It cannot change what an item buys, from which Exchange, at what price or on what terms, or for whom, without breaking that offer's Exchange signature or the agent's acceptance; it cannot add an item the agent did not accept; and it cannot drop, add or reorder items within one Exchange's sub-request without that Exchange seeing it, because the AgentRequestAcceptance fixes the complete ordered set: that Exchange denies every item of such a sub-request with DENIAL_REASON_SIGNATURE_INVALID, in the body, and purchases none of them. No cross-item integrity is needed, and none is provided. On the response side, the one signed value in a result item is retrieval_endpoint, a URL the issuing Exchange signed, so the Broker cannot forge or alter one. Every other field of a result item, and every field of the combined response, is unsigned: it is the Broker's report of what each Exchange answered, and the charge itself is still bounded by the signed offer the Exchange verified. The Broker's own refusals are non-OK Connect errors carrying an ErrorDetail, and when the Broker refuses, it has sent no sub-request, so nothing was bought. They come first, in this order, and only a request that passes them reaches the critical-extension check (see "Critical extensions" in the file header), then routing: - The agent's request signature does not verify: UNAUTHENTICATED with request_auth_failure, exactly as at an Exchange. - requester.domain is not the agent's verified signing directory — the host of the origin the agent's covered Signature-Agent member names, the directory the request-signing key resolved from: UNAUTHENTICATED with request_auth_failure SIGNATURE_INVALID. The signature verifies, but not as the requester the body claims, which is a signature that does not verify for that requester. The Broker refuses it because every Exchange would: they resolve the acceptance keys from requester.domain. - A malformed request — no items, an item with no offer or no AgentAcceptance, no AgentRequestAcceptance, or an AgentRequestAcceptance whose payload does not list the request's items in order (each offer.signature and offer.exchange) or names another requester or idempotency key: INVALID_ARGUMENT. - An item whose offer.exchange the Broker cannot route to, or does not approve for purchases: a non-OK error (FAILED_PRECONDITION), with ErrorDetail.metadata["exchange"] naming that host. The Broker checks every item before it sends anything, so one unroutable Exchange refuses the whole call rather than buying the rest. Upstream answers are ALWAYS IN THE BODY. Once the Broker has sent its sub-requests the call returns OK. An Exchange's refusal of its whole sub-request — a non-OK answer, or no answer at all — is never turned into an error of the Broker's own: it rides on each affected item as TransactionResultItem.refusal, and the other Exchanges' results come back unchanged. An Exchange's per-item denial is that Exchange's result item, unchanged; a one-item sub-request an Exchange denies is such a result item, because an Exchange answers every per-item denial in the body. A provider that does not accept relayed purchases is one such per-item denial, DENIAL_REASON_RELAY_NOT_ACCEPTED, decided by each offer's provider: the other items of the same sub-request may succeed. Only a refusal of the whole sub-request, which decides no item, becomes a refusal on each of its items. DENIAL_REASON_CONTENT_UNAVAILABLE is not a catch-all for upstream failures; the Broker never stamps it, or any denial_reason, onto an item an Exchange did not deny. The one exception is the Broker's own answer to a key the agent's request lists in ext_critical that the Broker does not understand: it then denies every item as DENIAL_REASON_UNKNOWN_CRITICAL_EXTENSION, in the body, and sends no sub-request (see "Critical extensions" in the file header). If an Exchange answers OK without exactly one result item per item sent, the Broker cannot attribute the answer, and reports every item of that sub-request with a refusal whose code is "internal" and no detail. A retry is safe: the Broker forwards the agent's idempotency_key unchanged, so each Exchange answers a repeated sub-request from its stored result (see "Idempotency" in the file header). That is also how an agent settles an item whose refusal left the outcome unknown (code "unavailable" or "deadline_exceeded"): it retries the same request with the same key, through the same Broker, another one, or directly. The purchase is the agent's (see "Accounts" in the file header).
FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
requesterRequester3Requester identity — who is making this request, the scopes it states, and optional delegation chain. REQUIRED: a query names its requester, whose Requester.id and Requester.domain are required in turn.
urisrepeated string8Resource URIs being queried.
acceptable_restrictionsrepeated AcceptableRestriction9The limits this query operates within, per restriction axis (function, geography, user-type, …) — see AcceptableRestriction. Advisory selection inputs the Exchange/Broker MAY pre-select offers against (convenience, not enforcement); the agent self-selects and bears compliance.
deadlineoptional Duration6Maximum time the caller will wait for a response. Exchange SHOULD prioritize speed over completeness when tight. Absent = "0.5s" default (proto-JSON encodes Duration as seconds).
supported_profilesrepeated string7Domain extension profiles the caller understands. Declares which ext field vocabularies the caller can parse and act on. The Exchange SHOULD include profile-specific ext fields in Offers when the caller declares support. The Exchange MAY skip expensive metadata computation (e.g., retraction checking, consolidation verification) when the caller does not declare the relevant profile. Absence means "send all available metadata" — Exchange MUST NOT withhold ext fields solely because the caller omitted this field. Values match the Exchange's WellKnownManifest.supported_profiles entries. Examples: ["fora-news-v1", "fora-academic-v1", "fora-legal-v1"]
exchangestring10REQUIRED. Bare host of the recipient this request is addressed to (e.g. "exchange.example" or "exchange.example:8081"). See "Request recipient" in the file header for the full contract, including the recipient's duty to reject a request that names someone else.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

The limits a query operates within on one restriction axis, in the same RestrictionKind vocabulary the terms use. The Exchange/Broker MAY pre-select offers whose term restrictions fall within these (convenience, not enforcement). Used in ResourceQuery and DiscoveryRequest.

FieldTypeNumberDescription
axisRestrictionKind1Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY / USER_TYPE / OTHER.
valuesrepeated string2The values the query operates within on this axis — same token vocabulary as the terms (e.g. FUNCTION ["ai-train"], GEOGRAPHY ["US", "EU"]).
FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
exchangestring3Canonical domain of the responding Exchange, in the shape "Request recipient" defines in the file header. The response counterpart of the recipient field on the request: it names who answered.
offersrepeated Offer4Flat list of offers (for single-URI queries).
offer_groupsrepeated OfferGroup5Offers grouped by requested URI (for multi-URI batch queries). When populated, offers SHOULD be empty to avoid ambiguity.
rate_limitoptional RateLimitInfo6Rate limit status for this caller. Present when the Exchange enforces per-caller rate limits on discovery. Enables agents/Brokers to throttle proactively rather than hitting hard limits. Particularly important when a Broker fans out the same batch query to multiple Exchanges — mid-batch rate limiting can cause partial results if not signaled early.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.
FieldTypeNumberDescription
uristring1The URI this group of offers is for (echoed from ResourceQuery.uris).
offersrepeated Offer2Zero or more offers for this URI. Empty = resource not available.
discovery_methodoptional DiscoveryMethod3How this URI was discovered by the Broker (v2 extension point). v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange). v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa), DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs through any source, then routes through Exchange for pricing/transaction. The discovery method does not affect the transaction flow — it's metadata for the agent to understand how the resource was found.
absence_reasonoptional OfferAbsenceReason4Why no offers are available for this URI. Set only when offers is empty, and not always then: a resource the requester's scopes leave with no presentable offer is answered with empty offers and this field unset (the existence-hiding rule, stated on Requester.scopes). Enables agents/Brokers to distinguish "resource not in catalog" from "resource blocked for your use case" without trial-and-error transactions. Analogous to OpenRTB nbr codes and Shutterstock per-item error metadata in batch responses.
restriction_filtersrepeated RestrictionKind5When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove the convenience pre-filter, in the same RestrictionKind vocabulary the terms use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term). Advisory diagnostics, not an enforcement verdict.

Rate limit status modeled after IETF RateLimit header fields.

FieldTypeNumberDescription
limitint321Maximum requests allowed in the current window.
remainingint322Requests remaining in the current window.
reset_atTimestamp3When the current window resets (UTC). After this time, remaining resets to limit.
windowoptional Duration4Duration of the rate limit window (e.g. 60s = per-minute limit).

Identity and entitlements only — who is asking and what they’re entitled to. What they’re asking for (uris) and the limits they’ll accept (acceptable_restrictions) live on the ask (ResourceQuery / DiscoveryRequest), not here. Used in ResourceQuery, DiscoveryRequest and TransactionRequest.

id and domain are both required. domain is the host of the requester’s key directory and is never a free label: on a direct request a verifier resolves the agent’s keys from the covered Signature-Agent header and requires domain to name that same directory, refusing a mismatch as unauthenticated with request_auth_failure SIGNATURE_INVALID; on a purchase relayed through BrokerService.ExecuteTransaction, the Exchange verifies the agent’s acceptances against the directory domain names, because the request signature there is the Broker’s. id is a free label the agent chooses for attribution, such as which sub-agent or end customer is asking; it is never identity, never used to find keys, never selects an account, and never trusted. See Requester domain and the key directory and Accounts.

scopes is the requester’s own statement, which the Exchange does not verify, and decides which terms are presented; an Exchange may also require a verified delegation before it presents a scoped term. It hides existence: a requester never learns about a resource outside its scopes. When the scopes leave no presentable offer for a resource, the Exchange answers with no offers and no absence reason, exactly as for any resource with nothing to offer, and an offer it presented is honoured until it expires. See Existence hiding.

FieldTypeNumberDescription
idstring1REQUIRED. A free label the agent chooses for attribution (e.g. "agent-research-bot-001"), 1 to 255 characters. Use it to tell apart the sub-agents or end customers that share one key directory. It is never identity: no verifier finds keys from it, and no party trusts it for an authentication or authorization decision. The requester's identity is domain together with the key that verifies. The acceptance signatures cover this label (see AgentAcceptancePayload), so a relaying Broker cannot rewrite it, but its value is still only what the agent says.
domainstring2REQUIRED. Bare host of the requester's key directory: the WBA directory at {domain}/.well-known/http-message-signatures-directory that publishes the agent's Ed25519 keys. It carries the bare-host shape "Request recipient" defines in the file header (a port allowed), for the same structural reason: a scheme, path or query smuggled in here would choose what gets fetched, not merely from where. It is never a free label. Every verifier reads it as the name of that directory, and the rule that binds it depends on whether the request is direct or relayed, which the receiver decides as "Direct and relayed requests" in the file header states: - Direct request. The agent's own RFC 9421 signature arrives because the agent sent the request itself. The verifier resolves the agent's keys from the COVERED Signature-Agent member, never from this field. It then MUST require this field to name that same directory: the host of the origin that member names, compared by the identity rule "Request recipient" defines (the shape check first, then case-folded, an absent port the same as ":443", a subdomain a different party). A mismatch is refused as UNAUTHENTICATED with request_auth_failure SIGNATURE_INVALID: the signature verifies, but not for the requester the body names. A Broker applies this rule at BrokerService.ExecuteTransaction. At BrokerService.Resolve it refuses nothing for this field: every query it originates names the host of the origin that member names (see DiscoveryRequest.requester). An Exchange applies it on every request whose signer it does not admit as a Broker; a mismatch from a signer it does admit is a relayed request, one of the two cases below. - Purchase relayed through a Broker. BrokerService.ExecuteTransaction re-packages the purchase, so the request signature and the covered Signature-Agent member are the Broker's. They say only that the call comes from the Broker, and they sign no purchase. The Exchange MUST verify each item's AgentAcceptance, and the AgentRequestAcceptance, against the Ed25519 keys currently valid in the directory this field names (see AgentRequestAcceptance), never against the Broker's key. Those acceptances are the only agent signatures the Exchange sees, and the purchase belongs to the account of the directory they verify under (see "Accounts" in the file header). - Broker-led discovery. A ResourceQuery a Broker originated carries no agent signature, so nothing on that leg authenticates this field: it is the Broker's statement, under the Broker's own signature, of whom it queries for. A delegation on such a query is not honoured (see Delegation).
typeRequesterType3What kind of entity is making this request.
nameoptional string4Human-readable name (e.g., "Acme Research Assistant").
scopesrepeated string6Entitlement scopes: the requester's own statement of what it is entitled to. On a query a Broker originated, they are the Broker's statement for the agent. They are signed and attributable, and the Exchange does not verify them. Scope coverage decides which terms the Exchange presents (see LicenseTerm.scopes, which also states when an Exchange MAY require a verified delegation); it is not access control. A false statement is traceable to the signing key, its directory and its account, and the agent's acceptance signs the scoped offer it buys; it is settled through reconciliation and the account relationship. EXISTENCE HIDING. A requester never learns about a resource outside its scopes. When the scopes leave no presentable offer for a resource, the Exchange answers with no offers and no absence reason (an OfferGroup with empty offers and absence_reason unset), exactly as for any resource with nothing to offer. A Broker relaying that answer adds no reason of its own. At purchase, an offer the Exchange presented is honoured until it expires, so a scope refusal cannot arise there. No reason in the protocol reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent. Scope format: colon-separated segments, "{domain}:{permission}" or "{profile}:{permission}", optionally multi-segment ("dist:US:CA"); matching is segment-wise per the rule below (no implicit hierarchy). Examples: "credit:read" — can access credit reports "subscription:marketdata-2026" — has active MarketData subscription "academic:" — full access to academic resources "internal:reports" — can access internal reports "" — unrestricted (public Exchange default) Matching is SEGMENT-WISE (":" separated). A granted scope G covers a required scope R iff, segment by segment, each G segment equals the corresponding R segment or is ""; a terminal "" matches all remaining segments, of which there must be at least one. There is NO implicit prefix match, and a grant NARROWER than the requirement does not cover it (G must be equal-to-or-broader than R), so a grant with more segments than the requirement never covers it, even when its last segment is "". A "" in R is a literal segment: a narrow grant never covers a wildcard requirement. Matching is case-sensitive and never trims. Examples: "dist:" covers "dist:US", "dist:US:CA" and "dist:US:" but not "dist"; "dist:US:" covers "dist:US:CA" but not "dist:EU" or "dist:US"; "publisher::read" covers "publisher:news:read" but not "publisher:news:"; bare "dist" covers only "dist"; granted "dist:US:CA" does NOT cover required "dist:US"; "" covers everything. This same rule governs LicenseTerm.scopes and the attenuation of a delegation child against its parent — one algorithm protocol-wide. When empty, Exchange applies its default access policy (typically returns all publicly available resources).
delegationoptional Delegation7Optional delegation — present when the requester acts on behalf of another entity (user, organization, upstream agent). On a query a Broker originated it is not honoured, because the Broker signs alone and nothing on that leg can pass its holder binding; the Exchange answers as if none were present (see Delegation).
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Scoped, time-limited credential. The credential is carried in token, at most 32768 bytes, and token_format selects how to interpret it: "jwt" (the default and sole format; the field stays open for a future one). The format is never encoded as a prefix inside the token bytes. The token is a holder-bound JWT chain nested in one compact JWS: each child carries its complete signed parent in the fora_parent claim and its own Ed25519 public key in the protected jwk header, that key hashes to the parent’s cnf.jkt (RFC 7800 cnf.jkt = RFC 7638 thumbprint), each child narrows its parent, and the holder proves possession of the key the last cnf.jkt names by signing the request (RFC 9421). The root is verified with a key the Exchange has configured for its iss; no key is ever fetched. The complete profile, with the required protected header (typ: fora-delegation+jwt, alg: EdDSA or Ed25519), the required claims (iss, aud, cnf.jkt, exp), the clock leeway and the limits, is the Delegation section of the authentication page.

FieldTypeNumberDescription
principal_domainstring1Mirror of the ROOT token's iss, the configured trust anchor's identity. Informational: no key is looked up from it, and it establishes no trust.
principal_idstring2Principal's identifier (e.g., "user@acme.com", "marketdata.example.com"). Informational; not read by the verifier.
scopesrepeated string3Mirror of the terminal token's effective scopes. In the chain, each child's scopes are covered by its parent's under the scope-matching rule on Requester.scopes (attenuation — can only narrow, not widen).
expires_atTimestamp4Mirror of the chain's earliest exp. The Exchange MUST reject an expired token; the signed exp values decide, not this field.
max_spend_centsoptional int645Maximum spend in currency minor units (e.g., cents for USD). Exchange tracks cumulative spend against this cap.
max_accessesoptional int329Maximum number of accesses allowed under this delegation. Exchange tracks cumulative access count against this cap. Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit. For subscriptions with "10,000 accesses/month", this carries the ceiling.
quota_periodoptional Duration10Quota reset period. How often the access/spend counters reset. Example: 30 days for monthly subscriptions — "2592000s" on the wire (proto-JSON encodes Duration as seconds; "720h" is not accepted). When absent, the quota is lifetime (bounded only by expires_at).
tokenbytes6Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the complete chain, every embedded parent included, must fit. The limit is a wire rule, checked before the token is parsed and independent of whether the bytes are a valid JWT; a longer token is an invalid request, not a per-item denial.
token_formatstring7Token format: "jwt" (default). Empty is treated as "jwt". The field stays open for a future format.
revocation_urioptional string8Informational. Revocation lookup is not part of the delegation profile: the verifier never fetches this URI. A chain is invalidated early by withdrawing the configured root key.
issueroptional string11Mirror of the ROOT token's iss, the identity configured with the trusted key (never an intermediate or terminal issuer). Informational: the Exchange never discovers a key from it, through OIDC, JWKS or otherwise.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Delegation claims. The claim vocabulary carried inside the token is not re-listed here — the authoritative registry is the Delegation-Claims Profile table in authentication.mdx. Two points are worth restating. Every token MUST carry iss, aud, a finite exp and the holder binding (cnf.jkt = the RFC 7638 thumbprint of the holder key): the key the request proves possession of MUST hash to the last token’s cnf.jkt, or the chain is rejected; without it the token would be bearer-usable. On a purchase a Broker re-packaged (BrokerService.ExecuteTransaction), the request reaching the Exchange is signed by the Broker, so the Exchange checks cnf.jkt against the key each item’s AgentAcceptance verifies under instead of the request signer. And a claim the profile does not list is rejected, never ignored. The plaintext fields of this message mirror the chain (issuer and principal_domain mirror the root’s iss) and establish nothing.

Narrowing example: An agent can further restrict (but never widen) a delegation by issuing a child token:

  • Root: scope: credit:* dist:*, exp in two hours
  • Child: scope: credit:read dist:US:*, exp in one hour, signed by the key the root’s cnf.jkt names, with the root inside fora_parent

Multi-hop forwarding (no message — HTTP layer)

Section titled “Multi-hop forwarding (no message — HTTP layer)”

There is no in-message hop chain. Request authentication is carried in RFC 9421 HTTP Message Signatures in the headers: each party signs the request it sends with one labeled signature. A Broker originates its own discovery queries and re-packages execute requests, signing each alone; on a purchase the agent’s identity and consent are AgentAcceptance in the body. Only where a party forwards a request unchanged and covers the signature before it do the signatures form a tamper-evident chain, and FORA’s Broker never does (see Multi-hop forwarding). The Exchange counts the signatures against WellKnownManifest.max_intermediary_hops before it verifies any, then resolves each keyid (an RFC 7638 thumbprint) in the directory the signer’s own Signature-Agent member names, at {origin}/.well-known/http-message-signatures-directory. RequestConstraints.max_hops never reaches an Exchange: it rides only on DiscoveryRequest, and no party enforces it in this version. Each request is answered to the party that sent it, and delivery needs no relay, because the signed retrieval_endpoint is bound to the agent (agent_identity_hash).

BrokerService.ExecuteTransaction terminates at the Broker, which authors and signs one new request per Exchange. The agent’s consent travels in the body as AgentAcceptance and AgentRequestAcceptance, not as a request signature.

FieldTypeNumberDescription
offer_idstring1Unique identifier for this offer, assigned by the Exchange. Opaque to the caller: not derived from the resource, its URL, or any other field, and carries no meaning beyond identifying this offer. Two offers for the same resource have different offer_ids.
titleoptional string2Resource title (human-readable, for display/logging).
pricingPricing3The offer's price, and its ONLY price. An offer represents a single licensing arrangement: each projected LicenseTerm yields its own offer, and the Exchange moves that term's price here, so the term inside terms carries no pricing of its own (the offer.terms.pricing_unset rule). This is the price execute charges, the price a Broker ranks and compares across Exchanges, and the price a metered purchase is charged at. A resource with multiple alternative terms (e.g. dual-licensed) produces multiple separate offers, one per term — never one offer with a "headline" picked among them. It derives from exactly one catalog term: the term's pricing as the publisher declared it (ResourceEntry.terms). Each offer derives from one term, and the offer carries no second copy, so the offer's price and its term's price cannot disagree. On a metered (PER_UNIT) offer, the rate and the estimate a metered purchase is charged at are read from here (see Pricing). The estimate is optional. When the publisher states one — on the term's own pricing, or once for the resource on ResourceEntry.estimated_quantity — the Exchange carries it here as estimated_quantity, the term's own estimate taking precedence. When the publisher states none, the offer may carry none, and the purchase then charges one unit.
delivery_methodDeliveryMethod4How resource will be delivered.
reportingoptional ReportingObligation5Post-usage reporting requirements for this offer.
expires_atoptional Timestamp6When this offer expires (ISO 8601). An offer for a term gated behind a verified delegation MUST NOT expire later than that delegation chain's effective expiry, the earliest exp in the chain (see Delegation).
identityoptional ResourceIdentity7Resource identity for cross-exchange deduplication. Enables Brokers to recognize the same resource offered by different Exchanges and compare pricing.
exchangestring8REQUIRED. Bare host of the Exchange that issued this offer (e.g. "exchange.example" or "exchange.example:8081"), in the form "Request recipient" defines in the file header. This is the execute-routing target: the agent, or a relaying Broker, sends the ExecuteTransaction call for this offer to this Exchange, and a Broker relaying a mixed batch groups the items by this value. Because it is an ordinary Offer field it falls inside the signed bytes (see signature below — the signature covers every field except signature / signature_algorithm), so an intermediary cannot redirect the execute call to a different Exchange without invalidating the offer, and it is what retires the X-FORA-Exchange-Endpoint transport header. It is also the audience statement of an ExecuteTransaction, which is why TransactionRequest carries no top-level exchange: on receipt, an Exchange MUST reject the request unless EVERY item's offer.exchange names its own domain. Presence is enforced because an empty value is unroutable — a relaying Broker has nothing to group or dial on, and the swap-protection above is vacuous when the signed bytes carry no recipient at all.
signaturestring9REQUIRED. Hex-encoded detached Ed25519 signature over the canonical serialization of the ENTIRE Offer — every field, including pricing, terms (the full licensing payload), expires_at, and exchange. Only signature and signature_algorithm are excluded from the signed bytes. expires_at is signed so the offer's validity window is integrity-protected: a relaying Broker cannot extend (or shorten) the TTL of a signed offer to replay it outside the window the Exchange intended. CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes are: signed_payload = JCS( protojson(msg with signature + signature_algorithm cleared) ) i.e. render the message to canonical proto-JSON with the PINNED option set below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic protobuf BINARY marshaling is explicitly NOT canonical across languages and versions (protobuf's own caveat), so it cannot be a cross-language signing primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS, Python) without a protobuf binary codec, so a broker/exchange/client in any language signs and verifies byte-identically. This same definition applies to the agent offer-acceptance signature (AgentAcceptance.signature). PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector — whatever these options render MUST be byte-identical across all languages): - enum values as NAME strings (not numbers); - int64 / uint64 / fixed64 as decimal STRINGS; - bytes as standard (padded) base64; - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules (RFC 3339 string for Timestamp); - unpopulated fields are OMITTED (never emitted as defaults); - field naming is snake_case (the proto field name, UseProtoNames=true), the naming every SDK target shares — wire, corpus, and signed form are all snake_case; - google.protobuf.Struct (ext) → a plain JSON object; JCS then sorts its keys recursively, so the Struct case needs no special handling. UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or PRESERVES it, and the rule follows from which: - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a canonicalizer CANNOT reproduce the signed bytes of a message carrying unknown fields — what it renders silently drops part of what the signer covered. It MUST refuse the message rather than emit the reduced bytes, and a verifier built on it MUST reject rather than verify over them. The refusal binds at EVERY depth: a nested message and each element of a repeated or map field carries its own unknown-field set. - PRESERVING (a canonicalizer that carries unrecognized members through): it reproduces the signed bytes faithfully, so there is nothing to refuse. Either way an APPENDED field cannot pass: an omitting canonicalizer refuses the message, and a preserving one renders the appended member into bytes the signer never covered, so the signature fails. Without the refusal the omitting case would fail OPEN — an intermediary could add unknown fields to an already-signed message and leave its signature verifying, smuggling unauthenticated content through a message the recipient treats as verified. Extensions therefore ride in ext / ext_critical, which are defined fields and inside the signed bytes — never as undeclared field numbers. Because the signature covers terms, pricing, expires_at, and exchange, an intermediary (Broker) cannot tamper with price, restrictions, quotas, obligations, the expiry, the execute-routing target, or any licensing term without invalidating it. Agent SHOULD verify the signature (RFC 2119) against the Exchange's public key, and MUST reject an offer whose expires_at is in the past.
signature_algorithmstring10JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for Ed25519. Advisory only: this field is cleared before the canonical payload is signed, so it is not covered by the signature.
subscription_idoptional string11If set, this offer is available under an existing subscription/deal. No per-request billing — usage tracked against subscription quota. Pricing.rate = "0" for subscription offers (zero marginal cost). The Broker SHOULD prefer subscription offers when available.
iab_categoriesrepeated string13IAB Content Taxonomy category codes. Enables agents to filter offers by topic (e.g., "only finance resources"). Uses IAB Content Taxonomy 3.1 codes.
attestationsrepeated ResourceAttestation14Signed attestations about the resource at this URI. Attestations provide cryptographic proof of resource properties from trusted parties (providers or verification vendors). Three verification levels determine what is independently verifiable: Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID) for identification, but nothing is cryptographically verifiable. Only CDN delivery failure is auto-disputable. Level 1 (self-attested): Provider signs own claims with Ed25519 key. Agent can independently verify content hash and token count. CDN delivery failure + content hash mismatch are auto-disputable. Level 2 (third-party attested): Independent verification vendor crawled the resource and attested to its properties. Agent trusts the attestation (does not re-verify hash). Token count discrepancy is auto-disputable when corroborated by CDN response size. Multiple attestations may be present (e.g., provider self-attestation plus a third-party verification). Agents choose which to trust.
data_as_ofoptional Timestamp16When the offered data was current. For dynamic resources (resource_mutability = DYNAMIC), this is the snapshot timestamp. Enables the Broker to evaluate freshness: "this credit report reflects data as of March 18" or "this drug database was updated today." Not set for STATIC resources (content doesn't change) or LIVE resources (content doesn't exist yet). The Broker compares this against RequestConstraints.max_data_age to filter stale offers. Example: agent requests max_data_age = 7 days, Broker drops offers where now() - data_as_of > 7 days.
subscription_quotarepeated SubscriptionQuotaInfo17Subscription quota state, when this offer is under a subscription. Enables the agent to see remaining quota before committing. Multiple entries when the subscription has independent quotas (e.g., access count + spend cap).
previewsrepeated Preview18Lightweight previews for offer evaluation. The Exchange holds URLs (50–200 bytes each); the provider's CDN serves the actual bytes. Agents fetch previews only when evaluating offers — not on every discovery query. Multiple previews at different sizes allow agents to pick the cheapest fetch for their evaluation needs. Per content type: Image: watermarked thumbnail (150–450px JPEG) Video: short clip (10–30s MP4, watermarked) Audio: short clip (15–30s MP3, low-bitrate or watermarked) Text: snippet or abstract (first 200 words as text/plain) Data: sample records (1–3 rows as application/json) Stream: optional frame capture or none (streams are priced by time) Modeled after Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to 30s clip), IIIF (parameterized image URLs), and OpenRTB native (img.url + dimensions).
termsrepeated LicenseTerm19The licensing term this offer sells, sourced from the publisher's ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked for: an offer IS a single licensing arrangement, so N terms on a resource project to N offers on that resource, each selling one of them — see pricing above. An offer with no term would be an offer projected from nothing, which is why the floor is one and not zero. The field is repeated because it shipped that way in v1 and renumbering a released field is not available to us, so the cardinality rides as a validation rule instead. ResourceEntry.terms is the plural side, bounded at 32: a resource carries many terms, an offer sells one of them. The distinction is not cosmetic. pricing is what a Broker ranks on and what execute charges, so a term that is not on its own offer has no price a Broker can compare and no offer_id an agent can buy. Fusing several terms onto one offer makes every term but the first unsellable — a dual-licensed resource sells only under whichever arrangement the publisher happened to store first. The term here carries everything the publisher declared EXCEPT its price: its pricing MUST be unset (the offer.terms.pricing_unset rule), because the offer's price is pricing above. Its semantics, license, restrictions, quotas, obligations, scopes and part_label are the publisher's, unchanged. Where a term is reachable only under an existing subscription, its offer carries subscription_id and prices at zero marginal cost; the terms a requester may see at all are selected by LicenseTerm.scopes. See: Universal Licensing Core section.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Signed envelope of claims from a trusted party (provider or verification vendor) about content at a specific URI.

FieldTypeNumberDescription
verifierstring1Canonical domain of the attesting party (e.g., "nytimes.com" for self-attestation, "doubleverify.com" for third-party attestation). Used to look up the verifier's attestation-signing keys in its WBA directory (WBAFile.keys) at https://{verifier}/.well-known/http-message-signatures-directory
keyidstring2RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's attestation-signing key, resolved against the verifier's WBA directory (WBAFile.keys). Identifies which Ed25519 key signed this attestation. Enables key rotation: new keys are published with overlapping validity, new attestations use the new key's thumbprint, old attestations remain verifiable while the old key is still published.
attested_atTimestamp3When this attestation was created. Agents use this to assess freshness (e.g., "I accept attestations up to N hours old for breaking news").
uristring4The resource URI this attestation covers. Must match the URI in the Offer or ResourceEntry this attestation is attached to.
claimsStruct5Signed claims about the resource (max 4KB). A JSON object containing whatever properties the attesting party can determine about the resource. Recommended claim names for interoperability: estimated_quantity (integer): estimated consumption quantity (e.g., token count for text) word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text) language (string): ISO 639-1 language code iab_categories (string[]): IAB Content Taxonomy 3.1 codes content_hash (string): hash of content in "method:hexdigest" format hash_method (string): algorithm used for content_hash Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment). The protocol does NOT define "quality score" — it is inherently subjective. If a vendor provides a proprietary score, the vendor defines what it means via their WellKnownManifest ext["fora.attestation.claims_schema"].
signaturestring6Ed25519 signature over JCS-canonicalized (RFC 8785) representation of {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting, ECMAScript number serialization, strict string escaping, no whitespace. Each attestation is self-contained — new claim fields do not invalidate old attestations because the signature covers the specific claims instance.

Three verification levels:

LevelConditionWhat’s Verifiable
0 — Noneattestations emptyCDN delivery failure only
1 — Self-attestedverifier matches provider domainContent hash + token count
2 — Third-partyverifier is a verification vendorToken count (with CDN corroboration)
FieldTypeNumberDescription
modelPricingModel1Provider's pricing model.
ratestring2Price in the provider's model, as an exact decimal string — e.g. "0.05" = $0.05 per article. NOT a float: money is decimal to avoid binary rounding and to allow arbitrary sub-cent precision (e.g. "0.0001234"). Denominated in currency.
currencystring3ISO 4217 currency code (e.g. "USD", "EUR").
unit_costoptional string4Normalized cost per unit — the universal comparison metric, exact decimal string. For text: cost per token. For video: cost per second. For data: cost per record. For APIs: cost per call. Denominated in the Exchange's base_currency (from its WellKnownManifest).
estimated_quantityoptional int325Estimated quantity in the metering unit (unit). For text: token count. For video: duration in seconds. For documents: page count. For data: record count. OPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer. It is E in the metered price rule above. When a metered offer states it, it is positive (the offer.metered.estimate_positive rule), and the purchase charges E × rate. When a metered offer states none, the purchase charges one unit, 1 × rate. A PER_UNIT price may state a rate with or without an estimate; whether to state one is the publisher's decision, and nothing requires it. The estimate describes the resource rather than the arrangement, so a publisher may state it once on ResourceEntry.estimated_quantity instead of on each term. An estimate the publisher states either way is carried onto the offer's pricing (see Offer.pricing).
license_duration_monthsoptional int327License duration in months. How long the granted access remains valid.
unitoptional string8Metering basis — the "per what" of PER_UNIT pricing. REQUIRED when model = PER_UNIT. Custom units namespace as "vendor:unit". Ignored for FREE / FLAT. The (fora.v1.vocab) entries below are the SOLE authored source of the registered bare tokens. A buf plugin reads them structurally and emits the pricingunits constants + IsRegistered; ingest enforces membership from those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) — it never lists the tokens, so it cannot drift from the registry.
meteringoptional PricingMetering9How usage is tracked for billing reconciliation. Absent = PRICING_METERING_ONLINE (default real-time tracking). NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction. OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.

Pricing has no ext / ext_critical fields — the licensing core is closed.

Metered price. With R = rate and E = estimated_quantity (optional, positive when stated), a metered purchase charges E × R, or one unit, 1 × R, when the offer states no estimate, and the charge is final. A disagreement about the quantity consumed is a dispute, which this version does not define further. See Charging a metered purchase.

Layered content identification for cross-exchange dedup and integrity verification.

FieldTypeNumberDescription
canonical_urloptional string1Provider's authoritative URL for this resource (rel="canonical"). Always available. Different per provider for syndicated content.
doioptional string2Digital Object Identifier — persistent, never changes.
iptc_guidoptional string3IPTC NewsML-G2 globally unique identifier. Present when resource flows through news wire syndication (AP, Reuters).
isnioptional string4International Standard Name Identifier for the creator.
content_hashoptional string5Hash of the content. Interpretation depends on hash_method: "simhash-v1" → locality-sensitive hash, for fuzzy dedup (Level 1) "sha256" → exact-match integrity hash (Level 2) Level 1 (SimHash): computed by Exchange from extracted text. Agent verifies that fetched content is "substantially similar." Tolerates dynamic page elements. Level 2 (SHA-256): computed by provider from deterministic payload. Agent verifies exact match. Requires provider to serve consistent content (e.g., API endpoint, static HTML, structured JSON). Mismatch = dispute. Commands premium pricing.
hash_methodoptional string6Hash algorithm and verification level. Examples: "simhash-v1", "minhash-v1", "sha256", "sha384"
resource_mutabilityResourceMutability8Signals whether this resource's content is stable, changes over time, or does not exist at offer time (live streaming). Drives hash verification behavior: STATIC: content_hash is stable. Agent SHOULD verify delivered content matches. DYNAMIC: content changes between offer and fetch (credit reports, drug databases). content_hash reflects state at offer generation time. Hash mismatch is expected and MUST NOT trigger automatic dispute. LIVE: content does not exist at offer time (streaming feeds, live broadcasts). content_hash is not applicable. The "resource" is the stream endpoint. Validated across 18 use cases: static content (articles, patents, legislation), dynamic data (credit reports, drug interactions, stock snapshots), and live streams (MarketData quotes, NPR broadcast, news monitoring feeds).
c2pa_manifestoptional string7C2PA content credentials manifest URI. Points to a sidecar or embedded C2PA manifest for this resource. C2PA-aware agents MAY follow this URI to validate the full provenance chain (creator identity, transformation history, ingredient composition) using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely on c2pa_status and c2pa-bridged attestation claims instead. Formats: Sidecar: HTTPS URI to a .c2pa manifest file Embedded: same URI as canonical_url (manifest is inside the asset) Content Credentials Cloud: https://contentcredentials.org/verify?uri=...
c2pa_statusoptional C2PAStatus9Summary validation status of the C2PA manifest. Populated by the Exchange or a verification vendor after validating the C2PA manifest. Enables agents to filter for provenance-verified content without parsing JUMBF/COSE themselves. The full C2PA validation details (signer identity, trust list, action history, training/mining status) are carried in a ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.
soft_bindingoptional string10Soft binding hash — content-derived identifier that survives format transcoding (resolution changes, compression, PDF-to-text extraction). Extracted from C2PA soft binding assertion when present. Enables post-delivery verification when the hard binding hash breaks due to legitimate format conversion. Algorithm specified in soft_binding_method. Values are algorithm-specific (e.g., perceptual hash hex string, watermark identifier).
soft_binding_methodoptional string11Algorithm used for soft_binding. Examples: "phash-v1" (perceptual hash), "c2pa-watermark" (C2PA invisible watermark), "chromaprint" (audio fingerprint).
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Quota status for a subscription within the current billing period. Appears on Offer (field 17) as a pre-commit snapshot and on TransactionResponse (field 17) as the post-transaction remaining quota. The field is repeated to support multi-dimensional quotas (e.g. access count + spend cap).

FieldTypeNumberDescription
subscription_idstring1Subscription this quota applies to.
quota_limitint322Total allowed in the current period.
quota_usedint323Used so far in the current period.
quota_remainingint324Remaining in the current period.
resets_atoptional Timestamp5When the quota counter resets (UTC).
unitoptional string6What is being metered. Distinguishes access count quotas from spend quotas from burst limits. Standard values: "accesses", "tokens", "spend_cents", "burst"

Lightweight resource preview for offer evaluation. The Exchange populates preview URLs during catalog ingestion; assets are served by the provider’s CDN, not by the Exchange. Carried on Offer (field 18, repeated).

FieldTypeNumberDescription
urlstring1URL to a preview asset (thumbnail, clip, snippet, sample). Served by the provider's CDN, not by the Exchange.
media_typestring2MIME type of the preview. Examples: "image/jpeg", "image/webp", "audio/mpeg", "video/mp4", "text/plain", "application/json"
widthoptional int323Dimensions in pixels (for images and video).
heightoptional int324Height in pixels (images and video)
durationoptional int325Duration in seconds (for audio and video clips).
sizeoptional string6Size category hint. Agents use this to select the right preview without fetching all of them. Standard values: "thumbnail" — smallest useful preview (100–150px or 5–10s) "preview" — mid-size for evaluation (300–500px or 15–30s) "sample" — larger / more detailed (for data: 1–3 sample records)
FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
idempotency_keystring2Idempotency key (REQUIRED). The server MUST dedupe on this: a replay returns the original result rather than re-executing, and the same key with different items is refused with already_exists. The key is scoped to the agent, whatever the route: a purchase repeated directly or through any Broker the Exchange admits is the same purchase, and the Broker forwards the agent's key unchanged. A key another agent or tenant used is never a collision. The full rule is "Idempotency" in the file header. The transaction's durable identity is the Exchange-assigned transaction_id in the response.
requesterRequester4Requester identity — forwarded for authorization and audit. REQUIRED: a purchase names its requester, whose Requester.id and Requester.domain are required in turn, and every acceptance binds that requester.
itemsrepeated TransactionItem7The offers committed in this request (REQUIRED, min 1), each carrying its own reflected signed Offer + detached acceptance. A single offer is the degenerate 1-element list. The Exchange verifies each item's offer.signature (which covers pricing, terms, and expires_at) over the presented bytes against its own key — stateless, self-contained bearer tokens, with no reconstruct-from-catalog.
agent_request_acceptanceoptional AgentRequestAcceptance8Optional for wire compatibility. When present, an Exchange verifies this before creating or serving request-level idempotency state. A Broker MUST forward it unchanged on every projected subrequest. Older clients that omit it retain per-item execution semantics but receive no request-level claim.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.
FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
agent_identity_hashstring10Identity that a delivered retrieval_endpoint is bound to: the RFC 7638 JWK Thumbprint of the agent's Ed25519 request-signing key (see "Retrieval-URL identity binding" above). On a sub-request a Broker re-packaged, the request signer is the Broker, and this is the thumbprint of the agent key the AgentAcceptances verify under instead. Shared across the request; set once.
itemsrepeated TransactionResultItem13Per-offer results (one entry per committed item, in original order).
total_costoptional Cost14Aggregate cost of the purchased items (those without denial_reason): their cost.amount summed as exact decimals, in their one shared currency. unit_cost is unset. UNSET when the purchased items span more than one currency: amounts are never summed across currencies, and currency conversion is out of scope for this version. A caller that needs the totals then sums each item's cost per currency itself.
subscription_quotarepeated SubscriptionQuotaInfo17Post-transaction quota state. Tells the agent how much quota remains after this transaction. Enables proactive throttling ("1 access left"). Multiple entries for multi-dimensional quotas.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

A single offer commitment within a batch transaction.

FieldTypeNumberDescription
offerOffer3The FULL signed Offer for this batch entry, reflected back exactly as received at discovery. The Exchange verifies offer.signature over these presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every batch item carries its offer.
agent_acceptanceoptional AgentAcceptance4The agent's detached acceptance signature over this item's offer. Optional on the wire; the Exchange enforces presence per item at the service layer for relayed batches. Signed bytes = the canonical AgentAcceptancePayload form, with requester_* and idempotency_key taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.

The agent’s detached acceptance signature over an accepted Offer. It travels in the execute body alongside the reflected Offer and is independent of the transport (RFC 9421) request signature, so it survives any number of broker relays. The Exchange verifies it against an Ed25519 key in the WBA directory requester.domain names and binds the delivery URL to that key (RFC 7638 thumbprint). On a purchase relayed through a Broker it is the only agent signature the Exchange sees, so it must name a requester: its canonical bytes carry a non-empty requester_id and requester_domain, a signer refuses to sign bytes that name an empty requester, and a verifier refuses an acceptance whose bytes name one, even when the signature verifies. signature is a hex-encoded detached Ed25519 signature over the JCS-canonicalized (RFC 8785) proto-JSON of AgentAcceptancePayload — the same canonical signing form Offer.signature defines, reduced here to JCS(protojson(payload)) because the payload carries no signature fields to clear; signature_algorithm is EdDSA.

FieldTypeNumberDescription
signaturestring1Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload bytes (see the canonical-signing definition on Offer.signature).
signature_algorithmstring2Signature algorithm; "EdDSA" for Ed25519.

The agent’s detached authorization of one complete ordered execute set. Its payload travels with the signature so a Broker can forward the same proof unchanged on every per-Exchange projection. A receiving Exchange verifies the agent signature and requires its subrequest to equal the complete in-order projection of signed items addressed to that Exchange before creating or serving request-level idempotency state. A subrequest whose items are not exactly that projection (an item dropped, added or reordered) is refused per item, in the body: every item is denied with DENIAL_REASON_SIGNATURE_INVALID, because the agent’s signature does not cover what arrived. It is not an invalid_argument error.

FieldTypeNumberDescription
payloadAgentRequestAcceptancePayload1The signed payload is carried because a projected subrequest does not carry offers addressed to other Exchanges and therefore cannot reconstruct the original complete set by itself.
signaturestring2Hex-encoded detached Ed25519 signature over the canonical payload bytes.
signature_algorithmstring3Signature algorithm; "EdDSA" for Ed25519.

The signed field set: ordered AgentRequestAcceptanceItem references plus the requester identity and idempotency key. requester_id and requester_domain are never empty, under the same rule as AgentAcceptance. Canonical bytes use the same RFC 8785 JCS over canonical proto-JSON rule as Offer and AgentAcceptance signatures.

FieldTypeNumberDescription
itemsrepeated AgentRequestAcceptanceItem1Complete original request order, before Broker fan-out. Capped at 256 — the same ceiling a discovery query's uris list carries, so one request can reference at most one offer per queried URI at the query cap. The Go verification helper enforces the same bound itself before doing any canonicalization work, because a verifier may run with wire validation off and the canonical rendering of an unbounded list is the expensive step an unauthenticated caller could otherwise buy for free.
requester_idstring2Requester.id of the request. Non-empty (see AgentRequestAcceptance).
requester_domainstring3Requester.domain of the request. Non-empty (see AgentRequestAcceptance).
idempotency_keystring4

A signed request-set reference containing offer_sig and exchange. The offer signature binds the complete Offer; the explicit issuing Exchange lets each fan-out recipient derive the exact subset it must receive without copying every full Offer into every subrequest.

FieldTypeNumberDescription
offer_sigstring1
exchangestring2

The canonical signing structure for AgentAcceptance. It is never sent on the wire — this message fixes the field set, and the byte layout is the canonical signing form defined on Offer.signature: RFC 8785 JCS over canonical proto-JSON with a pinned option set. Both halves are normative, so signer and verifier derive byte-identical signed bytes in any language without a protobuf binary codec, and the contract cannot drift between implementations. offer_sig is the accepted Offer.signature (which transitively binds pricing, terms, and expiry); requester_id, requester_domain, and idempotency_key come from the enclosing TransactionRequest, and the two requester fields are never empty.

FieldTypeNumberDescription
offer_sigstring1The accepted Offer's signature (Offer.signature). Anchors the whole signed offer without re-serializing its terms/pricing/expiry.
requester_idstring2Requester label (Requester.id) the acceptance is bound to. Never empty.
requester_domainstring3Requester domain (Requester.domain) the acceptance is bound to: the host of the key directory the acceptance verifies against. Never empty.
idempotency_keystring4The transaction's idempotency key — binds the acceptance to a single execute so it cannot be replayed under a different transaction. The key is scoped to the agent whatever the route, so an acceptance executes at most once at an Exchange, however it arrives (see "Idempotency").

Result for a single offer in a batch transaction. refusal is set only in a BrokerTransactionResponse, on an item whose Exchange refused the Broker’s whole sub-request or did not answer it. For a metered item, cost is the purchase charge: estimate × rate, or one unit’s rate without an estimate. The charge is final.

FieldTypeNumberDescription
offer_idstring1The offer_id this result is for.
transaction_idstring2Exchange-assigned transaction identifier.
billing_idstring3Billing record identifier minted by the Exchange's billing adapter for this transaction (not the account handle — see RegisterResponse.billing_ref).
resource_titleoptional string4Resource title echoed from the Offer.
costCost5Cost for this item. For a metered (PER_UNIT) item this is the purchase charge: the offer's estimated_quantity × rate, or one unit, 1 × rate, when the offer states no estimate (see Pricing).
subscription_idoptional string6If under subscription, no per-request charge.
subscription_unit_valueoptional Cost11Computed per-unit cost for financial attribution on subscription transactions. Even when cost.amount="0" (subscription), this field carries the value of the access for accounting purposes (e.g., ASC 606 prepaid drawdown).
denial_reasonoptional DenialReason7Set if this specific item was denied (others may succeed). Every per-item denial is answered here, in a successful response, also when the request carried only this item: a denied one-item purchase is never a non-OK error.
restriction_mismatchesrepeated RestrictionKind13DEPRECATED, never set. It named the restriction axes a request failed under DENIAL_REASON_RESTRICTION_NOT_SATISFIED, which is never sent: the Exchange never enforces a restriction (see Restriction). The field is retained because removing it would break the v1 wire contract; a receiver ignores it.
expires_atoptional Timestamp8When retrieval_endpoint expires.
retrieval_endpointoptional string12Signed retrieval URL for this item. Bound to the requesting agent's identity via the parent TransactionResponse.agent_identity_hash (shared across all batch items) — on a BrokerTransactionResponse, the agent_identity_hash of the ExchangeOutcome for this item's Exchange; expires at expires_at. Absent if this item was denied or its delivery_method is not signed-URL-based.
delivery_methodDeliveryMethod9How resource is delivered for this item.
reporting_obligationoptional ReportingObligation10Reporting requirements for this item.
refusaloptional UpstreamRefusal14Set only on a BrokerTransactionResponse, and only when the Exchange that owns this item refused the Broker's whole sub-request instead of answering it: a non-OK answer, or no answer at all. offer_id names the item; transaction_id, billing_id, cost, denial_reason, retrieval_endpoint and the other result fields stay unset. Every item the Broker sent in that sub-request carries the same refusal. Whether the item was purchased depends on the refusal's code. With the code "unavailable" or "deadline_exceeded" the outcome is unknown: the Exchange may have completed the purchase before the answer was lost, whether it gave no answer or answered that code, and retrying the agent's request with the same idempotency_key settles it, on any route: through the same Broker, another one, or directly (see UpstreamRefusal.code). With any other code the Exchange answered with a refusal that decided no item, so the item was not purchased. An Exchange never sets this field, and a Broker never replaces it with a denial_reason: an upstream refusal is not a per-item access decision.

A peer’s refusal of a call made on the caller’s behalf: the refusing party, the Connect code it answered with, and its ErrorDetail unchanged. It is the one shape for “a peer refused”. On a Broker purchase it rides on each affected item instead of becoming an error of the Broker’s own, and the party is the Exchange. An Identity Service MCP tool error carries it as {"refusal": UpstreamRefusal}.

FieldTypeNumberDescription
partystring1Bare host of the party that refused the call made on the caller's behalf, in the form "Request recipient" defines in the file header.
codestring2The Connect code the party answered with, in its wire form, e.g. "permission_denied" or "unauthenticated". When the party gave no answer — unreachable, or the call timed out — it is the code the relaying party's call failed with ("unavailable", "deadline_exceeded"). A refusal with either of those codes is an ambiguous outcome: a purchase may have completed. On a Broker purchase, retrying the agent's request with the same idempotency_key, on any route, is answered from each Exchange's stored result, so it settles the outcome without buying twice.
detailoptional ErrorDetail3The party's typed reason, unchanged — the ErrorDetail it attached to its refusal. Absent when the party attached none or gave no answer.

Actual transaction cost.

FieldTypeNumberDescription
amountstring1Exact decimal string (not a float), e.g. "19.99". Denominated in currency.
currencystring2ISO 4217
unit_costoptional string3Effective cost per unit (decimal string)
FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
idempotency_keystring2Idempotency key (REQUIRED). The server MUST dedupe on this so a replayed report does not double-count usage; the same key with a different report is refused with already_exists. Scoped to the agent; see "Idempotency" in the file header. The report's durable identity is the Exchange-assigned report_id in UsageReportResponse.
transaction_idstring3Transaction ID from the delivery.
billing_idstring4Billing record identifier from the delivery (TransactionResultItem.billing_id).
usageUsage5How the resource was actually used.
timestampTimestamp6When the resource was used (ISO 8601).
exchangestring8REQUIRED. Bare host of the recipient this report is addressed to (e.g. "exchange.example" or "exchange.example:8081") — the Exchange that issued the offer and therefore holds the reporting obligation. See "Request recipient" in the file header for the full contract. Promoted from optional: an absent or empty value used to skip the recipient check entirely, which made the check opt-in for the caller.
assetsrepeated UsageAsset9Assets that were delivered and used.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Requirements attached to a delivery.

FieldTypeNumberDescription
requiredbool1Whether post-usage reporting is required.
windowoptional Duration2Duration within which the report must be submitted (e.g. "86400s" = 24 hours; proto-JSON encodes Duration as seconds). A report that arrives after the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED (Connect code failed_precondition).
endpointoptional string3URL to submit the usage report to (if different from Exchange).
required_fieldsrepeated string4Field names that must be present in the report.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.
FieldTypeNumberDescription
functionrepeated string1How the resource was used. Standard values: "ai-train", "ai-input", "ai-index", "search", "display". Multiple allowed. CoMP-specific values available via fora-comp-v1 extension profile.
subfnrepeated string2Sub-function detail. Standard values: "training", "rag", "grounding", "agent_view", "agent_actions".
consumed_quantityint323REQUIRED. Actual quantity consumed, in the metering unit from the Offer's Pricing. For text: tokens consumed. For video: seconds watched. For data: records accessed. The agent reports what it consumed, not what it estimated: an Exchange MUST NOT refuse a report because this differs from the estimate, in either direction.
displayed_to_useroptional bool4Whether resource/output was displayed to a human.
citation_includedoptional bool5Whether citation was included as required by the offer terms.
attributionrepeated AttributionDetail6Structured attribution details for each citation provided.
consumed_unitoptional string8Metering unit for consumed_quantity. Must match the Offer's Pricing.unit. If omitted, defaults to "tokens". Same token format as Pricing.unit: a bare registered token or a vendor:namespaced token.

Structured attribution metadata for usage reporting.

FieldTypeNumberDescription
displayed_urloptional string1URL displayed to the user as the attribution link.
formatoptional CitationFormat2How the citation was presented.
visible_to_useroptional bool3Whether the attribution was visible to the end user.

A single asset included in the usage report.

FieldTypeNumberDescription
uristring1Asset URI
titleoptional string2Asset title
package_idoptional string3Package identifier

Acknowledgment of a usage report. A successful response means the report was accepted; a rejection travels as a non-OK transport error carrying ErrorDetail.usage_report_rejection. A report past its reporting window is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED and the code failed_precondition; a malformed report with USAGE_REPORT_REJECTION_REASON_MALFORMED and invalid_argument. The reported quantity is unrestricted: a consumed quantity that differs from the estimate, above or below, is never a reason to refuse a report.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
report_idstring3Exchange-assigned report identifier. Required for the dispute chain — the agent must reference this report_id in DisputeRequest to prove that a usage report was filed before disputing. The complete evidence chain: Offer → Transaction (transaction_id, billing_id) → UsageReport → UsageReportResponse (report_id) → DisputeRequest (transaction_id + report_id)
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Agent signals a content delivery problem for a completed transaction.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
idempotency_keystring2Idempotency key (REQUIRED). The server MUST dedupe on this so a replayed filing does not open a duplicate case; the same key with a different filing is refused with already_exists. Scoped to the agent; see "Idempotency" in the file header. The dispute's durable identity is the Exchange-assigned dispute_id in DisputeResponse.
transaction_idstring3Transaction being disputed.
billing_idstring4Billing record identifier from the disputed transaction (TransactionResultItem.billing_id).
reasonDisputeReason5Reason for the dispute.
descriptionoptional string6Human-readable description of the issue.
received_content_hashoptional string7Evidence: content hash of what was actually received. Exchange compares against the hash promised in ResourceIdentity.
received_hash_methodoptional string8Hash algorithm the agent used
report_idstring9Must reference a filed UsageReport. The agent MUST file a UsageReport (via ReportUsage RPC) and receive a report_id BEFORE filing a dispute. This prevents fire-and-forget disputes and ensures the Exchange has the complete evidence chain: what was offered, what was transacted, what the agent reported using, and what the agent disputes. The dispute chain: Transaction → UsageReport → Dispute.
exchangestring10REQUIRED. Bare host of the recipient this request is addressed to (e.g. "exchange.example" or "exchange.example:8081"). See "Request recipient" in the file header. The dispute's subject identifiers above cannot stand in for it: transaction_id, billing_id and report_id are opaque and Exchange-scoped, so verifying one means a database lookup, while the recipient check must run before any lookup happens.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Exchange acknowledges and processes the dispute. Every Exchange implements DisputeTransaction. A successful response means the dispute was accepted for processing; a refusal to file travels as a non-OK transport error carrying ErrorDetail.dispute_failure, whose DisputeFailureReason names the one check the filing failed.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
dispute_idoptional string2Exchange-assigned dispute case identifier.
estimated_resolutionoptional Duration4Expected resolution timeline.
statusDisputeStatus5Current lifecycle status of the dispute. Tracks progression through the three-tier resolution process: Tier 1 (automated, <1s): FILED → AUTO_RESOLVED or EVIDENCE_NEEDED Tier 2 (rule-based, <24h): UNDER_REVIEW → RESOLVED Tier 3 (pattern investigation, async): ESCALATED → SETTLED → FINAL Losing party may appeal: RESOLVED → APPEALED → back to UNDER_REVIEW.
resolutionoptional ResolutionType6Resolution outcome, populated when the dispute reaches a terminal state (RESOLVED, SETTLED, or FINAL). Absent while dispute is in progress (FILED, UNDER_REVIEW, ESCALATED, etc.).
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Request an ACME-style domain verification challenge.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
domainstring2The provider domain to verify (e.g., "techcrunch.com").
caller_idoptional string3Caller identity (registered with the Exchange).
exchangestring4REQUIRED. Bare host of the recipient this request is addressed to (e.g. "exchange.example" or "exchange.example:8081"). See "Request recipient" in the file header. Distinct from domain above, which is the provider domain being verified — the subject of the request, not its recipient.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Exchange returns a challenge token to be placed at the provider’s domain.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
tokenstring2Opaque challenge token. Provider must serve this at: https://{domain}/.well-known/fora-verify/{token}
expires_atTimestamp3When this challenge expires. Provider must confirm before this time.
verification_urlstring4The exact URL the Exchange will fetch to verify.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Confirm domain verification and register a signing key.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
domainstring2The domain being verified.
tokenstring3The challenge token (echoed from DomainVerificationChallenge).
signing_keyoptional string4Optional: the delivery endpoint's verification key, registered atomically with the domain on successful verification. PUBLIC key material only. The Exchange signs delivery URLs with a private key it holds and never publishes; a delivery endpoint verifies with the public half and holds nothing secret. Where the Exchange has to sign with a key the provider generated -- a CloudFront trusted key group is the provider's own AWS resource -- the private half is provisioned to the Exchange out of band and never travels in this field. Format follows cdn_type: a PEM-encoded RSA public key for "cloudfront", or the base64url-encoded raw Ed25519 public key (the JWK "x" value) for "edge-ed25519".
cdn_typeoptional string5Which delivery-URL verification scheme this key is for: "edge-ed25519" (a code-capable edge that verifies the Ed25519 URL signature itself) or "cloudfront" (AWS CloudFront trusted key groups, RSA, verified natively by the CDN). One value per Exchange-side tenant signing scheme: "edge-ed25519" is ED25519, "cloudfront" is AWS_CLOUDFRONT_RSA.
exchangestring6REQUIRED. Bare host of the recipient this request is addressed to (e.g. "exchange.example" or "exchange.example:8081"). See "Request recipient" in the file header. Distinct from domain above, which is the provider domain being verified — the subject of the request, not its recipient.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

A successful response means verification succeeded; a failure travels as a non-OK transport error carrying ErrorDetail.domain_verification_failure.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
key_idoptional string2If signing_key was provided: confirmation of key registration.
valid_untiloptional Timestamp4Verification is valid until this time. Provider must re-verify periodically.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Agent asks the Exchange to create its account. The caller’s identity is proven by the verified request signature, never asserted in the body; the business-registration payload rides in registration_data. Whether the Exchange inspects that payload follows its manifest: one publishing AccountRegistration.data_schema validates against that schema and refuses a non-conforming payload, one publishing none passes it through to its system of record uninspected. The payload is bounded either way, by four checks that run in a fixed order — top-level member count (at most 64), then nesting depth (at most 32 containers), then whether the payload has a canonical JSON form at all, then the size of that form (at most 16384 bytes of RFC 8785 canonical JSON) — because the cost of validating is the schema’s cost multiplied by the elements in the payload, and the unit has to be named for a field that arrives decoded rather than as bytes. The order is part of the contract: the two counts bound the document the third then has to walk, and the byte cap is defined as the length of the canonical encoding, so until that encoding exists there is no number to compare against. All four run before the schema does. Submitting a registration also states which terms the operator accepted — terms_digest echoes the value the manifest publishes, the request signature covers that echo, and the Exchange records the accepted digest with the account, so a later terms revision does not erase what was agreed.

Every gate above applies to account creation only. A repeat registration, from an agent that already holds an account, is answered from the stored record: the Exchange returns the existing billing_ref and changes nothing. It runs none of the registration_data checks, no terms gate and no schema check, because it discards registration_data rather than validating it — and because applying operator-controlled gates to a returning caller would break every one of them on the day the operator revises its terms. What a repeat still runs is everything outside those gates: the request signature is verified, exchange is checked against the recipient, and the field-level constraints still apply.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
registration_dataStruct2Business-registration data about the operator behind this agent — the details an Exchange needs to open a commercial account (legal entity, address, jurisdiction, tax identifiers and the like). The specific members are operator-defined and not fixed in the wire contract; whether the Exchange inspects them follows its manifest — see AccountRegistration.data_schema. This is NOT an identity claim: the caller's identity is taken from the verified request signature, never from this payload, so nothing here is trusted as authentication. Bounded, because a published data_schema is applied to THIS. The schema's own caps bound the schema; the cost of checking a payload against it is roughly that cost multiplied by the elements in the payload, and the multiplier was unbounded — a subschema under items is counted once by the schema's evaluation cap and evaluated once per element at runtime. At most 64 members at the top level. Top level rather than every level: nested bulk is already bounded by the byte cap below, and a recursive count would refuse a small document that merely nests, which a business entity legitimately does — an address is an object. At most 32 nested JSON containers, counting this member itself as the first — the same number and the same counting rule as the schema's own depth cap, because it is the same question asked of the other document. It is bounded for a reason the other two caps do not cover: a deeply nested payload is small and has few top-level members, and canonicalising it walks it RECURSIVELY, so without a stated bound the verdict was a property of the reader's runtime rather than of the payload — one implementation refused past roughly five hundred containers on one release of its language and accepted nine hundred on the next, while two others accepted every depth tried. Checked before the payload is canonicalised, so the bound precedes the walk it exists to bound. At most 16384 bytes, measured as this member's RFC 8785 (JCS) CANONICAL JSON encoding. The unit is named on purpose and is the load-bearing half of the rule: every other cap in this contract is over bytes a party actually served, and this member is never served as bytes — it is a Struct, decoded before any consumer sees it. "16KB" therefore means nothing until an encoding is chosen, and two implementations choosing privately is the same both-ends disagreement the schema rules exist to prevent. JCS also pins number formatting, which is not a detail: a payload carrying 1e300 is seven bytes under one renderer and three hundred under another. A payload with NO JSON REPRESENTATION at all is refused in the same class as the three bounds: it has no canonical encoding, so the byte cap above has nothing to measure. This member is a Struct, and a Struct can carry two such values — both of which the protobuf binary decoder accepts and proto-JSON refuses to render: a NON-FINITE NUMBER. JSON can represent neither NaN nor an infinity, while Struct's number_value is an IEEE-754 double that carries one perfectly well. a VALUE WITH NO KIND SET. google.protobuf.Value holds its payload in a oneof, and a oneof with no member set is well-formed on the wire. There is no JSON value it denotes, so there is nothing to write. That check MUST read the decoded protobuf value, never a native map converted from it. This is stated because two conformant implementations already answered the same signed request differently, and because NEITHER value survives the conversion. Some runtimes render a non-finite double as the STRING "NaN", "Infinity" or "-Infinity", and once that has happened the payload cannot be told apart from one that legitimately carries that text — an operator legally named NaN is a valid string value that has to be accepted. A value with no kind set converts to the same empty result as a JSON null, which is a value the payload may legitimately carry. In both cases the conversion destroys the information, so the check has to precede it. ORDER. The four checks on this member run in this sequence, and the sequence is not free choice: 1. top-level member count 2. nesting depth 3. canonicalizability, which is where both no-JSON-form checks live 4. canonical byte size The first two are counts, and they bound the document the third then has to walk. The third precedes the fourth because the byte cap is DEFINED as the length of the canonical encoding: until that encoding exists there is no number to compare against, and answering "too large" for a payload that has no encoding at all would state something untrue about it. All four are checked BEFORE the schema runs, for the reason the schema's own size cap is checked before the document is parsed — a check that exists to stop work has to precede the work. A payload breaking any of them is a malformed request, NOT REGISTRATION_FAILURE_REASON_INVALID_REGISTRATION_DATA: that reason names non-conformance to a published schema and applies only where one is published, while these four hold either way. The terms_digest gate sits between these four and the schema — see RegisterRequest.terms_digest for why that order is also fixed.
exchangestring3REQUIRED. Bare host of the recipient this request is addressed to (e.g. "exchange.example" or "exchange.example:8081"). See "Request recipient" in the file header. It matters most here: the caller reaches this endpoint by resolving a fetched, cached manifest, so the RFC 9421 signature covers only the URL that was dialled — this field is what lets the genuine Exchange refuse a registration that was meant for a different one.
terms_digestoptional string4Echo of WellKnownManifest.terms_digest, stating WHICH terms document the operator accepted. The request signature covers this statement, so it is the durable record a later dispute asks for; the Exchange stores the accepted value with the account. Four cases, all defined: the Exchange publishes a digest and this matches — registration proceeds; it publishes one and this differs — refused with REGISTRATION_FAILURE_REASON_TERMS_DIGEST_STALE; it publishes one and this is absent — refused with the SAME reason, because the caller's remedy is identical (read the manifest, echo, retry) and a second reason would split one fix in two; it publishes none and this is present — the Exchange MUST ignore the value and MUST NOT record it as an acceptance, since it publishes no digest and therefore cannot verify what document the value refers to, and storing it would put an unverifiable claim exactly where this field exists to hold a verified one. A registering client MUST read the digest from a FRESHLY fetched manifest rather than a cached copy — a cached endpoint is fine, a cached digest is not, because a client cannot detect staleness locally and a warm cache would otherwise make it retry a refused value until the cache expired. Registration happens once per Exchange, so the extra fetch is cheap. GATE ORDER. This gate runs AFTER the four registration_data checks (see RegisterRequest.registration_data) and BEFORE the published data_schema. Terms before schema is not arbitrary: the schema may itself have changed in the revision the caller has not read yet. Validating a stale-terms caller against the CURRENT schema hands back field errors describing a document it has never seen, so it fixes those members, re-fetches, and finds the requirements have moved. Terms first means a caller is always told to go read the current manifest before it is told anything about that manifest's contents. It also keeps one refusal to one remedy: TERMS_DIGEST_STALE says re-fetch and echo, INVALID_REGISTRATION_DATA says fix the payload, and a request that would earn both is given the one that has to be done first. The whole order applies only when an account is being CREATED: a repeat registration is answered from the stored record and runs no gate at all, so the four cases above are the four cases of a FIRST registration. See "Repeat registration" in the Agent Account Registration section header.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Exchange returns the minted billing_ref — the opaque, long-lived, per-Exchange account handle — and the account’s current active state. A repeat Register for the same agent returns the same billing_ref; a refused registration travels as a non-OK transport error carrying ErrorDetail.registration_failure.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
billing_refstring2Opaque, long-lived, per-Exchange account handle minted by the Exchange. Means nothing on its own and is never accepted as caller input. A repeat Register for the same agent returns the same value.
activebool3Whether the account is currently active. Accounts may start inactive and be activated out-of-band by the Exchange operator.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Read-only check of whether the calling agent’s account is active. Deliberately carries no identifying field — the agent always sends it itself, and the Exchange resolves the account from the verified request signature (see Accounts).

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
exchangestring2REQUIRED. Bare host of the recipient this request is addressed to (e.g. "exchange.example" or "exchange.example:8081"). See "Request recipient" in the file header. Accounts are per-Exchange, so "which Exchange am I asking about" is not derivable from anything else in this message.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

The account’s current state. An agent the Exchange knows, its request signature verified, but that holds no account at this Exchange is answered with the Connect code not_found instead, so an OK answer always carries billing_ref; a receiver reads an empty billing_ref from an Exchange built before that rule as the same answer. balances balances lists what the account can spend now, one entry per currency. An empty balances list means the Exchange does not report balances, not a zero balance.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
billing_refstring2The account handle minted at registration (see RegisterResponse.billing_ref). Set on every OK answer: a calling agent with no account is answered NOT_FOUND, not with an empty handle (see GetAccountStatus). A receiver reads an empty handle, which an Exchange built before that rule may send, as the same answer.
activebool3Whether the account is currently active.
terms_digestoptional string4The terms document this account ACCEPTED: the "method:hexdigest" value the agent echoed in RegisterRequest.terms_digest, which the Exchange recorded with the account. This is the read side of that record. Without it the protocol required an Exchange to store the acceptance and named the question the record exists to answer — "which terms did this operator accept" — while giving no way to ask it, so the only party who could check what it had agreed to was the party holding the database. An Exchange that holds a recorded digest for this account MUST return it here. Absence has exactly ONE meaning: no acceptance is recorded. Two situations produce it — the Exchange publishes no terms_digest, so nothing was ever accepted (and per RegisterRequest.terms_digest it MUST NOT record a presented value in that case), or the account was created before the operator began publishing one. It does NOT mean "this account has no terms", and an Exchange MUST NOT withhold a digest it holds: absence is already spoken for, so withholding would make the field state something untrue. The value is what was ACCEPTED, not what is published now. The two differ as soon as the operator revises its terms, and that difference is the point: comparing this against a freshly fetched WellKnownManifest.terms_digest is how an agent discovers that the terms moved under an account it already holds. A repeat Register will not tell it — a repeat is answered from the stored record and runs no gate at all.
balancesrepeated Cost5The amount the account can spend now, one entry per currency. Holds for pending transactions are already subtracted. Each amount is an exact decimal string and is never negative. The list holds at most one entry per currency, and its order has no meaning. unit_cost is never set. The value is a snapshot. It does not guarantee that the next execute passes the balance check: another transaction can spend the funds first. An empty list means the Exchange does not report balances. It does NOT mean a zero balance. The same holds for a currency missing from a non-empty list: that balance is not reported, it is not zero. An Exchange that reports a zero balance sends the entry with amount "0". Balances are only allowed when billing_ref is set.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Served at /.well-known/fora.json by every FORA participant (agent, exchange, broker, publisher). Single canonical document, role-tagged via role. Signing keys are not carried here — each participant publishes them in its WBA directory (the JWK Set at /.well-known/http-message-signatures-directory, modeled by WBAFile). Per-role fields are populated only when that role applies; consumers MUST ignore non-applicable fields based on role.

Two advertised addresses carry the same host binding: endpoint (ExchangeService) and catalog_endpoint (CatalogService) MUST each be on the host and port that serve the manifest, or a subdomain of that host on that port, and MUST NOT carry userinfo — because a signed call goes to that address and a manifest naming an unrelated host would redirect it to a party the signature never covered. An absent catalog_endpoint means the Exchange does not expose CatalogService; a consumer does not fall back to endpoint. See Endpoint host binding.

Who checks each is not the same today. The rule on endpoint is enforced below the caller: it runs inside the shared endpoint resolver in all three SDKs, before the value is returned and before it is cached, and it is corpus-locked. No SDK reads catalog_endpoint at all — a publisher configures the catalog address the way the agent client’s home Exchange is configured — so a deployment that does read it from a manifest MUST apply the binding itself before dialling. The MUST is on the consumer, not on a library it can assume.

FieldTypeNumberDescription
verstring1Version of THIS MANIFEST DOCUMENT's layout — "1.0", stamped by the party that serves the document from the SDK's WellKnownManifestVersion, never from ProtocolVersion. A namespace separate from the RPC envelope ver: a change to this document's layout bumps both numbers, a protocol change that leaves the document untouched bumps only the envelope's, so a reader that parses only manifests upgrades when the manifest changes and at no other time. Consumers read ver before any other member. They ACCEPT a recognised MAJOR version whatever the MINOR — a minor revision of the manifest is additive, and a reader ignores members it does not know. They REJECT an unrecognised MAJOR, a value that is not MAJOR.MINOR, and an ABSENT ver: the document sits at a fixed, unversioned path and is read before any signature is checked, so a layout the reader cannot classify must not supply anything a signed call then carries — the endpoint that call is sent to, or the terms_digest it echoes and its signature covers. It binds every consumer of this document rather than one use of it. In the SDK that is both manifest-reading faces, in all three languages — the endpoint resolver and the registration-requirements reader — each applying it at its own call site, pinned to one corpus. The key face is exempt because it reads a plain JWK Set, which carries no version of this document and never will.
roleRole2Role this manifest describes.
domainstring3Canonical domain serving this manifest.
contactoptional string4Contact email (licensing, integration, security).
exchangesrepeated AuthorizedExchange7Publisher-only. Authorized exchanges for this publisher's resources. Like ads.txt — declares who may sell. MUST be empty for non-publisher roles.
catalog_contributorsrepeated CatalogContributor8Publisher-only. Authorized third-party catalog contributors. MUST be empty for non-publisher roles.
nameoptional string9Exchange-only. Human-readable Exchange name.
operatoroptional string10Exchange-only. Organization operating this Exchange.
operator_domainoptional string11Exchange-only. Operator's corporate domain (may differ from domain).
endpointoptional string12Exchange-only. ExchangeService endpoint URL. MUST be on the same host AND PORT that serve this manifest, or on a subdomain of that host on that port, and MUST NOT carry userinfo. A consumer refuses an endpoint anywhere else: this document is only as trustworthy as the host that served it, so an endpoint naming an unrelated host would let whoever answers for the manifest redirect a signed call to a party the signature never covered, and another port is another service the publisher of the manifest need not control. The host match is on a full dot-delimited label boundary, so evil-a.com is not a subdomain of a.com. A port equal to the scheme's default and an omitted port are the SAME port, so https://x, https://x:443 and x all match. An Exchange reachable on a non-default port names that port on both sides.
health_endpointoptional string13Exchange-only. The URL a consumer, such as a Broker deciding whether to route to this Exchange, probes to judge whether it is serving: an absolute URL, or a path resolved against this manifest's own URL (RFC 3986). A 200 answer means serving. When absent, a consumer judges reachability by fetching this manifest itself; it never assumes a path the protocol does not name.
catalog_endpointoptional string14Exchange-only. CatalogService endpoint URL (if exposed). It carries the same binding as endpoint: it MUST be on the same host AND PORT that serve this manifest, or on a subdomain of that host on that port, and MUST NOT carry userinfo. A consumer refuses a catalog endpoint anywhere else — a publisher's push is a signed call, and a manifest naming an unrelated host would redirect it to a party the signature never covered. The host match is on a full dot-delimited label boundary, and a port equal to the scheme's default and an omitted port are the SAME port. Absent means this Exchange does not expose CatalogService; a consumer does not fall back to endpoint.
protocol_versions_supportedrepeated string16Exchange-only. Supported FORA protocol versions (e.g. ["1.0"]).
pricing_models_supportedrepeated PricingModel17Exchange-only. Supported pricing models.
delivery_methods_supportedrepeated DeliveryMethod18Exchange-only. Supported delivery methods.
hash_methods_supportedrepeated string19Exchange-only. Accepted resource hash methods for attestation verification.
accepted_verifiersrepeated string20Exchange-only. Trusted attestation verification vendors (domains).
terms_urioptional string21Exchange-only. Terms of service URL.
privacy_urioptional string22Exchange-only. Privacy policy URL.
supported_profilesrepeated string23Exchange-only. Domain extension profiles this Exchange conforms to. See standards-layering docs.
supported_auth_methodsrepeated AuthMethod24Exchange-only. Authorization methods this Exchange supports (ordered by preference).
oidc_issueroptional string25Exchange-only. OIDC Discovery URL when OAuth methods are supported.
gnap_grant_endpointoptional string26Exchange-only. GNAP grant endpoint when GNAP is supported.
base_currencyoptional string27Exchange-only. Base currency for pricing (ISO 4217). All unit_cost values from this Exchange are denominated in this currency.
max_intermediary_hopsoptional int3228Exchange-only. Maximum number of RFC 9421 HTTP Message Signatures this Exchange accepts on an inbound request. Every signature counts, whether or not it covers another. A request signed only by the party that sent it — an agent, or a Broker for a request it authored — carries one, so the cap bounds a request that a party forwarded unchanged and signed again. An Exchange refuses a request carrying more before it verifies any signature, with the Connect code resource_exhausted (HTTP 429) and no typed reason. Lets Exchanges publish their tolerance so a sender stays within it. Absent = no published limit (Exchange applies its own default policy).
account_registrationoptional AccountRegistration30Exchange-only. How to open an account here — see AccountRegistration, which owns the contract. Absent: registration_data is accepted uninspected, exactly as before this field existed.
terms_digestoptional string31Exchange-only. Digest of the document served at terms_uri, in "method:hexdigest" form (e.g. "sha256:9f86d081..."), pinning WHICH terms document this manifest is currently offering. terms_uri alone cannot answer that: it is a URL, and its content changes, so after the first revision every earlier registration points at a document that no longer says what was agreed. RegisterRequest.terms_digest echoes this value, the request signature covers that echo, and the Exchange records the accepted digest with the account — which is what makes "which terms did this operator accept" answerable later, through GetAccountStatusResponse.terms_digest, which is where the accepted value is read back. Because a digest identifies a document only while a copy of it still exists, keeping the historical terms documents retrievable is the Exchange's obligation. It sits at the top level rather than inside account_registration on purpose: an Exchange with pass-through registration publishes no block yet still needs to pin its terms version, and coupling "I enforce a schema" to "I version my terms" would tie together two independent decisions. Operator note: publishing this field for the first time refuses every client that does not yet echo it, so it is a coordinated change rather than a safe addition.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown values reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → ignore-unknown.

How to open an account at an Exchange, published as the optional account_registration block on the manifest. Omitting the whole block keeps the pass-through behaviour: registration_data is accepted uninspected. data_schema is the API mode — publishing it commits the Exchange to enforcing it on account creation, and an Exchange that publishes it MUST accept registration through the API. The gate runs on creation only: a repeat registration is answered from the stored record and runs no schema check at all. A future web mode (a URL to a registration page where a human completes steps an API call cannot carry) is an additional option an agent MAY offer its user instead, never a replacement for the API path. Terms versioning deliberately lives outside this block, on WellKnownManifest.terms_digest, so an Exchange with pass-through registration can still pin which terms document it serves.

data_schema carries a normative safety rule set, because a client reads it out of a third party’s manifest and validates against it before any signature has been checked: same-document references only and no reference cycles, one pinned dialect, a JSON object at the top level, a 16KB size cap, a 32-container depth cap, a 10,000-evaluation work cap, a restricted pattern alphabet with no nested quantifiers, and format left as an annotation. The bounds are numbers in the contract rather than each implementation’s choice, because a schema is validated at both ends of one registration and a limit invented by one side refuses payloads the other accepts. The work cap is the one that is easy to omit: the size and depth caps bound the document and say nothing about how expensive checking a payload against it is, and branches multiply along a reference chain. See Registration schema rules for the list and the reasoning.

FieldTypeNumberDescription
data_schemaStruct1JSON Schema (draft 2020-12) describing the RegisterRequest.registration_data object this Exchange expects. This field is the single home of the enforce/pass-through contract, and publishing it IS the enforcement switch. Present: this Exchange validates registration_data against the schema and refuses a non-conforming payload with REGISTRATION_FAILURE_REASON_INVALID_REGISTRATION_DATA, naming the offending members in RegistrationFailure.field_errors. Absent: registration_data is passed through to the system of record uninspected, so an Exchange that publishes no schema needs no change to stay conformant. The gate above runs on ACCOUNT CREATION ONLY. A repeat registration, for an agent that already holds an account, is answered from the stored record and runs no schema check at all — it discards registration_data rather than validating it. That exception is stated here rather than left to the section above, because this field calls itself the single home of the contract and a reader who comes here for the whole rule would otherwise leave with the wrong one. See "Repeat registration" in the Agent Account Registration section for why, and for the other gates it applies to. Absent means the field carries no bytes, or only JSON whitespace — space, tab, carriage return and line feed, RFC 8259's four and no others. Nothing else counts, and the distinction is load-bearing rather than pedantic: this is the enforcement switch, so a byte sequence read as absent is one that turns validation OFF. A consumer that asked its own language what "blank" means got three different answers to the same document — U+00A0 and U+3000 are whitespace to some runtimes and not others, and a decoder that strips a byte order mark makes a mark followed by a space look like nothing at all. A document that is not empty and not JSON is malformed, which is a refusal; it is never silence. Safety rules, because a consumer reads this schema out of a THIRD PARTY's manifest and validates against it before any signature has been checked. A publisher MUST satisfy every rule below and a consumer MUST refuse a schema that does not. The bounds are stated here as numbers rather than left to each implementation on purpose: a schema is validated at both ends of the same registration, and a limit chosen privately by one side refuses payloads the other accepts. Self-contained. Every $ref, $dynamicRef and $recursiveRef MUST be a same-document reference — it begins with "#". A consumer MUST NOT resolve a reference that leaves the document: doing so turns every reader into an SSRF vector aimed at a URL the schema's author chose. One dialect. $schema, wherever it appears in the document, MUST name https://json-schema.org/draft/2020-12/schema (an empty fragment on the end is the same value). A document declaring none is read as that dialect. An older draft is refused rather than validated under semantics its author did not intend. Data is not schema. const, enum, default and examples hold arbitrary JSON VALUES, and their contents are never read as keywords: a const whose value happens to carry a "$ref" or "$schema" member states a value a payload may equal, not a reference to resolve or a dialect to honour. Both rules above therefore stop at those four keywords, and so does the pattern alphabet. Their nesting still counts against the depth cap. Likewise the child keys of properties, patternProperties, $defs, definitions and dependentSchemas are NAMES rather than keywords, so a property called "$ref" is a property. Bounded size: 16KB, measured as the UTF-8 bytes of this member AS SERVED in fora.json. One encoding. The bytes MUST be well-formed UTF-8 (RFC 8259 requires it for interchange) and MUST NOT begin with a byte order mark. RFC 8259 forbids ADDING a mark and lets a parser ignore one, so both policies conform and the choice is made here rather than left to each implementation: parsers differ, and one that strips a mark validates a different document — and counts three bytes against the size cap that the schema does not contain. A consumer MUST NOT repair ill-formed bytes either; substituting U+FFFD silently enforces a schema nobody published. A mark is in any case only valid at the start of a JSON text, and this is a member inside one. Bounded depth: 32 nested JSON containers, counting the schema itself as the first. Bounded WORK: 10000 evaluations, counted statically over the SCHEMA — each anyOf/oneOf/allOf branch and prefixItems entry costs its own subschema, and a $ref costs its target. It is the cost of applying the schema at ONE location in a payload, not of a whole payload: a subschema under items is counted once here and evaluated once per element at runtime. The size and depth caps do not bound it and are not a substitute for it: branches multiply along a reference chain, so a 1.6KB schema five containers deep can cost tens of millions of evaluations and tens of seconds against a two-member payload. A definition nobody references costs nothing, so a document may carry a library of them. Bounded reference chains: 100 hops, counted as the longest path of $ref hops rather than as the number of references the document contains. This is a THIRD axis, and a flat chain of definitions shows why it has to be: each one referring to the next is three JSON containers deep however long it is, so the depth cap never sees it, and it costs one evaluation per link, so the work cap does not either. What it does reach is the recursion a validator performs while resolving the chain — a chain of a few hundred exhausted one implementation's stack outright, on a document every other rule had passed. A schema describing a business entity chains one or two references. No reference cycles. A $ref chain MUST NOT return to a schema already on it. The construct is legal JSON Schema and is how a recursive structure is written, but its evaluation cost has no static bound and it is what makes a validator recurse until it aborts. Registration data describes a business entity, which is not a recursive shape. A portable pattern alphabet, stated as what a pattern MAY contain rather than as what it may not. A group MUST open with "(" or "(?:" and nothing else; only the escapes $, (, ), *, +, ., /, ?, \D, \W, [, \, ], ^, \d, \f, \n, \r, \t, \v, \w, {, \

The pure WBA directory, served at /.well-known/http-message-signatures-directory (Content-Type: application/http-message-signatures-directory+json). Holds each participant’s inline signing keys and an optional emergency-revocation pointer. Keys are identified by their RFC 7638 thumbprint (the RFC 9421 keyid). Each key carries alg EdDSA, the JOSE name RFC 7517 defines, which is a known, deliberate deviation from WG-00 §5.5.1 (see Key Directory); it and the other two directory rules where FORA departs from WG-00, the signed response and each key’s not_before and not_after, are listed in Where FORA departs from WG-00.

FieldTypeNumberDescription
keysrepeated JsonWebKey1Signature-verification keys; ≥1 valid at serve time
revocation_urloptional string2Emergency key-revocation list URL (KeyRevocationList)

Inline RFC 7517 JWK. FORA v1.0 supports Ed25519 only. Time bounds are RFC3339 strings; the validity window is half-open [not_before, not_after). Keys carry no kid label — a key is identified by its RFC 7638 thumbprint (the RFC 9421 keyid).

FieldTypeNumberDescription
ktystring2Key type. FORA v1.0: MUST be "OKP".
crvstring3Curve. FORA v1.0: MUST be "Ed25519".
usestring4Intended key use. FORA v1.0: MUST be "sig".
algstring5Signing algorithm. FORA v1.0: MUST be "EdDSA".
xstring6base64url-encoded 32-byte Ed25519 public key.
not_beforestring7REQUIRED. RFC3339 timestamp. Key is invalid before this instant, and a key without it is never valid.
not_afterstring8REQUIRED. RFC3339 timestamp. Key is invalid at and after this instant (strict upper bound), and a key without it is never valid.

Body served at WBAFile.revocation_url. Snapshot semantics: revoked is the complete set of revoked key thumbprints at as_of; consumers replace their local revocation set on each successful poll.

FieldTypeNumberDescription
as_ofTimestamp1Server's response time (RFC3339, UTC). Consumers use this to detect clock skew.
revokedrepeated string2Complete list of revoked key thumbprints (RFC 7638, base64url-no-pad) at as_of.

Authorizes a third party to push catalog metadata on the provider’s behalf.

FieldTypeNumberDescription
domainstring1Canonical domain of the authorized contributor (e.g., "doubleverify.com").
relationshipstring2Relationship of this contributor to the provider. Examples: "verifier" (resource intelligence vendor that attests to resource properties), "exchange" (an Exchange that enriches catalog entries).

A Exchange authorized to sell this provider’s content.

FieldTypeNumberDescription
domainstring1Canonical domain of the Exchange, in the shape "Request recipient" defines in the file header.
endpointstring2FORA ExchangeService endpoint URL.
relationshipProviderRelationship3Relationship type (mirrors ads.txt DIRECT/RESELLER).
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Messages for the Agent-to-Broker path (Steps 1 and 6), and the combined answer of a purchase through the Broker. When an agent talks directly to an Exchange, it uses ResourceQuery/TransactionRequest instead. BrokerService.ExecuteTransaction takes the same TransactionRequest an Exchange does.

Agent sends to Broker (Step 1), carried by BrokerService.Resolve. Pure discovery: it returns offers, executes no transaction, and so carries no idempotency_key (retrying is naturally safe). Correlation rides on the X-Request-ID header, not the body.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
requesterRequester3Requester identity — who is making this request, the scopes it states. The Broker forwards this to Exchanges in ResourceQuery.requester, with Requester.domain the host of the origin that the covered Signature-Agent member of the agent's verified signature names. A Requester.domain that names another host is never forwarded, and the Broker answers the request all the same (see "Direct and relayed requests" in the file header). REQUIRED: a discovery names its requester, whose Requester.id and Requester.domain are required in turn.
urisrepeated string8Resource URIs the agent wants. The Broker forwards these to Exchanges in ResourceQuery.uris. Optional when query / search_filters drive Broker-side discovery instead.
acceptable_restrictionsrepeated AcceptableRestriction9The limits the agent will operate within, per restriction axis — see AcceptableRestriction. The Broker forwards these to Exchanges in ResourceQuery.acceptable_restrictions. Advisory selection inputs, not enforcement.
constraintsoptional RequestConstraints4Constraints for exchange filtering and offer selection.
supported_profilesrepeated string5Domain extension profiles the agent understands. The Broker MUST forward this list unchanged in ResourceQuery.supported_profiles on every query it sends an Exchange, and forward an absent list as absent. It does not route by it: which Exchanges it queries is decided on other grounds, and each Exchange answers for its own profiles (see ResourceQuery.supported_profiles). Examples: ["fora-academic-v1"] — agent working on literature review
queryoptional string6Search query for Broker-side resource discovery. Used when the agent doesn't know specific URIs but wants the Broker to find matching resources across Exchanges. When present, the Broker interprets the query and discovers resources across Exchanges on the agent's behalf. Results returned as Offers in DiscoveryResponse, same as for specific URI requests. Can be used alongside uris (specific URIs + search in one request).
search_filtersoptional Struct7Structured search filters (optional, alongside or instead of query). Keys are profile-specific: "academic.topic", "news.category", "legal.jurisdiction", etc. The Broker maps these to Exchange-specific query parameters.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

Budget and preference constraints for exchange filtering and offer selection. BrokerService.Resolve may apply the budget as a filter over offers (OFFER_ABSENCE_REASON_BUDGET_EXCEEDED when no offer for a URI remains); it neither denies nor charges. How the budget is computed is the Broker’s own behaviour.

FieldTypeNumberDescription
exchangesrepeated string1Authorized Exchange domains, in the shape "Request recipient" defines in the file header. Broker queries only these. This is a FILTER over third parties, not an address — the recipient of the request carrying it is a separate question.
max_priceoptional Cost2Maximum price the agent is willing to pay.
max_unit_costoptional string3Maximum effective cost per unit, as an exact decimal string (not a float).
delivery_preferencerepeated DeliveryMethod4Preferred delivery methods, in order of preference.
reporting_capableoptional bool5Whether the agent supports post-usage reporting.
preferred_exchangesrepeated string6Exchanges the agent has existing relationships with (subscriptions, contracts). The Broker SHOULD prefer these when resource is available — subscription resource has zero marginal cost.
budget_scopeoptional string7Budget scope identifier: whose budget this is. E.g. "user:u-12345" for per-user budgets, "team:eng" for per-team.
period_budgetoptional Cost8Per-period budget limit for the budget_scope.
budget_periodoptional Duration9Budget period (e.g. "2592000s" = 30 days; proto-JSON encodes Duration as seconds).
max_data_ageoptional Duration10Maximum acceptable age of resource data. The Broker SHOULD exclude offers where (now - Offer.data_as_of) exceeds this duration. Only relevant for DYNAMIC resources. Ignored for STATIC (content is immutable) and LIVE (content doesn't exist yet). Examples: 7 days — "credit report updated within the last week" 1 hour — "stock snapshot from the last hour" 30 days — "drug interaction database updated this month"
max_hopsoptional int3211Maximum number of hops the agent will allow its discovery to travel. It rides only on DiscoveryRequest, which terminates at the Broker, and a Broker originates its own queries rather than forwarding the agent's request, so no party receives or enforces it in this version: an Exchange never sees it, and an Exchange's own cap is WellKnownManifest.max_intermediary_hops. The field is kept for multi-hop Broker discovery, an open protocol question. Absent = no cap stated.

Broker returns to Agent (Step 6). Discovery-only: offer_groups (field 4, one OfferGroup per requested URI, each carrying the full signed Offer with Offer.exchange as the execute-routing target) or absence_reason (field 16) with empty offer_groups. There are no inline delivery fields — the per-transaction result rides on TransactionResponse, or on BrokerTransactionResponse through the Broker, via the separate execute path.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
offer_groupsrepeated OfferGroup4Offers grouped by requested URI — the sole offer representation in this response. One OfferGroup per URI the agent asked for (echoed in OfferGroup.uri); a group with no offers carries OfferGroup.absence_reason explaining why. Each contained Offer is the full signed Offer the Exchange issued (including Offer.exchange, the execute-routing target), forwarded by the Broker unchanged so the agent can verify the signature end to end.
absence_reasonoptional OfferAbsenceReason16Why the resolve produced no offers at all. Set (and offer_groups empty) on a successful "no result" answer; unset when offer_groups is non-empty. Same vocabulary DiscoverResources uses for OfferGroup.absence_reason. RESTRICTION_FILTERED may appear here, but Resolve does not surface the per-axis detail: DiscoveryResponse has no restriction_filters companion (unlike OfferGroup). A consumer needing the filtered axes calls DiscoverResources. Existence-oracle note: an authorization-flavored reason (NOT_AUTHORIZED, NOT_IN_CATALOG, CONTENT_BLOCKED) confirms a resource exists and why access was refused. Resolve surfaces the same oracle at the broker that OfferGroup.absence_reason does at the Exchange, so the same mitigation applies: where existence itself must stay hidden, the Broker MAY omit the reason (leave this unset) rather than reveal it. See the threat model. A scope shortfall never reaches this field: the Exchange answers it with no absence reason, and the Broker MUST NOT supply one (the existence-hiding rule, stated on Requester.scopes).
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

The Broker’s answer to BrokerService.ExecuteTransaction. items has one entry per request item, in request order: an Exchange’s TransactionResultItem unchanged, or an item carrying refusal. exchanges has one ExchangeOutcome per Exchange contacted. totals holds the charged amounts, one Cost per currency, never summed across currencies and never converted: currency conversion is out of scope for this version. No field is signed; a result item’s one signed value is its retrieval_endpoint, signed by the issuing Exchange.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
itemsrepeated TransactionResultItem2One per request item, in request order. A purchase that succeeded, or that its Exchange denied per item (denial_reason), is that Exchange's TransactionResultItem unchanged. An item whose Exchange refused the whole sub-request, or did not answer it, carries that refusal in refusal, with offer_id set and no other result field. When the Broker itself does not understand a key the agent's request lists in ext_critical, every item is the Broker's denial, DENIAL_REASON_UNKNOWN_CRITICAL_EXTENSION, and no Exchange was contacted.
exchangesrepeated ExchangeOutcome3One per Exchange the Broker contacted, in the order each Exchange first appears among the request items.
totalsrepeated Cost4Charged totals, one per currency, in the order each currency first appears among the charged items. An item is charged when it carries neither denial_reason nor refusal; its cost.amount is added, as an exact decimal, into the entry for its cost.currency. Amounts are never summed across currencies and never converted: currency conversion is out of scope for this version. unit_cost is unset. Empty when no item was charged. An item whose refusal carries the code "unavailable" or "deadline_exceeded" may have been charged but is not counted, so totals is then a lower bound of what the purchase charged.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

The request-level part of one Exchange’s answer to its sub-request: the offers sent to it, its agent_identity_hash (what each of its retrieval URLs is bound to) and its subscription_quota.

FieldTypeNumberDescription
exchangestring1Bare host of the Exchange, in the form "Request recipient" defines in the file header: the offer.exchange its items share.
offer_idsrepeated string2The offer_id of every item sent to this Exchange, in request order.
agent_identity_hashstring3That Exchange's TransactionResponse.agent_identity_hash: the identity the retrieval_endpoint of each of its items is bound to. Empty when the Exchange refused the sub-request or did not answer.
subscription_quotarepeated SubscriptionQuotaInfo4That Exchange's TransactionResponse.subscription_quota, unchanged.

Messages for the optional CatalogService RPC — the publisher role’s surface: push, remove and refresh the catalog entries a publisher, or a contributor it authorised, supplies to an Exchange.

Every pushed entry is checked in two tiers, and both are stated so a publisher can run them before sending. The wire tier is protovalidate: the ResourceEntry envelope rules below and the LicenseTerm rules, applied to the request exactly as received. The ingest tier runs over the canonicalised terms: restriction tokens are trimmed of RFC 8259 whitespace, ASCII-case-folded (lower for RESTRICTION_KIND_FUNCTION and RESTRICTION_KIND_USER_TYPE, upper for RESTRICTION_KIND_GEOGRAPHY; a non-ASCII byte is never folded) and alias-resolved to their registered token; then a bare (non-namespaced) Pricing.unit or Quota.metric that is not a registered token is rejected, as is a restriction whose permitted and prohibited lists name one token once folded (restriction.canonical_disjoint), while an unregistered restriction token and an OBLIGATION_KIND_OTHER obligation without detail are accepted and reported in PushResourcesResponse.warnings. Disjointness is the one property both tiers assert, over different values, so a term the boundary clears can still be refused at ingest. The SDK ships both tiers (ValidateResourceEntry and its Python/TypeScript twins) and a catalog client (NewCatalogClient / createCatalogClient / CatalogClient) in all three languages; the Exchange’s own run of the checks is the deciding one.

Push or update content entries in the Exchange catalog. entries carries at least one entry and at most 256 — an empty push is refused rather than answered with zero counts, and a larger feed is pushed in several submissions. Both bounds are wire rules: a push outside them is a malformed request, refused invalid_argument by wire validation with no catalog_rejection. The cap bounds one submission, not the work of checking it: that is bounded by the maximum request size the recipient will read. exchange is the bare domain of the Exchange the push is meant for (the recipient-addressing rule), and the caller is whoever signs the request: caller_id is deprecated and not needed, because each entry’s domain names whose resource it is and the Exchange checks the signer against it.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
tenant_idstring2Tenant identifier
entriesrepeated ResourceEntry3Content entries to push. At least one: an empty push asks for nothing and is refused rather than answered with zero counts. At most 256, the bound a caller-chosen batch carries elsewhere in this contract (see ResourceQuery.uris) — it bounds one submission, so a larger feed is pushed in several. The cap is over entries because a submission is stored or refused whole, and a refusal names each entry that failed; it does not bound the work of checking a submission, which the recipient bounds at the transport. Both bounds are wire rules (min_items and max_items below). A push outside them is a malformed request, refused by wire validation with the Connect code invalid_argument like any other malformed request; it carries no catalog_rejection, because no entry was examined.
caller_idstring4DEPRECATED, never needed. Each entry's domain names whose resource it is, and the caller is the party that signs the request: the Exchange checks that the verified signer may push for each entry's domain (see CATALOG_REJECTION_REASON_NOT_CATALOG_CONTRIBUTOR), and a receiver does not rely on this field. The field is retained because removing it would break the v1 wire contract. It does not yet carry the deprecated field option, so implementations that still read it keep building; the option follows once they no longer do.
exchangestring5REQUIRED. Bare host of the recipient this request is addressed to (e.g. "exchange.example" or "exchange.example:8081"). See "Request recipient" in the file header. Distinct from tenant_id above, which names a publisher tenant WITHIN an Exchange, not the Exchange itself.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

A single resource catalog entry. The envelope carries its own wire rules, so an entry that cannot become a catalog URI is refused at the boundary rather than after ingestion. The list caps below bound how many — how many terms one entry may carry, how many entries a submission can store — never how many bytes, and not the work of checking one: a validator walks every element it is handed before any cardinality rule is reported, so that cost is bounded at the transport instead. Several members inside an entry carry no length rule, so a conformant push has no size ceiling and the transport cap can refuse one.

  • domain is a bare host in the recipient-addressing shape (a port is allowed; a scheme, path, query or userinfo is not; at most 260 characters). With path it forms the catalog URI by concatenation, so anything but a host would choose the URI rather than name the host.
  • path is an absolute URL path: it starts with /, carries no ? or #, no whitespace and no control character, and is 1–2048 characters.
  • title (512), content_id and content_hash (255), hash_method (64) and provenance_source (260) are length-bounded, in characters. content_hash is deliberately not format-checked — a bare hex digest and a method:hexdigest form both travel — because hash_method names the algorithm.
  • word_count and estimated_quantity are non-negative.
  • Every length above is characters — Unicode code points, not bytes, which is what protovalidate’s max_len counts. path’s pattern admits non-ASCII and the unpatterned fields admit anything, so a conformant value can exceed its character count in bytes several times over.
  • attestations carries at most 64 entries and terms at most 32, stated on the wire so every implementation refuses the same size. Because the terms cap is a wire rule, an over-cap entry is refused at the boundary before any per-entry classification runs — which is why CATALOG_REJECTION_REASON_TERMS_LIMIT_EXCEEDED can no longer be produced for a push.
  • resource_mutability, when present, must not be RESOURCE_MUTABILITY_UNSPECIFIED; omitted, the Exchange applies RESOURCE_MUTABILITY_STATIC at Offer build.
FieldTypeNumberDescription
domainstring1Provider domain — the bare host the resource lives on, in the shape "Request recipient" defines in the file header: a port is allowed, a scheme, path, query or userinfo is not. With path it forms the catalog URI by concatenation, so a value carrying anything but a host would choose the URI rather than merely name the host.
pathstring2Content path — an absolute URL path such as "/premium/article-42.html": starts with "/", carries no query or fragment delimiter, no whitespace and no control character, and is at most 2048 characters. Characters, not bytes: protovalidate's max_len counts Unicode code points, and the pattern admits non-ASCII, so a conformant path can exceed 2048 bytes.
content_idoptional string3Content identifier
titleoptional string4Content title
word_countoptional int325Word count
estimated_quantityoptional int326Estimated quantity in the metering unit
content_hashoptional string7Content hash, carried as the publisher computed it — a bare hex digest or a "method:hexdigest" form; bounded in length, never format-checked, because hash_method names the algorithm.
hash_methodoptional string8Hash algorithm
sourceoptional IngestionSource9How the entry was discovered
provenance_sourceoptional string10Who provided this resource metadata. Creates audit trail for "where did this catalog entry come from?"
provenance_timestampoptional Timestamp11When this metadata was collected/generated.
attestationsrepeated ResourceAttestation12Signed attestations about this resource entry. Same semantics as Offer.attestations — see ResourceAttestation message for verification levels and claim vocabulary. Attestations pushed via CatalogService are verified at push time: the Exchange checks that the attestation verifier is authorized to push for this provider (via catalog_contributors in the provider's WellKnownManifest) and validates the attestation signature against the verifier's public key from its WBA directory (the JWK Set at /.well-known/http-message-signatures-directory; the keyid is the key's RFC 7638 thumbprint). The verifier's fora.json carries only its role, determined by the verifier's operator.
termsrepeated LicenseTerm13Publisher-declared licensing terms for this resource. See LicenseTerm for the full model. Every term carries its Pricing (the resource_entry.terms.pricing_required rule above), whatever its semantics. For REFERENCE_ONLY terms, License.uri is authoritative. The Exchange validates ENUMERATED terms at push time and surfaces each term on its own offer on discovery: the term's price becomes Offer.pricing, and the term in Offer.terms carries no pricing. At most 32 terms per entry, stated on the wire so every implementation refuses the same size. An over-cap entry refuses the whole submission, as every catalog rejection does; what being a wire rule changes is WHEN — the refusal now happens at the boundary, before any per-entry classification runs, which is why the rejection reason that named this cap can no longer be produced for a push.
resource_mutabilityoptional ResourceMutability14Optional mutability hint. When omitted, the Exchange applies the STATIC default at Offer build; an explicit UNSPECIFIED is rejected. A value in ext is not read — the typed field is authoritative, so an ext-only value is treated as omitted. Mirrors the required Offer-side ResourceIdentity.resource_mutability.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052): keys within ext that the AGENT must understand. The Exchange carries them onto Offer.ext_critical and does not consume them, so an unknown key here never refuses the push (see "Critical extensions" in the file header). Empty (default) → all ext keys are safe to ignore.

accepted tallies the entries a successful push stored — and because a push is all-or-nothing, a success stores every entry it carried. A push the Exchange processed and could not apply travels as a non-OK transport error carrying ErrorDetail.catalog_rejection. A push refused before processing follows the general rules instead: a mis-addressed or malformed push is invalid_argument with no typed reason, and one whose signature fails is unauthenticated with request_auth_failure.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
acceptedint322Number of entries accepted. A push is all-or-nothing, so a successful push stored every entry it carried and this is the submission's own size. A push the Exchange processed and could not apply is not a response at all: it travels as a non-OK transport error carrying ErrorDetail.catalog_rejection. A push refused before processing is answered by the general rules instead: a mis-addressed push (see "Request recipient" in the file header) and a malformed one are invalid_argument with no typed reason, and one whose signature fails is unauthenticated with ErrorDetail.request_auth_failure.
rejectedint323Number of entries rejected. Structurally always 0 on this path, and kept for the same reason CATALOG_REJECTION_REASON_TERMS_LIMIT_EXCEEDED is kept: a rejection returns an error rather than a response, so there is no successful answer in which this can be non-zero. It remains meaningful only for a deployment that applies catalog rules somewhere the all-or-nothing rule above does not front. Do NOT read a zero here as "nothing failed" — read accepted.
warningsrepeated string4Non-fatal issues encountered during ingestion — the ingest tier's lint, which accepts the term and flags it. Examples: an unregistered bare restriction token on any axis, and an OBLIGATION_KIND_OTHER obligation with no detail. Warnings do not cause rejection; they are surfaced so publishers can fix their feeds without a hard failure. A condition that rejects is not a warning — a REFERENCE_ONLY term with no License.uri, for instance, is refused by license_term.reference_only.requires_uri and never reaches this list.
extStruct15Extension point
ext_criticalrepeated string90Critical extension keys (COSE crit pattern, RFC 9052). Lists keys within ext that the consumer MUST understand. Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION. Empty (default) → all ext keys are safe to ignore.

resources names at least one resource and at most 256 — the same batch bound PushResourcesRequest.entries carries — each by the domain and path a push names it by (ResourceRef). For each one the Exchange resolves /.well-known/fora.json from its domain and checks that the request’s signer is authorized to push for that domain, exactly as for a push; the caller is whoever signs. paths is deprecated and ignored: a bare path names no domain, and a request carrying only paths has no resources and is refused as malformed.

FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
tenant_idstring2Tenant identifier
pathsrepeated string3DEPRECATED, ignored. A bare path names no domain, so the Exchange cannot tell whose resource it is or whether the signer may remove it. Name each resource in resources instead; a request that carries only paths has no resources and is refused as malformed. The field and its item rules are retained because removing them would break the v1 wire contract.
exchangestring4REQUIRED. Bare host of the recipient this request is addressed to (e.g. "exchange.example" or "exchange.example:8081"). See "Request recipient" in the file header. Distinct from tenant_id above, which names a publisher tenant WITHIN an Exchange, not the Exchange itself.
resourcesrepeated ResourceRef5The resources to remove, each named by the domain and path a push names it by — at least one and at most 256, the same batch bound PushResourcesRequest.entries carries and for the same reason.

One catalog resource, named the way a push names it: ResourceEntry.domain and ResourceEntry.path, under the same rules.

FieldTypeNumberDescription
domainstring1The bare host the resource lives on, as ResourceEntry.domain.
pathstring2The resource's absolute path, as ResourceEntry.path.
FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
removedint322Number of entries removed
FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
tenant_idstring2Tenant identifier
exchangestring3REQUIRED. Bare host of the recipient this request is addressed to (e.g. "exchange.example" or "exchange.example:8081"). See "Request recipient" in the file header. Distinct from tenant_id above, which names a publisher tenant WITHIN an Exchange, not the Exchange itself.
FieldTypeNumberDescription
verstring1FORA protocol version — "1.0". Stamped by the sender from a single constant; advisory on receive. See "Protocol version" in the file header.
startedbool2Whether the refresh was started

A resource carries zero or more LicenseTerm entries — each term is a complete commercial arrangement. Multiple terms are the normal case: a news article may be free for academic use and paid for commercial use; a stock photo may be perpetually licensed with an impressions cap.

See /protocol/licensing-terms for a full conceptual walkthrough with examples.

One complete access arrangement for a resource. Lives at both ingestion (ResourceEntry.terms, many per resource) and emission (Offer.terms, exactly one per offer).

Validation rules (wire-enforced via protovalidate unless noted). A parenthetical code is the cross-field CEL rule id: that enforces the rule (e.g. license_term.reference_only.requires_uri); single-field rules use protovalidate’s standard constraints (enum.not_in, string.pattern, …).

  • pricing MUST be present on every catalog term (ResourceEntry.terms), any semantics — absent Pricing is a validation error (resource_entry.terms.pricing_required). model = FREE must be stated explicitly — absent Pricing is not free.
  • pricing MUST be unset on the term an offer carries (Offer.terms), because the offer’s price is Offer.pricing, stated once (offer.terms.pricing_unset). Both rules belong to the message that holds the term, so a LicenseTerm on its own is valid with or without pricing.
  • semantics MUST be set — TERM_SEMANTICS_UNSPECIFIED is rejected (the field’s enum.not_in:[0] rule).
  • REFERENCE_ONLY requires license.uri to be non-empty (license_term.reference_only.requires_uri); a License with a uri requires a uri_digest (license.digest_required_with_uri).
  • At most one Restriction per kind (license_term.one_restriction_per_kind); a token cannot be both permitted and prohibited (restriction.permitted_prohibited_disjoint). That rule compares the tokens as written, because it runs over the request as received — so two accepted spellings of one token (an alias beside its registered form, or either in another ASCII case) clear it and collide once folded. The ingest tier asserts the same property over the canonicalised tokens as restriction.canonical_disjoint; both refuse the term, and a term that fails both is reported by both. Both are scoped to one Restriction: what makes one restriction the whole of an axis is license_term.one_restriction_per_kind, so the per-axis reading is held by the two rules together.
  • quotas and obligations each carry at most 64 items, the bound every per-message list in the contract carries when no rule walks it more than once. They bound what one term can carry, not the cost of checking it. restrictions carries at most 8, and like the other two this bounds the document, not the cost of checking it. Only one restriction per axis is valid and Restriction.kind is defined-only, so four is the longest conformant list and eight leaves room for an axis this version does not have. The tighter bound is deliberate for a second reason: this is the one list a message rule walks against itself, so the cap is also the threshold of the size test the one-per-kind rule carries, and a conformance guard holds the two equal. The disjointness rule on each element is quadratic only in that element’s two token lists, both capped at 64, so its cost is bounded per restriction and linear across the list.
  • Unknown tokens in restrictions[].permitted / prohibited produce PushResourcesResponse.warnings[] but do NOT cause hard rejection (ingest-time, not CEL). Tokens are canonicalised first — RFC 8259 whitespace trimmed, ASCII case folded, and the aliases authored beside the tokens resolved (train-ai → ai-train, tdm → text-and-data-mining, personal → individual, …) — so Generative-AI is the registered ai-input, not an unknown token.
  • A bare (non-namespaced) pricing.unit or quotas[].metric that is not a registered token IS a hard rejection at ingest (registry membership, not CEL: a rule that re-listed the vocabulary would drift from it). A vendor:token value bypasses membership on every axis.
  • The same checks ship in the SDK as a publisher pre-check (ValidateLicenseTerm / ValidateResourceEntry and their Python and TypeScript twins); the Exchange’s own run is the deciding one.
FieldTypeNumberDescription
licenseoptional License1Governing license document. Authoritative for REFERENCE_ONLY terms, which MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that references nothing is rejected at ingest.
semanticsTermSemantics2How to interpret the machine fields.
restrictionsrepeated Restriction3Usage restrictions (function, geography, user-type). Multiple restrictions are AND-combined — the agent must satisfy all of them. At most 8, and this list is the one of the three that does NOT carry the contract's usual 64: only one restriction per axis is valid, the axis enum is defined-only, so four is the longest conformant list and eight leaves room for an axis this version does not have. Like the caps on quotas and obligations, this one bounds the DOCUMENT — how many restrictions one term may carry — and not the work of checking it: a validator walks every element it is handed before any cardinality rule is reported, so an over-cap list is traversed in full on its way to being refused. What makes this list different is that one rule walks it against ITSELF. The one-per-kind rule below is quadratic, so it carries its own size test and stays silent above this cap; a conformance guard holds the two numbers equal, because a cap raised without the test would leave the lists in between unchecked for duplicate axes and accepted. The neighbouring disjointness rule on each element is quadratic only in that element's two token lists, both capped at 64, so its cost is bounded per restriction and linear across the list — it needs no such test.
quotasrepeated Quota4Usage caps. The agent must not exceed any individual Quota. At most 64, the bound every per-message list in this contract carries when no rule walks it more than once. It bounds what one term may carry, not the work of checking one — a validator walks every element it is handed before the cap is reported, so the cost of checking is bounded at the transport.
obligationsrepeated Obligation5Post-use behavioral requirements. At most 64, for the reason quotas carries.
pricingoptional Pricing6Pricing for this term. Where it is required depends on the message that holds the term, so the requirement is a rule of that message rather than of this field: - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of semantics — an agent cannot act on a priceless term, so absent Pricing is a validation error at ingest (resource_entry.terms.pricing_required). model = FREE must be stated explicitly (absent Pricing is not free). A REFERENCE_ONLY term states its price here too; its License governs the human-readable terms but does not replace the machine-readable price. - On the term an offer carries (Offer.terms) it MUST be unset (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated once.
scopesrepeated string7Scope-gating: the Exchange presents an offer for this term only when the scopes in the requester's signed request (Requester.scopes) cover ALL of these scopes (AND-semantics). Empty = public. A subscription term is Pricing{model:FREE} + scopes:["subscription:..."]. Requester.scopes is the requester's own statement, which the Exchange does not verify, so a scope gate hides a term from requesters that do not state the scope; it does not stop one that states it falsely. An Exchange MAY instead gate a term behind a verified delegation (a subscription term is the usual case): then only the delegation chain's verified effective scopes are matched against these scopes, and Requester.scopes is ignored for that term. To a requester that presents no valid delegation, it answers as if the term did not exist, as existence hiding requires (see Requester.scopes). An offer the Exchange presented is honoured until it expires, and a gated offer expires no later than the chain's effective expiry (see Delegation). Coverage uses the SAME matching rule as Requester.scopes: segment-wise (":" separated), each granted segment must equal the corresponding required segment or be "", a terminal "" matches all remaining segments (at least one), and there is NO implicit prefix match (a grant narrower than the requirement does not cover it). "dist:*" covers "dist:US" and "dist:US:CA"; "dist" covers only "dist". There is exactly one scope-matching algorithm across the protocol.
part_labeloptional string8Informational human-readable name for this sub-part (sub-part terms).

Identifies the governing license document for a LicenseTerm.

FieldTypeNumberDescription
urioptional string1Canonical identity of the license document (RFC 3986). MUST NOT be URL-validated — data-labels TDL identifiers use non-URL schemes. For REFERENCE_ONLY terms this is the authoritative specification. Examples: "https://creativecommons.org/licenses/by/4.0/" "https://techcrunch.com/licensing/ai-terms-2026" "MUST NOT URL-validate" means do not REJECT non-URL schemes — it does NOT mean fetch blindly. A consumer that dereferences this URI MUST apply the SSRF countermeasures in the security threat model (T-LIC-1): scheme allowlist, block loopback/private/metadata addresses (resolve-then-check), fetch via an egress proxy, and treat the response as untrusted content. Verify the fetched bytes against uri_digest before use.
idoptional string2Stable short identifier: SPDX short-id ("GPL-3.0-only"), TollBit cuid, or catalog doc-id. Used by agents and the vocab linter for known-license lookup; SHARE_ALIKE derivatives default their scope_license to this.
nameoptional string3Human-readable name (licenseType, schema.org node name).
immutableoptional bool4Data-labels TDL: the document at uri is versioned and will not change.
uri_digestoptional string5Cryptographic digest of the document at uri, in "method:hexdigest" form (e.g. "sha256:9f86d081..."). Pins the referenced document so a consumer can verify the bytes it fetches match what was offered; covered by the offer signature, so it is tamper-evident end to end. REQUIRED whenever uri is non-empty — any semantics, mutable or not: without a pinned digest a MitM (or the publisher) can swap the document the agent reads. The Exchange pins it at ingestion (computing it over the safely-fetched document, or accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a non-URL TDL scheme). The method MUST be a collision-resistant hash — sha256, sha384, or sha512. Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat the swap-protection this field exists for. The CEL is STRUCTURE ONLY (allowlisted prefix + matching hex length); presence (digest-when-uri) is enforced at ingest.

A single constraint on one licensing dimension: function (what), geography (where), or user-type (who).

Reading a restriction: a value is in-scope when it matches at least one permitted[] token AND none of the prohibited[] tokens. Empty permitted[] = any value permitted on this axis. Restrictions ride on the offer, which carries exactly one term; the agent chooses among the offers a resource yields, one per entitled term (the Exchange does not pre-filter terms against requester attributes).

Vocabulary sources: proto-native — (fora.v1.vocab_enum) on the RESTRICTION_KIND_FUNCTION / RESTRICTION_KIND_GEOGRAPHY / RESTRICTION_KIND_USER_TYPE enum values (no side-car JSON registry).

FieldTypeNumberDescription
kindRestrictionKind1Which dimension this restriction applies to. Defined-only: the axis set is CLOSED, and a number outside it is refused rather than ignored. A custom axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited, so a new number was never the extension mechanism — accepting one would admit a restriction no consumer can evaluate onto a term whose default is BINDING (see advisory below), which fails open on the axis a publisher most needs honoured. Closing the axis does NOT bound the cost of the one-per-kind rule below, and must not be read as doing so: a number this rule refuses is still distinct from every other, so that rule's all() finds no duplicate to stop on and walks the list in full anyway. Its cost is bounded by the size test the rule itself carries.
permittedrepeated string2Tokens allowed on this axis. Empty = all permitted. For FUNCTION: "ai-input", "ai-train", "search", "editorial", "commercial", … For GEOGRAPHY: "US", "DE", "EU", "EEA", "*", … For USER_TYPE: "individual", "academic", "commercial_entity", …
prohibitedrepeated string3Tokens blocked on this axis. Takes precedence over permitted[].
advisorybool4Fail-closed by default. When false (the default), this restriction is BINDING: an agent that cannot evaluate every token in it — including an unknown vendor token — MUST decline the term. Set advisory = true to downgrade an unverifiable restriction to non-blocking. This deliberately inverts the COSE-crit opt-in default: a license restriction a consumer does not understand should stop it, not be silently ignored.

A usage cap that gates whether a LicenseTerm remains valid. Quotas limit consumption before a term expires or must be renegotiated — they are NOT billing quantities.

Metric vocabulary: proto-native — (fora.v1.vocab) on Quota.metric. The registered metrics, rendered from the proto at build time:

display-words impressions tokens input-tokens units-manufactured accesses copies seats

FieldTypeNumberDescription
metricstring1The unit being capped — an open vocabulary axis. The (fora.v1.vocab) entries below are the SOLE authored source of the registered bare metric tokens. A buf plugin reads them structurally and emits the quotametrics constants + IsRegistered; ingest enforces membership from those. The CEL is STRUCTURE ONLY (non-empty bare token or vendor:namespaced) — it never lists the tokens, so it cannot drift. Token meanings: display-words Words of content text rendered to an end user. impressions Times the content is displayed to an end user. tokens LLM output tokens generated using this content. input-tokens LLM input tokens consumed from this content. units-manufactured Physical units manufactured from this design/pattern. accesses Distinct content access / retrieval events. copies Digital or physical copies produced. seats Distinct named users licensed to access the content.
limitint642Maximum allowed value in the given window. A quota of 0 grants nothing — express "no access" by omitting the term, not a zero quota.
windowQuotaWindow3Time window over which the limit accumulates.

A post-use behavioral requirement attached to a LicenseTerm. Attribution and contribution are behavioral requirements here, not pricing models.

FieldTypeNumberDescription
kindObligationKind1What the agent must do.
triggerObligationTrigger2When the obligation activates.
scope_licenseoptional License3The license that derivatives must be released under. REQUIRED for SHARE_ALIKE (rejected if absent), where it MUST identify a license — set id (SPDX short-id, the common copyleft case, often the term's own License.id) and/or uri. Because it is a License, a referenced uri inherits the uri_digest swap-protection rule: a uri without a digest is rejected, exactly as for any other license reference.
detailoptional string4Free-form detail: attribution string, notice file URI, etc. OBLIGATION_KIND_OTHER without it → lint warning.

One way to communicate failure across every RPC. The transport carries a coarse canonical code (gRPC / Connect Code) as the error class; an ErrorDetail message — attached to the non-OK transport error’s details (the same mechanism protovalidate uses to attach its Violations) — carries the precise, machine-readable reason and structured context. Clients branch on the typed reason, never on a human string.

Error vs. body. A query that ran successfully returns its answer in the response body, and “no results” / per-item absence (OfferGroup.absence_reason) is a success, not an error. Only a method that could not perform the requested action returns a non-OK code plus an ErrorDetail. A per-item purchase denial is part of a successful answer, so it stays in the body on TransactionResultItem.denial_reason; transaction_denial carries only a refusal of the whole request. The per-domain reason enums below are the single source of truth that replaces the former in-body failure fields (rejection_reason, accepted, verified, failure_reason).

The structured detail attached to every non-OK transport error.

FieldTypeNumberDescription
messagestring1Developer-facing, NON-authoritative human message. Clients MUST branch on the typed reason below, never on this text. Servers SHOULD NOT place secrets, PII, or existence/authorization detail here that the closed typed reason deliberately withholds: unlike the enum, this free text is unbounded and easily becomes an existence oracle or leak channel (see metadata).
domainstring2Stable grouping for the failing surface, e.g. "fora.v1.ExchangeService". Mirrors google.rpc.ErrorInfo.domain so generic tooling can group errors.
metadatamap<string, string>3Dynamic key/value context that also appears in message (ids, limits, axes). Mirrors google.rpc.ErrorInfo.metadata. Strongly-typed context rides in the per-domain reason block below instead. Same leakage rule as message: servers SHOULD NOT put secrets, PII, or withheld existence/authorization detail here — it is the same potential side channel as the absence oracle.
transaction_denialTransactionDenial10reason oneof — ExecuteTransaction refused as a whole request
catalog_rejectionCatalogRejection11reason oneof — CatalogService rejection
registration_failureRegistrationFailure12reason oneof — agent/provider registration refused
dispute_failureDisputeFailure13reason oneof — DisputeTransaction filing refused
domain_verification_failureDomainVerificationFailure14reason oneof — domain verification failed
retrieval_auth_failureRetrievalAuthFailure15reason oneof — signed-URL / proof-of-possession check failed
usage_report_rejectionUsageReportRejection16reason oneof — ReportUsage filing rejected
request_auth_failureRequestAuthFailure17reason oneof — RFC 9421 request signature on an RPC refused

Exactly one typed reason block is set, selected by the failing method. The reason oneof is absent for generic transport-class failures (e.g. INVALID_ARGUMENT, INTERNAL) that carry no domain-specific reason.

ExecuteTransaction refused the whole request and decided no item, for a reason about the requester that would deny every item alike (for example DENIAL_REASON_ACCOUNT_NOT_REGISTERED or DENIAL_REASON_REPORTING_OVERDUE). On a purchase a Broker relayed, the requester is the agent and the account is the agent’s, never the Broker’s. Reuses the DenialReason vocabulary. A denial of an item is never carried here: it is that item’s TransactionResultItem.denial_reason, in a successful response, whatever the item count. restriction_mismatches and offer_id are deprecated and never set.

FieldTypeNumberDescription
reasonDenialReason1The denial reason (defined-only, non-zero)
restriction_mismatchesrepeated RestrictionKind2DEPRECATED, never set. RESTRICTION_NOT_SATISFIED is never sent: the Exchange never enforces a restriction (see Restriction). The field is retained because removing it would break the v1 wire contract; a receiver ignores it.
offer_idoptional string3DEPRECATED, never set. A whole-request refusal pertains to no one offer; a denial of one offer rides on that offer's TransactionResultItem. The field is retained because removing it would break the v1 wire contract; a receiver ignores it.
exchangeoptional string4Bare host of the Exchange that PRODUCED this denial, in the form "Request recipient" defines in the file header. Not an echo of what the caller sent: on a relayed or fanned-out execute the request went to a Broker, so the Exchange that refused may not be one the agent named (through BrokerService.ExecuteTransaction this detail reaches the agent inside UpstreamRefusal.detail, beside UpstreamRefusal.party). Carrying it here is what lets ACCOUNT_NOT_REGISTERED be actionable — the agent learns where to call Register without fetching a manifest to work it out. NOTHING SIGNS THIS VALUE: it rides in a response, and on a relayed path the response passed through an intermediary, so this field is exactly the unsigned addressing the request-side exchange field exists to refuse. Treat it as a HINT, not an instruction. Before acting on it — and registering is a consequential act, handing an operator's business data and a signed acceptance of that Exchange's terms to whoever answers — a caller MUST check the value against a domain it already trusts for this transaction: the signed offer.exchange of the items in the refused request, or its own RequestConstraints.exchanges set. A value matching neither is reported to the caller and never dialled, because a hostile intermediary that could choose it would be choosing where an unattended agent registers.

A CatalogService call could not be applied.

FieldTypeNumberDescription
reasonCatalogRejectionReason1The rejection reason (defined-only, non-zero)
rejected_pathsrepeated string2The entry paths the refusal is about. A catalog push is all-or-nothing, so these name which entries failed inside a submission that persisted nothing — they are not a list of what was dropped from an otherwise applied batch.

A registration request could not be completed.

FieldTypeNumberDescription
reasonRegistrationFailureReason1The failure reason (defined-only, non-zero)
field_errorsrepeated RegistrationFieldError2When reason = INVALID_REGISTRATION_DATA: the registration_data members that are missing or do not conform. Empty for every other reason — enforced by the message rule above, not left to prose.

One registration_data member that failed the Exchange’s published AccountRegistration.data_schema, carried on RegistrationFailure.field_errors.

FieldTypeNumberDescription
pathstring1RFC 6901 JSON Pointer to the offending member, relative to registration_data (e.g. "/vat_id", "/address/postal_code"). The empty string addresses registration_data itself, for whole-object failures (oneOf, minProperties) that belong to no single member.
errorstring2Developer-facing, NON-authoritative description of what failed (e.g. "required", "must match ^[A-Z]{2}[0-9]+$"). Wording is validator-defined and not stable across Exchanges; clients branch on reason, never on this text. States the constraint, NEVER the submitted value — the ErrorDetail leakage rule applies here too.

A dispute could not be filed (distinct from DisputeReason, why the agent disputes, and DisputeStatus, an accepted dispute’s lifecycle).

FieldTypeNumberDescription
reasonDisputeFailureReason1The failure reason (defined-only, non-zero)

RequestDomainVerification / ConfirmDomainVerification failed.

FieldTypeNumberDescription
reasonDomainVerificationFailureReason1The failure reason (defined-only, non-zero)

A signed-URL retrieval or its proof-of-possession check failed at the delivery edge.

FieldTypeNumberDescription
reasonRetrievalAuthFailureReason1The failure reason (defined-only, non-zero)

A usage report could not be accepted.

FieldTypeNumberDescription
reasonUsageReportRejectionReason1The rejection reason (defined-only, non-zero)

The RFC 9421 HTTP message signature on an RPC request failed verification, refused with the Connect code unauthenticated. Distinct from RetrievalAuthFailure, which reports the signed-URL / proof-of-possession check at the delivery edge.

FieldTypeNumberDescription
reasonRequestAuthFailureReason1The failure reason (defined-only, non-zero)
ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1NOT_CATALOG_CONTRIBUTORThe signer is not authorized to push for an entry's domain: it is neither that domain's publisher nor a contributor the publisher's manifest lists in catalog_contributors. An entry naming a resource owner the signer may not push for is this reason too.
2TENANT_MISMATCHtenant_id does not match the authenticated caller
3DOMAIN_NOT_VERIFIEDcontributing domain is not verified
4SIGNATURE_INVALIDDEPRECATED, never sent. An Exchange MUST NOT send this value. A catalog call whose request signature is missing, does not verify or is stale is refused like every other signed request: UNAUTHENTICATED with ErrorDetail.request_auth_failure (SIGNATURE_MISSING, SIGNATURE_INVALID or SIGNATURE_STALE; see RequestAuthFailureReason). A receiver treats this value as unknown. The number is retained because removing it would break the v1 wire contract, and it MUST NOT be reused or given a new meaning.
5MALFORMED_ENTRYa resource entry failed schema/validation
6UNKNOWN_VOCAB_TOKENan unregistered vocab token in a restriction/term
7QUOTA_EXCEEDEDcontributor push quota exceeded (per-caller)
8TERMS_LIMIT_EXCEEDEDA single entry carries more license terms than ResourceEntry.terms allows. Retired on the PushResources path: the cap is a wire rule now, so a push carrying an over-cap entry is refused whole, before any per-entry classification runs, and no rejection naming this reason can be produced for it. Kept for a deployment that applies the cap somewhere the wire rules do not reach — an entry that arrived by some other route than PushResources.
9URI_UNAVAILABLEThe URI cannot be claimed by this caller's entries. Named from the caller's own perspective ON PURPOSE: it MUST NOT disclose that another resource/ contributor already owns the URI. Within one publisher, mutually-untrusting contributors share a catalog, so an "owned by another" reason would be a confirmed-existence oracle a contributor could use to map a competitor's catalog. The conflict is resolvable only by the publisher (who is authorized to see full ownership); the human-readable message routes the caller there without confirming who, if anyone, holds the URI.
10UNKNOWN_CRITICAL_EXTENSIONThe push's own ext_critical names a key the Exchange does not understand (see "Critical extensions" in the file header). ResourceEntry.ext_critical is for the agent and never produces this reason.
11EXCHANGE_NOT_LISTEDThe publisher's manifest does not list this Exchange: an entry's domain serves a /.well-known/fora.json whose WellKnownManifest.exchanges does not name the recipient, so the Exchange is not authorized to sell that domain's content.
ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1DOMAIN_NOT_VERIFIEDcaller domain is not verified
2INVALID_KEYsigning key malformed or unsupported
3SIGNATURE_INVALIDrequest signature invalid
4ALREADY_REGISTEREDLEGACY, never emitted. Register MUST NOT emit this reason. Registering again for an agent that already holds an account SUCCEEDS: it is answered from the stored record and returns the existing billing_ref, so there is no refusal to report. See "Repeat registration" in the Agent Account Registration section. The number is retained and MUST NOT be reused, and this value MUST NOT be given a new meaning. It reads like a natural home for a future cross-account identity collision — one agent key claiming a business identity another account already holds — and that is exactly why it is closed off here: this contract defines no way for an Exchange to correlate business identity across accounts, since registration_data is operator-defined and passed through uninspected unless a schema is published. Repurposing 4 later would silently change what it means for every client already built against this text. If that case is ever specified, it needs its own identity model and its own named reason.
5QUOTA_EXCEEDEDregistration quota exceeded
6INVALID_REGISTRATION_DATAregistration_data does not conform to the Exchange's published AccountRegistration.data_schema
7TERMS_DIGEST_STALEterms_digest does not match the currently published WellKnownManifest.terms_digest, or was omitted while the Exchange publishes one
8UNKNOWN_CRITICAL_EXTENSIONThe request's ext_critical names a key the Exchange does not understand (see "Critical extensions" in the file header), on a first registration and on a repeat alike. Refused with the Connect code invalid_argument.
ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1TRANSACTION_NOT_FOUNDtransaction_id names no transaction this Exchange recorded under the caller's account.
2REPORT_NOT_FILEDNo accepted usage report precedes the dispute: report_id is empty, unknown, or names a report for another transaction. The agent files the transaction's UsageReport first and disputes with the report_id it returns (see DisputeRequest.report_id).
3WINDOW_EXPIREDThe filing arrived after the transaction's dispute window closed. The window is set by the Exchange; a filing after it is refused for that time cause alone.
4DUPLICATEA dispute was already filed for this transaction. A retry of the same filing under the same idempotency_key is not a duplicate: it is a replay, answered with the original DisputeResponse.
5INELIGIBLEThe transaction exists and the filing passes the checks above, but the transaction's state admits no dispute under the Exchange's rules. Filing again does not change the answer.
6UNKNOWN_CRITICAL_EXTENSIONThe filing's ext_critical names a key the Exchange does not understand (see "Critical extensions" in the file header). Refused with the Connect code invalid_argument.
ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1CHALLENGE_NOT_FOUNDtoken not served at the well-known path
2CHALLENGE_MISMATCHserved token does not match the issued token
3CHALLENGE_EXPIREDconfirmation arrived after the challenge expired
4FETCH_FAILEDExchange could not fetch the verification URL
5EXCHANGE_NOT_AUTHORIZEDfora.json does not list this Exchange
6KEY_REGISTRATION_FAILEDsigning-key registration failed during confirmation
7UNKNOWN_CRITICAL_EXTENSIONThe request's or confirmation's ext_critical names a key the Exchange does not understand (see "Critical extensions" in the file header). Refused with the Connect code invalid_argument.

Single-sources the delivery-edge token unions (signed-URL checks in verify.ts, RFC 9421 proof-of-possession in pop.ts).

ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1URL_EXPIREDSigned-URL checks (verify.ts).
2URL_SIGNATURE_MISSING'missing_sig'
3URL_EXPIRY_MISSING'missing_exp'
4URL_SIGNATURE_MISMATCH'signature_mismatch'
5AGENT_KEY_MISSINGProof-of-possession checks (pop.ts).
6PROOF_SIGNATURE_MISSING'missing_sig'
7KEYID_MISMATCH'keyid_mismatch'
8THUMBPRINT_MISMATCH'thumbprint_mismatch'
9PROOF_CREATED_MISSING'pop_missing_created'
10PROOF_EXPIRY_MISSING'pop_missing_exp'
11PROOF_EXPIRED'pop_expired'
12PROOF_SIGNATURE_INVALID'pop_sig_invalid'
ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1TRANSACTION_NOT_FOUNDtransaction_id is unknown
2DUPLICATEa report was already filed for this transaction
3WINDOW_EXPIREDThe reporting window closed before the report arrived: the transaction's ReportingObligation.window elapsed. This is the time cause, and it names only that: a report past its window is refused with this reason and the Connect code failed_precondition, whatever it reports. It is never used for a report's quantity.
4MISSING_REQUIRED_FIELDSReportingObligation.required_fields not satisfied
5MALFORMEDThe report payload failed validation: it breaks a field rule of UsageReport or carries a value the Exchange cannot read. Refused with the Connect code invalid_argument. A consumed_quantity that differs from the estimate is not malformed.
6UNKNOWN_CRITICAL_EXTENSIONThe report's ext_critical names a key the Exchange does not understand (see "Critical extensions" in the file header). Refused with the Connect code invalid_argument.

Deliberately coarse: each value names what the caller does next (sign the request, fix the signature, or re-sign now), never which validation step failed, so a refusal does not tell a forger how far its request got.

ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1SIGNATURE_MISSINGno signature, or none that parses; remedy: sign the request
2SIGNATURE_INVALIDthe signature does not verify: bad signature, unresolvable key, content-digest mismatch, required covered component missing
3SIGNATURE_STALEoutside its created/expires window, or already used (replay); remedy: re-sign now

The charging structure only (a closed set). The open-ended metering basis (“per what”) is NOT enumerated here — it lives in Pricing.unit as a registry-governed vocabulary. UNSPECIFIED is rejected on Pricing.model (omission cannot default to FREE).

ValueNameDescription
0UNSPECIFIEDunset — zero allowed on WellKnownManifest.pricing_models_supported (capability list); rejected (not_in:[0]) as the Pricing.model discriminator (omission cannot default to FREE)
1FREEno charge; rate must be 0 (state FREE explicitly — absent Pricing is not free)
2PER_UNITrate per Pricing.unit; unit REQUIRED (registered token or vendor:custom). Metered: the purchase charge follows the rule on Pricing; an estimated_quantity is optional, and positive when an offer states it
3FLATone-time flat fee; rate is the total, no unit

The metering basis (“per what”) is the Pricing.unit vocabulary, not a model; a subscription is FREE + scopes; attribution and contribution are Obligation.kinds; revenue-share settlement is off-protocol.

Registered Pricing.unit tokens (the metering basis — rendered from the proto, any vendor:namespaced token also accepted):

fetches accesses tokens calls pages seconds minutes records streams images seats units-manufactured characters bytes items sq-km

How usage is tracked for billing reconciliation. Used in Pricing.metering (field 9). Absent = ONLINE.

ValueNameDescription
0ONLINEDefault. Exchange tracks usage events in real time. ReportUsage is required.
1NONEOne-time perpetual sale. No ongoing metering; billing_id is issued at ExecuteTransaction and the ledger entry is closed. No ReportUsage required.
2OFFLINE_SELF_REPORTEDAgent self-reports physical-world consumption (e.g. units manufactured from a licensed design). Exchange audits.

How the Exchange interprets a LicenseTerm’s machine fields.

ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1ENUMERATEDMachine restrictions/quotas/obligations are the complete, authoritative expression of the term (internally consistent, no self-contradiction): declared terms the parties agree to when the agent accepts the offer. Pricing MUST be present on a catalog term (on an offer it is Offer.pricing).
2REFERENCE_ONLYThe document at License.uri (MUST be non-empty) is the authoritative, complete source; the agent reads it before using. Machine restrictions/quotas/obligations are optional here (the publisher MAY send Pricing alone) but any that are sent must be accurate (MUST NOT contradict the referenced document) and bind the parties just like ENUMERATED. Pricing is still required on a catalog term (on an offer it is Offer.pricing).

Which dimension a Restriction constrains.

ValueNameDescription
0UNSPECIFIEDunset — zero allowed on AcceptableRestriction.axis / OfferGroup.restriction_filters / *.restriction_mismatches; rejected (not_in:[0]) as the Restriction.kind discriminator
1FUNCTIONWhat the agent may do with the content. Seeded from RSL 1.0 AI-use vocabulary and extended with established IP/copyright terms.
2GEOGRAPHYWhere the agent may use the content. ISO 3166-1 alpha-2 codes (US, DE, GB) are valid structurally; only the non-ISO specials are registered here.
3USER_TYPEWho may access and use the content.
4OTHERCustom axis; values carried in permitted/prohibited

The complete token list for each axis, rendered directly from the proto’s (fora.v1.vocab_enum) options at build time — not hand-maintained here, so it cannot drift from the contract. Any vendor:namespaced token is also accepted on every axis; these are the registered bare tokens.

FUNCTION — what the agent may do:

all ai-all ai-train ai-input ai-index search crawl text-and-data-mining tts commercial advertising editorial research reproduce distribute modify display sync broadcast stream print manufacture sell
Accepted aliases, canonicalised at ingest: train-ai → ai-train, generative-ai → ai-input, scrape → crawl, tdm → text-and-data-mining, copy → reproduce, adapt → modify, derivative → modify

GEOGRAPHY — registered specials (plus structural ISO 3166-1 alpha-2 codes):

* EU EEA

USER_TYPE — what kind of entity the agent represents:

individual academic non_profit news_publisher broadcaster commercial_entity
Accepted aliases, canonicalised at ingest: personal → individual, business → commercial_entity, enterprise → commercial_entity

Time window over which a Quota.limit accumulates.

ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1HOURLYResets each hour
2DAILYResets each day
3MONTHLYResets each month
4TOTALLifetime cap — never resets

What the agent must do after use. ATTRIBUTION and CONTRIBUTION are behavioral requirements, not pricing models.

ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1ATTRIBUTIONCredit the author or publisher whenever the resource is used
2CONTRIBUTIONGood-faith payment — amount suggested, not contractually fixed
3SHARE_ALIKEDerivatives must be released under the same / compatible license (CC-BY-SA / GPL style); scope_license required
4NETWORK_COPYLEFTNetwork service triggers copyleft (AGPL style)
5NOTICEInclude the specified copyright notice
6OTHERCustom requirement, described in Obligation.detail

When an Obligation activates.

ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1ON_USETriggered on any use
2ON_DISTRIBUTIONTriggered when copies are distributed
3ON_NETWORK_SERVICETriggered when served over a network (AGPL)
4ON_DERIVATIVETriggered when a derivative work is produced

Why a purchase was refused. No value refuses a purchase for scope: an offer the Exchange presented is honoured until it expires. The value the table marks deprecated is never sent, and a receiver treats it as unknown.

ValueNameDescription
0UNSPECIFIEDoutput enum; zero = not-applicable on TransactionResultItem.denial_reason, rejected (not_in:[0]) where set on TransactionDenial.reason
1ACCOUNT_INACTIVEthe requester's account (the billing_ref minted at Register) exists but is not active — typically awaiting the Exchange operator's out-of-band activation; the remedy is to wait or contact the operator, NOT to register again
2INSUFFICIENT_BALANCERequester's balance too low
3RATE_LIMITEDToo many requests
4CONTENT_UNAVAILABLEthe Exchange that owns the resource reports it no longer available; NOT a catch-all for upstream failures — a Broker reports an Exchange that refused or did not answer its sub-request on TransactionResultItem.refusal, never as this reason
5RESTRICTION_NOT_SATISFIEDDEPRECATED, never sent. An Exchange MUST NOT send this value. Restrictions are declared terms the parties agree to, and the Exchange never enforces them (see Restriction), so a purchase is never denied for one. A receiver treats this value as unknown. The number is retained because removing it would break the v1 wire contract, and it MUST NOT be reused or given a new meaning.
6REPORTING_OVERDUERequester has >20% overdue reports (MAY threshold)
7OFFER_EXPIREDOffer TTL exceeded
8SIGNATURE_INVALIDA signature this item depends on does not verify, or does not cover what arrived: the offer's Exchange signature (Offer.signature), the item's AgentAcceptance, or the agent's AgentRequestAcceptance over the request's items. A subrequest whose items are not exactly the agent's signed set projected onto this Exchange (an item dropped, added or reordered) has every item denied with this reason, in the body (see AgentRequestAcceptance). On a purchase a Broker relayed, an item whose AgentAcceptance does not verify under the keys the directory requester.domain names publishes is denied with this reason, in the body, exactly as for the AgentRequestAcceptance: the item is denied and the whole request is not refused.
9QUOTA_EXCEEDEDSubscription access count exhausted for this period
10DELEGATION_INVALIDOn a purchase item: delegation missing, malformed, unverifiable (chain linkage, issuer, signature), expired or not yet valid, addressed to another recipient, holder binding failed, a child widens its parent, or scopes/caps do not cover the request. A token over the size limit is refused at wire validation, before any item; at discovery a failed delegation hides the gated terms and reports nothing
11SCOPE_INSUFFICIENTDEPRECATED, never sent. An Exchange MUST NOT send this value. An offer the Exchange presented is honoured until it expires, so a purchase is never refused for scope. A resource the requester's scopes leave with no presentable offer is answered at discovery with no offers and no absence reason, so the requester never holds an offer to buy (the existence-hiding rule, stated on Requester.scopes). A receiver treats this value as unknown. The number is retained because removing it would break the v1 wire contract, and it MUST NOT be reused or given a new meaning.
12ENTITLEMENT_MISSINGEntitlement family — subscription/entitlement access failures on a subscription-gated offer. Finer-grained than DELEGATION_INVALID so callers and operator tooling can triage each mode. These single-source the Exchange's KindEntitlement* refusal taxonomy, which today is distinguishable only by a server-side tag (all collapse to UNAUTHENTICATED on the wire). Format-neutral: they classify a JWT/opaque entitlement-token failure, not a format-specific one.
13ENTITLEMENT_MALFORMEDentitlement token failed to decode (malformed)
14ENTITLEMENT_EXPIREDentitlement token's validity window has passed
15ENTITLEMENT_WRONG_BUYERtoken's subscriber_org does not match the asserted requester
16SUBSCRIPTION_LAPSEDthe covering subscription contract has lapsed
17ENTITLEMENT_NOT_GRANTEDsubscription exists but no buyer-side grant ties this caller to it
18ACCOUNT_NOT_REGISTEREDSplit out of the former single billing-reference reason. An agent hits the wall at execute, not at register, and "no account here" and "account awaiting activation" are two different states of the agent's account with two different remedies — call Register, which an agent can do unattended, versus wait for a human. Neither value discloses the Exchange's internal billing state; both describe the agent's own account, also on a purchase a Broker relayed (see "Accounts" in the file header), which the agent may already query with GetAccountStatus. TransactionDenial.exchange names WHERE to register, so the agent converges without fetching a manifest first.
19RELAY_NOT_ACCEPTEDThe provider that sells this item's resource at this Exchange does not accept purchases relayed by a Broker (BrokerService.ExecuteTransaction). Decided PER ITEM, by each offer's own provider, and answered in the body on that item's denial_reason: other items in the same sub-request, sold by providers that accept relayed purchases, may succeed. It is never a refusal of the whole sub-request, and an Exchange MUST NOT send it as ErrorDetail.transaction_denial. The remedy is to buy this offer directly, calling ExchangeService.ExecuteTransaction at offer.exchange.
20UNKNOWN_CRITICAL_EXTENSIONThe request's ext_critical names a key the recipient does not understand (see "Critical extensions" in the file header). Every item of the request is denied with this reason, in the body.
ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1CONTENT_MISMATCHContent hash does not match what was promised in the Offer.
2DELIVERY_FAILEDResource was not delivered (signed URL returned 404/403/5xx).
3WRONG_CONTENTResource was delivered but is entirely different from what was described.
4EXPIRED_BEFORE_FETCHSigned URL expired before the agent could fetch the resource.
5INCOMPLETE_CONTENTResource was truncated or incomplete.

Full dispute lifecycle from filing to final resolution.

ValueNameDescription
0UNSPECIFIEDunset — output/optional; zero is a valid not-applicable/unset state
1FILEDAgent submitted DisputeRequest. Initial state.
2AUTO_RESOLVEDExchange auto-resolved via Tier 1 rules (CDN logs, hash comparison).
3EVIDENCE_NEEDEDExchange requests additional evidence from the agent or provider.
4UNDER_REVIEWExchange is reviewing with Tier 2 resolution rules.
5ESCALATEDEscalated to Tier 3 pattern-based investigation.
6RESOLVEDDecision made (credit, redelivery, rejected). See resolution field.
7APPEALEDLosing party appealed with new evidence. Re-enters review.
8SETTLEDFinancial settlement applied.
9FINALNo further appeals. Dispute closed.

Outcome of a resolved dispute.

ValueNameDescription
0UNSPECIFIEDunset — output/optional; zero is a valid not-applicable/unset state
1CREDITAccount credit applied to the agent's next billing cycle.
2REDELIVERYNew signed URL issued for the same resource (e.g., when content hash now matches after provider correction).
3REJECTEDDispute reviewed and rejected; no remedy applied.
4INVESTIGATIONEscalated to Tier 3 pattern analysis for further investigation.

How the citation is presented to the user.

ValueNameDescription
0LINKHyperlink citation
1FOOTNOTEFootnote citation
2INLINEInline text citation

Why no offers are available for a requested URI. No value reports a scope shortfall: a resource outside the requester’s scopes comes back with no offers and no absence reason. The value the table marks deprecated is never sent, and a receiver treats it as unknown.

ValueNameDescription
0UNSPECIFIEDunset — output/optional; zero is a valid not-applicable/unset state
1NOT_IN_CATALOGResource URI is not in this Exchange's catalog.
2CONTENT_BLOCKEDResource exists but the provider has opted out of AI access for it (the provider's consent/opt-out signal blocks licensing).
3RESTRICTION_FILTEREDResource exists but its offers were pre-filtered out for one or more restriction axes the requester stated (a convenience filter matched to the query, not an enforcement verdict — see Restriction). The filtered axes are listed in OfferGroup.restriction_filters, in the same RestrictionKind vocabulary the terms use. The agent MAY still be eligible.
4TEMPORARILY_UNAVAILABLEResource is temporarily unavailable (e.g., provider feed refresh in progress).
5NOT_AUTHORIZEDExchange is not authorized by the provider to sell this resource.
6SCOPE_INSUFFICIENTDEPRECATED, never sent. An Exchange MUST NOT send this value. When the requester's scopes leave no presentable offer for a resource, the Exchange answers with no offers and no absence reason, exactly as for any resource with nothing to offer, so the requester never learns the resource exists (the existence-hiding rule, stated on Requester.scopes). An offer the Exchange did present is honoured until it expires, so no scope refusal follows at purchase either. A receiver treats this value as unknown. The number is retained because removing it would break the v1 wire contract, and it MUST NOT be reused or given a new meaning.
7UNKNOWN_CRITICAL_EXTENSIONConsumer encountered ext_critical keys it does not recognize. The unrecognized keys SHOULD be listed in the OfferGroup's ext field under "unrecognized_critical_extensions" for diagnostic purposes.
8BUDGET_EXCEEDEDOffers exist, but the agent's budget (RequestConstraints) left every one out: BrokerService.Resolve applied the budget as a filter and no offer remained. Resolve returns this as a successful "no result" answer, on OfferGroup.absence_reason for a URI the budget left with no offer and on DiscoveryResponse.absence_reason when it left none at all. It is the only way a budget is reported: never as NOT_AUTHORIZED, and never as a non-OK error such as RESOURCE_EXHAUSTED (see BrokerService.Resolve).

What kind of entity is making the request.

ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1AGENTAutonomous AI agent (LLM, RAG system, research bot).
2HUMAN_TOOLHuman using an AI-powered tool (copilot, assistant).
3SERVICEEnterprise service account (automated pipeline, cron job).
4DELEGATEDAgent acting on behalf of a user (delegated identity).
5RESEARCHResearch pipeline (batch data collection, model training).

Identifies which FORA participant a WellKnownManifest describes.

ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1AGENTan agent's manifest
2EXCHANGEan Exchange's manifest
3BROKERa Broker's manifest; the role never admits the Broker anywhere
4PUBLISHERa publisher's manifest
ValueNameDescription
0UNSPECIFIEDunset — output/optional; zero is a valid not-applicable/unset state
1EXCHANGEURI was requested by the agent directly or found via Exchange query.
2SEARCHURI was discovered via a search engine (e.g., Exa, Tavily, Brave Search). The Broker searched on the agent's behalf, then routed through Exchange.
3RECOMMENDATIONURI was recommended by a resource recommendation service.
4SYNDICATIONURI was found via resource syndication tracking (e.g., same article on another domain).
ValueNameDescription
0UNSPECIFIEDunset — output/optional and capability-list; zero is a valid not-applicable/unset state
1DIRECTExchange returns resource inline or via its own endpoint.
2INSTRUCTIONSExchange returns access info (signed URL, token) for retrieval from a Resource Owner / Resource Delivery Endpoint.
3STREAMINGResource delivered via real-time streaming connection (WebSocket, SSE, gRPC stream). The signed URL points to a streaming endpoint. Agent connects and receives continuous data for the duration of the session.

Signals whether resource content changes over time. Drives hash verification behavior.

ValueNameDescription
0UNSPECIFIEDunset — the Exchange defaults to STATIC at Offer build; an explicit UNSPECIFIED is rejected on ResourceEntry.resource_mutability and the Offer's ResourceIdentity.resource_mutability (both {not_in:[0]})
1STATICContent is immutable. Hash computed at offer time will match at delivery time. Agent SHOULD verify content_hash on delivery. Mismatch is disputable.
2DYNAMICContent changes between offer generation and agent fetch. Hash reflects state at offer time — mismatch is expected, not disputable. Offer.data_as_of indicates when the snapshot was taken.
3LIVEContent does not exist at offer time (real-time streaming). No content_hash is applicable. The "resource" is the stream endpoint/channel. Metering is time-based (per-minute, per-hour).
ValueNameDescription
0UNSPECIFIEDunset — output/optional; zero is a valid not-applicable/unset state
1FORA_SITEMAPFORA XML namespace in sitemap
2RSLRSL rsl.txt
3SITEMAPStandard sitemap.xml
4HTML_CRAWLHTML crawl + readability extraction
5CMS_APICMS REST API (WordPress, etc.)
6MANUALManual configuration
7CATALOG_APIThird-party CatalogService push
ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1DIRECTProvider has a direct contract with this Exchange.
2RESELLERExchange resells resources via another authorized party.

Authentication methods a participant advertises in its WellKnownManifest.

ValueNameDescription
0UNSPECIFIEDunset — zero allowed on WellKnownManifest.supported_auth_methods (capability list); no discriminator carrier
1GNAPGNAP (RFC 9635) — key-first identity, key-bound tokens. Recommended.
2OAUTH_DPOPOAuth 2.0 + DPoP (RFC 9449) — sender-constrained tokens. Enterprise recommended.
3OAUTH_BEAREROAuth 2.0 Bearer JWT — acceptable, widely deployed.
4OAUTH_MTLSOAuth 2.0 + mTLS — high-security environments.

C2PA / content-provenance status carried on ResourceIdentity.

ValueNameDescription
0C2PA_STATUS_UNSPECIFIEDunset — output/optional; zero is a valid not-applicable/unset state
1C2PA_STATUS_TRUSTEDManifest is valid AND signer certificate chains to a C2PA Trust List root. Highest assurance: provenance is cryptographically verified by a trusted CA.
2C2PA_STATUS_VALIDManifest is structurally correct and signature verifies, but signer certificate does NOT chain to a C2PA Trust List root. The content has provenance, but the signer is not institutionally vouched for.
3C2PA_STATUS_INVALIDManifest is present but validation failed (signature mismatch, malformed JUMBF, certificate expired, hard binding broken).
4C2PA_STATUS_ABSENTContent was checked and has no C2PA manifest.