Skip to content

Provider Onboarding

Provider onboarding has three steps:

  1. Domain verification — prove you control the domain (ACME HTTP-01 pattern)
  2. CDN signing key provisioning — get signing keys into the Exchange
  3. Catalog contributor authorization — declare who can push resource metadata on your behalf

Both steps can be completed in under an hour using fora-cli.

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).

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.

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.

Terminal window
# A code-capable edge (Cloudflare, Fastly, Lambda@Edge) verifies Ed25519
fora-cli key generate --cdn edge-ed25519
# AWS CloudFront verifies RSA natively, via a trusted key group
fora-cli key generate --cdn cloudfront --algorithm rsa-2048

Includes ACME domain verification automatically:

Terminal window
fora-cli key push \
--exchange mp.ssp.com \
--domain techcrunch.com \
--cdn cloudfront \
--public-key ./fora-cloudfront-public.pem

The 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: abc123def456
Placing token at /.well-known/fora-verify/abc123def456
-> Edge function KV write: OK
Confirming domain verification...
-> Domain verified: techcrunch.com
-> Key registered: k-2026-03-18-cf
-> Valid until: 2026-06-18T00:00:00Z
Done.
Terminal window
fora-cli key rotate \
--exchange mp.ssp.com \
--cdn cloudfront \
--drain-minutes 10

Four-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).

Terminal window
fora-cli key status --exchange mp.ssp.com --domain techcrunch.com
Terminal window
fora-cli key revoke --exchange mp.ssp.com --key-id k-2026-03-18-cf

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.

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.

  1. Build a ResourceEntry per resource: domain (your bare host), path (an absolute URL path), the metadata you have, and one or more terms — every term carries a pricing (model: FREE is stated, never implied).
  2. Pre-check it with validate_resource_entry (Python), validateResourceEntry (TypeScript) or helpers.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 in PushResourcesResponse.warnings. Token spellings and aliases (generative-aiai-input, deDE) are canonicalised for you — which is also why a term must stay disjoint after canonicalisation: permitted: ["scrape"] with prohibited: ["crawl"] names one token on both sides and is refused.
  3. 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 as catalog_endpoint in 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. Set exchange to the Exchange’s bare domain, tenant_id to your tenant and caller_id to your domain; the client stamps ver, signs the request with your key and refuses before signing a request whose exchange is 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.

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.

Delivery endpointcdn_typeKey TypeAlgorithm
Edge function (Cloudflare, Fastly, Lambda@Edge)edge-ed25519AsymmetricEd25519
AWS CloudFrontcloudfrontAsymmetricRSA-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.

StepActionTime
1Deploy FORA edge function on CDN15 min
2Configure fora.json with authorized Exchange(s)5 min
3Generate and push signing key (or grant CDN access)5—10 min
4(Optional) Add catalog_contributors to fora.json5 min
5(Optional) Publish self-attestation keys in your WBA directory10 min
Total~30—45 min