JSONL Ingestion Feed
Overview
Section titled “Overview”The discovery paths (RSL, sitemap, well-known, crawl, …) let an Exchange pull a publisher’s licensing signals. The JSONL feed is the push counterpart: a publisher hands the Exchange a JSON-Lines file — one resource per line — and each line maps 1:1 onto a ResourceEntry and is submitted via CatalogService.PushResources. The reference client is the ramp-ingest binary.
publisher feed (.jsonl, one resource/line) └─► ramp-ingest parse (typed, strict) · canonicalize tokens · sign (RFC 9421) └─► CatalogService.PushResources ── ALL-OR-NOTHING ── │ trust: caller key fetched from /.well-known/ramp.json; tenant derived from the publisher domain │ validate: protovalidate CEL owns term shape / presence / coherence; vocab membership is a warning └─► catalog (per-tenant, URI-namespaced) ─► DiscoverResources ─► signed OfferThe feed is a transport convenience, not a third licensing model: every line is just a ResourceEntry, the same shape every other discovery path converges to. Parsing is strict — unknown keys are rejected, never silently dropped — so a typo is a loud error, not lost data.
Record schema (one line = one resource)
Section titled “Record schema (one line = one resource)”| Field | Type | Required | Maps to | Why it exists |
|---|---|---|---|---|
domain | string | yes | ResourceEntry.domain | resource origin; with path forms the canonical URI |
path | string | yes | ResourceEntry.path | URL path of the resource |
title | string | no | ResourceEntry.title | human label |
content_id | string | no | ResourceEntry.content_id | publisher’s stable id; keys the resource id so a re-push upserts in place |
word_count | int | no | ResourceEntry.word_count | content metric; informs token estimation |
estimated_quantity | int | no | ResourceEntry.estimated_quantity | pre-computed metering quantity (e.g. word_count × 1.32) |
content_hash | string | no | ResourceEntry.content_hash | integrity pin of the content bytes |
hash_method | string | no | ResourceEntry.hash_method | algorithm for content_hash (e.g. sha256) |
source | string | no | ResourceEntry.source (IngestionSource) | provenance of the metadata (INGESTION_SOURCE_CMS_API, …) |
resource_mutability | string (enum) | no | ResourceEntry.resource_mutability (ResourceMutability) | RESOURCE_MUTABILITY_STATIC/RESOURCE_MUTABILITY_DYNAMIC/RESOURCE_MUTABILITY_LIVE; drives delivery-time hash-verification behavior. Omitting the field defaults the Offer to RESOURCE_MUTABILITY_STATIC; explicit RESOURCE_MUTABILITY_UNSPECIFIED is rejected |
provenance_source | string | no | ResourceEntry.provenance_source | who supplied the metadata (audit trail) |
provenance_timestamp | RFC3339 | no | ResourceEntry.provenance_timestamp | when it was collected / generated |
license | object | no (yes for any REFERENCE_ONLY term) | LicenseTerm.license | governing license document {id, uri, uri_digest, name} |
terms | array (≥1) | yes | ResourceEntry.terms[] | the licensing offers on this resource (see below) |
ext | object | no | ResourceEntry.ext | free-form extension metadata; carries fields with no typed slot (e.g. previews[]) and domain-profile data |
ext_critical | string[] | no | ResourceEntry.ext_critical | keys in ext a consumer MUST understand (COSE crit pattern) |
attestations | array | no | ResourceEntry.attestations[] | signed attestations {verifier, keyid, attested_at, uri, claims, signature} |
resource_mutability is a typed ResourceEntry field. When present it is carried through to Offer.identity.resource_mutability; when omitted the Exchange defaults the Offer to RESOURCE_MUTABILITY_STATIC at Offer-build time (there is nothing to carry). An explicit RESOURCE_MUTABILITY_UNSPECIFIED is rejected (not_in:[0]); the Exchange reads the typed field, not ext. previews[] still has no typed field on ResourceEntry — it rides inside ext and is promoted to Offer.previews[] at discovery.
Term schema (one element = one LicenseTerm)
Section titled “Term schema (one element = one LicenseTerm)”| Field | Type | Maps to |
|---|---|---|
semantics | "enumerated" | "reference_only" | LicenseTerm.semantics |
functions | string[] | Restriction{kind=FUNCTION}.permitted[] |
prohibited_functions | string[] | Restriction{kind=FUNCTION}.prohibited[] |
user_types | string[] | Restriction{kind=USER_TYPE}.permitted[] |
geos | string[] | Restriction{kind=GEOGRAPHY}.permitted[] |
pricing | {model, unit, rate, currency} | LicenseTerm.pricing (model ∈ free | per_unit | flat) |
quotas | [{metric, limit, window}] | LicenseTerm.Quota[] |
obligations | [{kind, trigger, scope_license, detail}] | LicenseTerm.Obligation[] |
scopes | string[] | LicenseTerm.scopes (entitlement-gated, e.g. subscription:premium) |
Rules (enforced by protovalidate at the RPC boundary):
- Every term MUST carry
pricing— absence is not “free”, it is rejected. Usemodel: "free"explicitly (rate0). per_unitREQUIRESpricing.unit;flat/freecarry none.- A
reference_onlyterm MUST carrylicense.uri; anylicense.uriREQUIRESlicense.uri_digest. - Tokens are canonical registry values or
vendor:namespaced; unregistered bare tokens are a warning, not a reject.
All-or-nothing
Section titled “All-or-nothing”PushResources is atomic: if any entry fails a hard validation rule, the whole submission is rejected (InvalidArgument, naming the offending entry) and nothing is persisted. The publisher fixes and resubmits the full set. Membership / lint issues are returned in PushResourcesResponse.warnings[] and do not block.
Extension profiles in the feed
Section titled “Extension profiles in the feed”A publisher MAY place domain-profile data (e.g. CoMP) under ext. Profiles are recommendations for what keys to use, validated and rendered at discovery — see Extension Profiles. The licensing term is authoritative: for any field an offer owns (pricing above all), the Exchange renders from the selected term and shadows a conflicting ext value. So do not state pricing inside a profile ext payload; if you do, the offer carries the term’s price.
Worked example
Section titled “Worked example”{"domain":"example.com","path":"/articles/42","title":"…","content_id":"22392", "word_count":359,"estimated_quantity":474,"content_hash":"sha256:…","hash_method":"sha256", "source":"INGESTION_SOURCE_CMS_API","provenance_source":"example.com", "provenance_timestamp":"2026-06-18T09:23:45Z", "resource_mutability":"RESOURCE_MUTABILITY_STATIC", "terms":[ {"semantics":"enumerated","functions":["ai-input"], "pricing":{"model":"per_unit","unit":"accesses","rate":0.05,"currency":"USD"}, "obligations":[{"kind":"attribution","trigger":"on_use"}]} ]}One resource, one priced term: an AI-input license at $0.05/access with an attribution obligation, plus content-integrity and mutability metadata that surface on the discovered Offer.