Skip to content

Proto: FORA v1

Source: proto/fora/v1/fora.proto

The core protocol. Both AI agents and Brokers are valid clients.

RPCRequestResponseDescription
DiscoverResourcesResourceQueryResourceResponseDiscover available resource offers matching the query. Steps 2-3 in the FORA flow.
ExecuteTransactionTransactionRequestTransactionResponseCommit to an offer and receive delivery information. Steps 4-5 in the FORA flow.
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.
RequestDomainVerificationDomainVerificationRequestDomainVerificationChallengeRequest a domain verification challenge for provider onboarding. Used by fora-cli to prove domain control before pushing signing keys. 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.

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.
RefreshCatalogRefreshCatalogRequestRefreshCatalogResponseTrigger a full catalog refresh from configured sources.

The Broker entry point. 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 does not deliver inline — the per-transaction result (transaction id, billing id, retrieval endpoint, …) is returned on TransactionResponse via the separate execute path, routed by Offer.exchange.

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 calls ExchangeService.ExecuteTransaction on a selected offer, and rides there on TransactionResponse.DenialReason. 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.
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, what scopes they have, and optional delegation chain.
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. Present when offers is empty. 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 and DiscoveryRequest.

FieldTypeNumberDescription
idstring1Unique requester identifier (e.g., "agent-research-bot-001").
domainstring2Domain the requester belongs to. It carries the same bare-host shape "Request recipient" defines in the file header, for the same structural reason: a scheme, path or query smuggled in here would choose what gets fetched, not merely from where. It is NOT how a verifier finds this requester's keys: those live in the WBA directory, and verification resolves that directory from the COVERED Signature-Agent header, never from this self-asserted value.
typeRequesterType3What kind of entity is making this request.
nameoptional string4Human-readable name (e.g., "Acme Research Assistant").
scopesrepeated string6Entitlement scopes. Declare what the requester can access. The Exchange filters its catalog to resources matching these scopes. Resources outside the scopes are not returned — the requester never learns they exist. This is the enforcement mechanism for both enterprise RBAC and open-market subscription entitlements. 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. 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). Examples: "dist:" covers "dist:US" and "dist:US:CA"; "dist:US:" covers "dist:US:CA" but not "dist:EU"; bare "dist" covers only "dist"; granted "dist:US:CA" does NOT cover required "dist:US"; "*" covers everything. This same rule governs LicenseTerm.scopes — 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).
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, spend-capped credential. The credential itself is opaque bytes carried in token, and token_format selects how to interpret them: "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: a chain of cnf-linked JWTs (RFC 7800 cnf.jkt = RFC 7638 thumbprint) where each child is signed by the key its parent named and narrows scope, and the holder proves possession by signing the request (RFC 9421).

FieldTypeNumberDescription
principal_domainstring1Who granted this delegation (domain for public key lookup).
principal_idstring2Principal's identifier (e.g., "user@acme.com", "marketdata.example.com").
scopesrepeated string3Scopes granted by this delegation. MUST be a subset of the principal's own scopes (attenuation — can only narrow, not widen).
expires_atTimestamp4When this delegation expires. Exchange MUST reject expired tokens.
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).
token_formatstring7Token format: "jwt" (default). Empty is treated as "jwt". The field stays open for a future format.
revocation_urioptional string8Optional: URI for real-time revocation checking. Exchange MAY check this for high-value transactions. Not checked for routine low-value access (performance tradeoff).
issueroptional string11Token issuer. OIDC issuer URL or GNAP grant server URL. Exchange uses this for JWT validation (OIDC discovery → JWKS) or GNAP token introspection.
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. The one claim worth restating: every delegation MUST carry a holder binding (the cnf claim, cnf.jkt = the RFC 7638 thumbprint of the holder key) — the request-signing key MUST hash to cnf.jkt, or the token is rejected. Without it the token is bearer-usable. Everything else (scope, spend cap, expiry, etc.) is optional and defined in that single source of truth.

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

  • Original: scope: credit:*, max_spend_cents: 100000
  • Attenuated: scope: credit:read, max_spend_cents: 20000, uris: duns:123*

Multi-hop forwarding (no message — HTTP layer)

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

