For developers
API documentation
Everything a system needs to read a business's verified records. JSON-RPC and REST over HTTPS, bearer-token auth, no SDK required. The machine-readable version of this page is at /docs/openapi.json.
The one property worth designing around
When a question cannot be answered from what a publisher has actually published, this API
returns coverage.status = not_covered — it does not guess. A refusal is information: it means
the fact was never published, not that it is unknown to the business. Treat it as a signal, not an error,
and do not fall back to inference.
Authentication
Public documents — discovery, crawl pages, feeds — need nothing. The query endpoint takes a bearer token unless the publisher has enabled anonymous reads:
Authorization: Bearer kc_live_<prefix>_<secret>
Publishers create and rotate their own keys in Developer settings. Keys are rate-limited
per plan; exceeding the limit returns 429 with a Retry-After header. A revoked key
stops working within the tenant cache TTL, not instantly.
A first call
curl -s https://x-protocols.com/t/{publisher}/mcp \
-H 'content-type: application/json' \
-H 'authorization: Bearer kc_live_...' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Verifying a response
Every answer carries an Ed25519 signature over the whole envelope. Fetch the publisher's keys from their
JWKS document, match the kid in the envelope, and check the signature — about fifteen lines in
any language. Worked example: /docs/verify.
Retired keys stay published, so an envelope signed before a rotation stays verifiable.
Always match on kid; never assume the first key in the set.
Outbound webhooks
Instead of polling for changes, a publisher's own systems can be told the moment their catalogue changes. Subscriptions are created in Developer settings — no route here creates one; this section is for whatever receives the POST.
One delivery per write call, not per record: a bulk import of a thousand rows fires one
record.upserted event carrying all of them, not a thousand deliveries.
Event types and payload
record.upserted | Fires once per write call.
{"created": N, "updated": N, "items": [{"type_key","external_id","title"}, ...]} |
record.deleted | {"type_key": "...", "external_id": "..."} |
Headers
X-KC-Timestamp | Unix seconds when this delivery attempt was signed. |
X-KC-Signature | sha256=<hex> — see below. |
X-KC-Event-Type | record.upserted or record.deleted. |
Idempotency-Key | Stable per delivery row. A retry of the same event reuses it — the safe way to de-duplicate if a prior attempt's response was lost after we received it. |
Verifying a delivery
HMAC-SHA256, hex-encoded, prefixed sha256= — the same scheme in both directions, so a verifier
already written for an inbound live-data feed push can be reused here unchanged:
signature = "sha256=" + hex( HMAC_SHA256(secret, timestamp + "." + raw_request_body) )
Sign the exact bytes of the request body, before any JSON re-serialization — re-encoding first is the most common reason a verifier disagrees with a correct signature. The secret is shown once, at subscription creation, in Developer settings.
Retries and auto-disable
A non-2xx response or a connection failure is retried with backoff starting at 1 minute and doubling up to a 15-minute cap, for up to 8 attempts total before that one event is given up on. A subscription whose endpoint exhausts every attempt on 5 consecutive events is automatically disabled — visible and re-enableable in Developer settings, rather than silently retrying a dead endpoint forever.
Endpoints
Query
POST /t/{publisher}/mcp
API key
Ask this publisher a question
MCP over Streamable HTTP, JSON-RPC 2.0. Call tools/list to see what this publisher offers, then tools/call. Every answer carries a coverage status, a freshness contract and an Ed25519 signature. When a question cannot be answered from published records the response says not_covered rather than guessing.
POST /mcp
API key
Same, with the publisher taken from the Host header
Identical to the above. Use this form when a publisher is reached on its own hostname rather than a /t/{publisher} path.
Discovery
GET /t/{publisher}/.well-known/x-protocol/registry.json
public
What this publisher is and can be asked
The discovery document: identity, available tools, protocol version, and links to every other document here.
GET /t/{publisher}/.well-known/x-protocol/manifest.json
public
The AI manifest
What data this publisher offers, in which formats, where the endpoints are, how authenticity is checked and when the data was last updated.
GET /t/{publisher}/.well-known/x-protocol/jwks.json
public
Public signing keys
Every key this publisher has ever signed with, active first. Retired keys stay published so an envelope signed months ago stays verifiable after a rotation. Match on the kid in the envelope, never on position.
GET /t/{publisher}/.well-known/x-protocol/revocations.json
public
Withdrawn records
An empty list is a positive assertion: nothing you hold was withdrawn. For a publisher that does not exist this returns 404, never an empty list, so you can tell the two apart.
Public data
GET /b/{publisher}
public
The publisher's public page
Server-rendered HTML, no JavaScript required. Send Accept: application/json to get the same content as JSON.
GET /b/{publisher}/llms.txt
public
Model-oriented index
A markdown index written for models: what this publisher publishes, which questions need context, every endpoint, and how to verify any of it.
GET /b/{publisher}/dump.jsonl
public
Bulk export
Every published record, one JSON object per line. The cheapest way to ingest a publisher wholesale.
GET /b/{publisher}/sitemap.xml
public
Sitemap for this publisher
One URL per published record.
GET /b/{publisher}/{type}/{id}
public
One record
A permanent URL per record. Accept: application/json works here too.
GET /b/{publisher}/verify
public
What was verified, and when
Human-readable proof behind the trust badge.
GET /llms.txt
public
Platform-wide model index
Every publisher, and how to read them.
GET /sitemap.xml
public
Platform sitemap
Index of every publisher's sitemap.
Live data
POST /ingest/{stream}
HMAC signed
Push live data (stock, prices, free slots)
HMAC-SHA256 over "{timestamp}.{body}" in X-KC-Signature, with X-KC-Timestamp. Identify the publisher with X-KC-Publisher. Idempotent per idempotency key. For publishers without a developer there is a pull alternative: publish an XML feed and register its URL instead — see /docs/live-data-feed.
Syndication
GET /t/{publisher}/syndication/acp/products.jsonl
public
ACP product feed
OpenAI/Stripe Agentic Commerce Protocol shape, one Product per line. ETag and gzip supported. Publishers who sell nothing return 404 not_syndicated, which is correct rather than an error.
GET /t/{publisher}/syndication/acp/header.json
public
ACP feed metadata
Feed-level metadata for the above.
GET /t/{publisher}/.well-known/ucp
public
UCP business profile
Public and unauthenticated by the protocol's own requirement.
GET /t/{publisher}/ucp/v1/products
public
UCP live catalogue search
Never cached; answers from live data where the publisher publishes it.
Operations
GET /healthz
public
Liveness
200 when the process is up.
GET /readyz
public
Readiness
200 when dependencies (database, engine, Redis) are reachable.
Errors
401 | Missing or invalid API key. |
402 | The publisher's plan does not include this capability, or a quota is exhausted. |
404 | No such publisher, or the publisher has not proved domain control. Both look identical on purpose — confirming that an unverified publisher exists is free reconnaissance. |
429 | Rate limited. Honour Retry-After. |
A not_covered answer is a 200, not an error. It is the API
working as designed.
Also worth reading
- How to check a signature yourself
- Live data feed format — for publishers with no developer
- openapi.json — this page, machine-readable