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:

request
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

list what a publisher offers
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.upsertedFires once per write call. {"created": N, "updated": N, "items": [{"type_key","external_id","title"}, ...]}
record.deleted{"type_key": "...", "external_id": "..."}

Headers

X-KC-TimestampUnix seconds when this delivery attempt was signed.
X-KC-Signaturesha256=<hex> — see below.
X-KC-Event-Typerecord.upserted or record.deleted.
Idempotency-KeyStable 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:

what X-KC-Signature is
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

401Missing or invalid API key.
402The publisher's plan does not include this capability, or a quota is exhausted.
404No such publisher, or the publisher has not proved domain control. Both look identical on purpose — confirming that an unverified publisher exists is free reconnaissance.
429Rate limited. Honour Retry-After.

A not_covered answer is a 200, not an error. It is the API working as designed.

Also worth reading