There is no in-message hop chain. Multi-hop forwarding (Agent → Broker → … → Exchange) is a stack of RFC 9421 HTTP Message Signatures: each forwarding party adds one labeled signature, and each signature covers the request plus the prior hop’s signature, so the ordered set of signatures is the chain (tamper-evident, order-bound). The Exchange resolves each keyid (an RFC 7638 thumbprint) in the signer’s WBA directory at {domain}/.well-known/http-message-signatures-directory, verifies every signature, and enforces RequestConstraints.max_hops / WellKnownManifest.max_intermediary_hops by counting them. Responses do not retrace the chain — the terminal Exchange returns directly to the originating agent (bound by agent_identity_hash).

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).
pricingPricing3Pricing for this offer. An offer represents a single licensing arrangement: each projected LicenseTerm yields its own offer, so this is that term's pricing (the authoritative copy lives in terms[].pricing). Used for cross-exchange comparison and Broker ranking. 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.
delivery_methodDeliveryMethod4How resource will be delivered.
reportingoptional ReportingObligation5Post-usage reporting requirements for this offer.
expires_atoptional Timestamp6When this offer expires (ISO 8601).
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 LicenseTerm19Licensing terms for this offer, sourced from the publisher's ResourceEntry. Multiple terms when the resource has different arrangements by use case. 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. For text: token count. For video: duration in seconds. For documents: page count. For data: record count.
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.

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. The transaction's durable identity is the Exchange-assigned transaction_id in the response. Uniqueness is scoped to the verified RFC 9421 signer: the server dedupes per (authenticated caller, key), never globally, so a key chosen by one caller cannot collide with another's cached result.
requesterRequester4Requester identity — forwarded for authorization and audit.
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). Shared across the request; set once.
itemsrepeated TransactionResultItem13Per-offer results (one entry per committed item, in original order).
total_costoptional Cost14Aggregate cost across all items.
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 and binds the delivery URL to the agent’s key (RFC 7638 thumbprint). 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.

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. 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_idstring2
requester_domainstring3
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.

FieldTypeNumberDescription
offer_sigstring1The accepted Offer's signature (Offer.signature). Anchors the whole signed offer without re-serializing its terms/pricing/expiry.
requester_idstring2Requester identity (Requester.id) the acceptance is bound to.
requester_domainstring3Requester domain (Requester.domain) the acceptance is bound to.
idempotency_keystring4The transaction's idempotency key — binds the acceptance to a single execute so it cannot be replayed under a different transaction.

Result for a single offer in a batch transaction.

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.
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).
restriction_mismatchesrepeated RestrictionKind13When denial_reason = RESTRICTION_NOT_SATISFIED, the restriction axes the request failed, in the same RestrictionKind vocabulary the terms use.
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); 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.

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 report's durable identity is the Exchange-assigned report_id in UsageReportResponse. Uniqueness is scoped to the verified RFC 9421 signer: the server dedupes per (authenticated caller, key), never globally, so a key chosen by one caller cannot collide with another's cached result.
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).
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. Exchange cross-references against Offer.pricing.estimated_quantity.
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.

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 dispute's durable identity is the Exchange-assigned dispute_id in DisputeResponse. Uniqueness is scoped to the verified RFC 9421 signer: the server dedupes per (authenticated caller, key), never globally, so a key chosen by one caller cannot collide with another's cached result.
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. 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.

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 Exchange resolves the account from the verified request signature.

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. billing_ref is empty when the calling agent has no account yet.

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). Empty when the calling agent has no account yet.
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.
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. Health check endpoint URL.
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 forwarding hops this Exchange tolerates on an inbound request (Agent → Broker → … → Exchange), counted as RFC 9421 HTTP Message Signatures. A request carrying more SHOULD be rejected. Lets Exchanges publish their chain-depth tolerance so Brokers prune before forwarding. 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/jwk-set+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).

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_beforestring7RFC3339 timestamp. Key is invalid before this instant.
not_afterstring8RFC3339 timestamp. Key is invalid at and after this instant (strict upper bound).

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). When an agent talks directly to an Exchange, it uses ResourceQuery/TransactionRequest instead.

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, what scopes they have. The Broker forwards this to Exchanges in ResourceQuery.requester.
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 uses this to: 1. Route queries to Exchanges that support these profiles 2. Forward the profiles in ResourceQuery.supported_profiles 3. Include profile-specific ext fields when returning results 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.

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 for per-period tracking. E.g. "user:u-12345" for per-user budgets, "team:eng" for per-team. The Broker tracks cumulative spend per scope across sessions.
period_budgetoptional Cost8Per-period budget limit. The Broker tracks spend against this for the budget_scope. Transactions that would exceed are denied.
budget_periodoptional Duration9Budget period (e.g. "2592000s" = 30 days; proto-JSON encodes Duration as seconds). Resets at period boundary.
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 forwarding hops the agent will allow (Agent → Broker → … → Exchange), counted as the number of RFC 9421 HTTP Message Signatures on the request. Caps chain depth so a request is not relayed through more brokers than the agent is willing to trust or pay. A Broker MUST NOT forward a request whose signature count would exceed this. Absent = agent imposes no cap (the Exchange's max_intermediary_hops still applies).

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 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 (SCOPE_INSUFFICIENT, 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.
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 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. 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 caller_id names the contributor whose key signed the request.

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.
caller_idstring4Identity of the caller (who is pushing this data). The Exchange verifies this matches a registered CatalogService client.
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. For ENUMERATED terms, Pricing MUST be present. For REFERENCE_ONLY terms, License.uri is authoritative. The Exchange validates ENUMERATED terms at push time and surfaces them in Offer.terms on discovery. 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). 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.

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 that could not be applied travels as a non-OK transport error carrying ErrorDetail.catalog_rejection.

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 that could not be applied is not a response at all: it travels as a non-OK transport error carrying ErrorDetail.catalog_rejection.
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.

paths carries at least one path and at most 256 — the same batch bound PushResourcesRequest.entries carries — each in the same absolute-path shape ResourceEntry.path carries.

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 string3Paths to remove — the absolute-path shape ResourceEntry.path carries, at least one and at most 256, the same batch bound PushResourcesRequest.entries carries and for the same reason.
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.
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) and emission (Offer.terms).

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 term, any semantics — absent Pricing is a validation error. model = FREE must be stated explicitly — absent Pricing is not free.
  • 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-aiai-train, tdmtext-and-data-mining, personalindividual, …) — 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. REQUIRED for every term regardless of semantics — an agent cannot act on a priceless term, so absent Pricing is a validation error at ingest. 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.
