Publisher API

Your product lists client sites

Industry standard for this space: OAuth 2.0 client credentials (RFC 6749), domain control validation (Search Console / ACME), REST write, HMAC webhooks, OpenAPI, and an authorized-sellers directory. The client frontend never holds the secret. MCP cannot publish.
  1. 01

    Register a publisher product

    Sign in at /publishers. You receive client_id + bc_pub_ secret. CiteFleet is already issued. Extra products can also be set with BOTCENTRAL_PUBLISHER_KEYS. A read partner key is not enough.

  2. 02

    Exchange client credentials for a Bearer

    POST /oauth/token with grant_type=client_credentials. Or send the bc_pub_ secret as Authorization: Bearer (Stripe-style). Discovery is /.well-known/oauth-authorization-server.

  3. 03

    Mint a verify token for the client

    POST /v1/publisher/verify-tokens { domain }. Show the client: DNS TXT botcentral-verify=<token> or a plain-text file at /.well-known/botcentral.txt. SPA HTML at that path is rejected.

  4. 04

    Wait until the origin is proven

    POST /v1/probe with the domain and token. Do not PUT until proof is dns-txt or well-known-file. Probe is public; it does not list the site.

  5. 05

    PUT the 1.1 card

    PUT /v1/publisher/sites/{domain}. BotCentral fetches the origin again. If proof fails you get 409 and nothing is listed.

  6. 06

    Listen for the result

    HTTPS webhook site.listed / site.unpublished / site.reverified, HMAC sha256. Also poll GET /v1/changes. The public card shows listed_by so assistants know which publisher wrote it.

OAuth token

curl -sS https://botcentral.org/oauth/token \
  -H 'content-type: application/x-www-form-urlencoded' \
  -d 'grant_type=client_credentials&client_id=bc_cid_...&client_secret=bc_pub_...&scope=publisher'

PUT a client site

curl -sS -X PUT https://botcentral.org/v1/publisher/sites/client.example \
  -H "authorization: Bearer $ACCESS" \
  -H "content-type: application/json" \
  -d '{
    "domain": "client.example",
    "canonical": "https://client.example/",
    "name": "Client",
    "summary": "What the site is.",
    "topics": ["example"],
    "allow_bots": true,
    "allow": ["ChatGPT-User","OAI-SearchBot","Claude-User"],
    "deny": ["GPTBot","ClaudeBot"],
    "pointers": {
      "robots": "https://client.example/robots.txt",
      "sitemap": "https://client.example/sitemap.xml",
      "llms": "https://client.example/llms.txt"
    },
    "pages": [{ "url": "https://client.example/", "rel": "home", "title": "Home" }],
    "verifyToken": "bc-verify-acme-ab12cd34",
    "consent": { "retrieve": true, "train": false, "act": false, "cite": true, "tdm": "reserved" }
  }'

Unpublish: DELETE /v1/publisher/sites/{domain}. Re-check: POST /v1/publisher/sites/{domain}/reverify. Who am I: GET /v1/publisher/me. Your listed clients: GET /v1/publisher/sites. Directory: GET /v1/publishers. Legacy POST /internal/publish still works for CiteFleet.

Connect has the copy-paste. /publishers mints the credentials. Get listed is the owner-facing version of the same proof.