Provider Onboarding
Overview
Section titled “Overview”Provider onboarding has three steps:
- Domain verification — prove you control the domain (ACME HTTP-01 pattern)
- CDN signing key provisioning — get signing keys into the Exchange
- Catalog contributor authorization — declare who can push resource metadata on your behalf
Both steps can be completed in under an hour using fora-cli.
Two Deployment Models
Section titled “Two Deployment Models”Model A: Exchange-Managed (Default)
Section titled “Model A: Exchange-Managed (Default)”The provider grants CDN access during onboarding. The Exchange generates keys, configures the CDN, and rotates keys automatically on a 90-day schedule.
Best for: Single-exchange providers who want zero operational overhead.
What you provide: IAM role (CloudFront), API credentials (Fastly).
Model B: Provider-Managed (Opt-In)
Section titled “Model B: Provider-Managed (Opt-In)”The provider generates keys locally, pushes them to each Exchange via fora-cli, and configures their own CDN.
Best for: Multi-exchange providers who need control over key distribution.
Domain Verification Flow
Section titled “Domain Verification Flow”The flow adapts the ACME HTTP-01 challenge used by Let’s Encrypt.
fora-cli Exchange Provider CDN | | | | 1. RequestDomainVerification | | { domain: "example.com" } | |------------------------------> | | | Generate challenge token | | | | | 2. DomainVerificationChallenge | | { token: "abc123...", | | expires_at: +10min } | |<------------------------------ | | | | | 3. Place token at | | /.well-known/fora-verify/{token} | |----------------------------------------------------> | | | | | 4. ConfirmDomainVerification | |------------------------------> | | | 5. Fetch verification_url| | |----------------------------> | | 6. Verify token + check | | | fora.json | | | | | 7. DomainVerificationResult | | { verified: true, key_id: "k-2026-03" } | |<------------------------------ |Double protection: The Exchange checks both the ACME challenge (domain control) and fora.json authorization (exchange authorization). Both must pass.
fora-cli Commands
Section titled “fora-cli Commands”Generate a signing key
Section titled “Generate a signing key”# A code-capable edge (Cloudflare, Fastly, Lambda@Edge) verifies Ed25519fora-cli key generate --cdn edge-ed25519
# AWS CloudFront verifies RSA natively, via a trusted key groupfora-cli key generate --cdn cloudfront --algorithm rsa-2048Push key to an Exchange
Section titled “Push key to an Exchange”Includes ACME domain verification automatically:
fora-cli key push \ --exchange mp.ssp.com \ --domain techcrunch.com \ --cdn cloudfront \ --public-key ./fora-cloudfront-public.pemThe push carries the public half only — it is what
DomainVerificationConfirmation.signing_key
registers. The Exchange signs delivery URLs, so under Model B it also needs the
private half; that is delivered to the Exchange operator through their key
provisioning channel, never through this RPC. Under Model A the Exchange
generates both halves itself and the question does not arise.
Output:
Requesting domain verification for techcrunch.com...Challenge token: abc123def456Placing token at /.well-known/fora-verify/abc123def456 -> Edge function KV write: OKConfirming domain verification... -> Domain verified: techcrunch.com -> Key registered: k-2026-03-18-cf -> Valid until: 2026-06-18T00:00:00ZDone.Rotate a key
Section titled “Rotate a key”fora-cli key rotate \ --exchange mp.ssp.com \ --cdn cloudfront \ --drain-minutes 10Four-phase zero-downtime rotation: PREPARE (generate new key, push to CDN) -> ACTIVATE (switch signer) -> DRAIN (wait for old URLs to expire) -> CLEANUP (remove old key).
Check key status
Section titled “Check key status”fora-cli key status --exchange mp.ssp.com --domain techcrunch.comEmergency revocation
Section titled “Emergency revocation”fora-cli key revoke --exchange mp.ssp.com --key-id k-2026-03-18-cfConfigure fora.json
Section titled “Configure fora.json”Set up /.well-known/fora.json to authorize Exchanges and catalog contributors:
{ "ver": "1.0", "provider": "nytimes.com", "contact": "licensing@nytimes.com", "exchanges": [ { "domain": "mp.ssp-alpha.com", "endpoint": "https://mp.ssp-alpha.com/fora/v1", "relationship": "DIRECT" } ], "catalog_contributors": [ { "domain": "doubleverify.com", "relationship": "verifier" }, { "domain": "gumgum.com", "relationship": "verifier" } ], "supported_profiles": ["fora-news-v1"]}The catalog_contributors field authorizes third parties to push attestations and catalog metadata for your resources via CatalogService.PushResources.
Push Your Catalog with the SDK
Section titled “Push Your Catalog with the SDK”Whatever your source material — a JSONL feed, a sitemap, an RSL file, a crawl, a CMS plugin — it converges on one ResourceEntry per resource, and the SDK owns the rest: the shape, the Exchange’s own validation run for you to call before you push, and the push itself.
- Build a
ResourceEntryper resource:domain(your bare host),path(an absolute URL path), the metadata you have, and one or moreterms— every term carries apricing(model: FREEis stated, never implied). - Pre-check it with
validate_resource_entry(Python),validateResourceEntry(TypeScript) orhelpers.ValidateResourceEntry(Go). The verdict lists every rule the Exchange would refuse the entry on, with its field path, and the warnings the accepted terms would carry — the exact strings that come back inPushResourcesResponse.warnings. Token spellings and aliases (generative-ai→ai-input,de→DE) are canonicalised for you — which is also why a term must stay disjoint after canonicalisation:permitted: ["scrape"]withprohibited: ["crawl"]names one token on both sides and is refused. - Push with the catalog client —
fora_sdk.client.CatalogClient,createCatalogClient,connect.NewCatalogClient— built against the CatalogService address you have configured for that Exchange, and your contributor signing key. The address is configuration, the way the agent client’s home Exchange is: nobody but you named it, so the client dials only what you gave it. An Exchange MAY advertise the address ascatalog_endpointin its/.well-known/fora.json, and a deployment that reads it from there MUST itself check the binding that field states — same host and port as the manifest, or a subdomain of that host on that port, no userinfo — before dialling it; a manifest naming an unrelated host would otherwise redirect a signed push to a party the signature never covered. Setexchangeto the Exchange’s bare domain,tenant_idto your tenant andcaller_idto your domain; the client stampsver, signs the request with your key and refuses before signing a request whoseexchangeis not a bare domain — the shape the wire rule admits, which is narrower than a usable host.
A push is all-or-nothing: an entry that fails a hard rule refuses the whole submission with the offending entry named, and nothing is persisted. Warnings never block. The pre-check is advice about what the Exchange will say; the Exchange re-runs both tiers on every push and its verdict decides. Worked examples in all three languages are in Source 7: CatalogService API Push; the feed format is in JSONL Ingestion Feed.
Self-Attestation Keys (Optional)
Section titled “Self-Attestation Keys (Optional)”If you want to self-attest resources (Level 1 verification), publish your Ed25519 signing keys in your WBA directory — a JWK Set served at /.well-known/http-message-signatures-directory. Keys live here, not in fora.json, and each is identified by its RFC 7638 thumbprint (the RFC 9421 keyid) rather than a label:
// GET /.well-known/http-message-signatures-directory// Content-Type: application/jwk-set+json{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "use": "sig", "alg": "EdDSA", "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", "not_before": "2026-01-01T00:00:00Z", "not_after": "2026-12-31T23:59:59Z" } ]}The claims schema stays in your fora.json (role=ROLE_PUBLISHER, the same file as the previous step) under ext["fora.attestation.claims_schema"]:
{ "ver": "1.0", "role": "ROLE_PUBLISHER", "domain": "nytimes.com", "ext": { "fora.attestation.claims_schema": { "content_hash": { "type": "string", "description": "SHA-256 hash of delivered content bytes" }, "estimated_quantity": { "type": "integer", "description": "Token count computed by NYT CMS" } } }}See Content Attestation for the full attestation system design.
CDN Key Types
Section titled “CDN Key Types”| Delivery endpoint | cdn_type | Key Type | Algorithm |
|---|---|---|---|
| Edge function (Cloudflare, Fastly, Lambda@Edge) | edge-ed25519 | Asymmetric | Ed25519 |
| AWS CloudFront | cloudfront | Asymmetric | RSA-2048 |
Both are asymmetric, so the separation is universal: the Exchange holds the private key for signing and the delivery endpoint holds only the public key for verification. What differs is whether the delivery endpoint can run code — and therefore whether it can enforce agent identity binding. CloudFront verifies natively but cannot, so its URLs are bearer credentials; an edge function does both.
Onboarding Checklist
Section titled “Onboarding Checklist”| Step | Action | Time |
|---|---|---|
| 1 | Deploy FORA edge function on CDN | 15 min |
| 2 | Configure fora.json with authorized Exchange(s) | 5 min |
| 3 | Generate and push signing key (or grant CDN access) | 5—10 min |
| 4 | (Optional) Add catalog_contributors to fora.json | 5 min |
| 5 | (Optional) Publish self-attestation keys in your WBA directory | 10 min |
| Total | ~30—45 min |
Next Steps
Section titled “Next Steps”- Content Attestation — set up content attestation after onboarding
- For Providers — provider value proposition overview
- Proto: FORA v1 —
DomainVerification*message reference