scopesrepeated string7Delegation scope-gating: the Exchange returns this term to an agent iff the agent's delegation grant covers ALL of these scopes (AND-semantics). Empty = public. A subscription term is Pricing{model:FREE} + scopes:["subscription:..."]. Coverage uses the SAME matching rule as Requester/delegation scopes: segment-wise (":" separated), each granted segment must equal the corresponding required segment or be "", a terminal "" matches all remaining segments, 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; the agent self-selects the term it can honour (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 enforced. 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. The per-domain reason enums below are the single source of truth that replaces the former in-body failure fields (denial_reason, 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 denial
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

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 could not complete. Reuses the DenialReason vocabulary.

FieldTypeNumberDescription
reasonDenialReason1The denial reason (defined-only, non-zero)
restriction_mismatchesrepeated RestrictionKind2When reason = RESTRICTION_NOT_SATISFIED, the failed axes (same RestrictionKind vocabulary the terms use).
offer_idoptional string3Batch mode: the offer this denial pertains to.
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. 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 denied item, 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)
ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1NOT_CATALOG_CONTRIBUTORcaller is not an authorized contributor for the domain
2TENANT_MISMATCHtenant_id does not match the authenticated caller
3DOMAIN_NOT_VERIFIEDcontributing domain is not verified
4SIGNATURE_INVALIDrequest signature missing or invalid
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.
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
ValueNameDescription
0UNSPECIFIEDunset — rejected at ingest
1TRANSACTION_NOT_FOUNDtransaction_id is unknown
2REPORT_NOT_FILEDno UsageReport precedes the dispute (report_id missing/unknown)
3WINDOW_EXPIREDfiled outside the allowed dispute window
4DUPLICATEa dispute already exists for this transaction
5INELIGIBLEthe transaction/state is not disputable
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

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_EXPIREDfiled outside the reporting window
4MISSING_REQUIRED_FIELDSReportingObligation.required_fields not satisfied
5MALFORMEDreport payload failed validation

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)
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) and are enforced. Pricing MUST be present.
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 are enforced just like ENUMERATED. Pricing is still required.

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-aiai-train, generative-aiai-input, scrapecrawl, tdmtext-and-data-mining, copyreproduce, adaptmodify, derivativemodify

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: personalindividual, businesscommercial_entity, enterprisecommercial_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
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_UNAVAILABLEResource no longer available
5RESTRICTION_NOT_SATISFIEDAccepted term's restriction not satisfied by the request; the axes are in TransactionDenial.restriction_mismatches (single) / TransactionResultItem.restriction_mismatches (batch), same RestrictionKind vocabulary as the terms
6REPORTING_OVERDUERequester has >20% overdue reports (MAY threshold)
7OFFER_EXPIREDOffer TTL exceeded
8SIGNATURE_INVALIDOffer signature verification failed
9QUOTA_EXCEEDEDSubscription access count exhausted for this period
10DELEGATION_INVALIDDelegation missing, unverifiable, expired, holder binding failed, or scopes/caps do not cover the request
11SCOPE_INSUFFICIENTRequester scopes don't cover this resource
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 caller 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 caller's own account, which it may already query with GetAccountStatus. TransactionDenial.exchange names WHERE to register, so the agent converges without fetching a manifest first.
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.

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_INSUFFICIENTRequester's scopes/subscription do not cover this resource. Applies wherever access is gated by subscription or scope entitlements (not only enterprise deployments): the resource exists but the requester's delegation token or subscription does not grant it. The Exchange returns this so the requester learns the resource is reachable under the right subscription/scope. (Where existence itself must stay hidden, the Exchange MAY omit it silently instead.)
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 none fit within the requester's budget (e.g. every offer's price exceeds RequestConstraints.period_budget). Returned by Resolve as a successful "no result" answer when a budget/price ceiling filtered out every otherwise-licensable offer.

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
1AGENT
2EXCHANGE
3BROKER
4PUBLISHER
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.