Connect products and agents to BMJ content through one stable contract.
The Content Gateway is a governed, customer-scoped read API over BMJ's open corpus: article and collection metadata, lexical and semantic search, grounded and rolling answers, and entitled canonical paragraph chunks. An access package defines what an application can do and whether it can connect through REST, MCP or both. Each API key may narrow those operations and connection methods; an MCP-enabled key may additionally narrow its named tools. Authentication performs reads only, and billable work is reserved and then settled or refunded.
Connect to this environment
| Hostname | Audience | Notes |
|---|---|---|
https://dev.bmj-content-hub.com | REST /v1/*, /mcp, documentation, showcase and administration | Use an API key issued in this environment. Dev and live have separate content, credentials and quotas. Browser challenges protect the administrator login; machine endpoints use authentication and request limits. |
Isolated dev JATS path
The original dev pilot used 100 JNIS and 25 OA JATS articles; the JNIS private stage now also has a daily Content Pipeline JATS feed. Read the pilot ingestion, D1 BM25 search, collection-scope and complete-delivery contract in the dated dev architecture document. Current feed operation is described in the protected administrator guide.
| Use case | Current article interface |
|---|---|
| Ranked candidates | GET /v1/articles?query=… — any unquoted term, quoted phrase units, BM25, filters before pagination. |
| Find an author's paper | Add search_mode=bibliographic for a name/year query, or explicit author, year and date_basis. Interpretation guide. |
| Complete abstracts and metadata | include=frontmatter, or ordered GET /v1/frontmatter?ids=… batches. |
| Whole article with references | GET /v1/articles/:id?format=jats|json|markdown, subject to content permissions and explicit size limits. |
| Agent interface | MCP search (auto interpretation) and fetch (whole Markdown article). |
The JATS profile uses lexical search with stemming and UK/US normalization. It excludes reference-only topic matches, does not guess typos or silently relax queries, and makes no embedding or generation calls. rank=vector and rank=hybrid are reserved and return 501.
Current diagrams: C4 context · C4 containers · Ingestion and delivery sequence. Wider legacy evidence and answer interfaces are documented below.
Open the synthetic public demo tour · Download the allowlisted demo bundle. The bundle has no API key and no JNIS source data; use the local demo server for guided recordings. The server retains the latest recording snapshot until it is overwritten or deleted; no automatic expiry is implemented.
Authentication and effective access
Customer and agent calls send Authorization: Bearer bmjp_…. The key is shown once at issuance; store it as a secret, never in a URL, prompt, log or source file. A call succeeds only when every applicable layer allows it:
- Package operation. The customer's package grants Browse, Find, Search, Answers, Rolling answers and/or Source chunks. Their internal capabilities are snapshotted as entitlements onto the active agreement.
- Connection method. The package permits REST, MCP or both; the key may select only a subset through
api_key_transports. - Key operation.
api_key_operationsnarrows what this application may do. The stored low-level scopes are derived from these selected operations as a defence-in-depth ceiling. - Content grant. The customer's contract grants each BMJ content release a level:
metadata<preview<chunk. A higher level always includes the lower ones. - Agreement collections. The active agreement snapshots the collections this customer can access. Every article read is intersected with that boundary.
- MCP tool. For MCP, the requested tool must also be explicitly selected for this API key; otherwise it is absent from
tools/list.
The full matrix, generated directly from the server's own access table so it cannot drift from this page:
| Application operation | Modes | Internal capability | Derived scope | Grant level | MCP tools |
|---|---|---|---|---|---|
Customer contextGET /v1/customer/context |
— | none | customer:read |
none | — |
Quota checkPOST /v1/customer/quota-check |
— | none | quota:check |
none | — |
BrowseGET /v1/documents, /v1/documents/:id, /v1/collections*, /v1/customer/corpus-summary, /v1/articles/:id/trust |
— | retrieve:metadata |
customer:read |
metadata |
list_articles, get_article, list_collections, get_collection, list_collection_articles, get_article_trust, list_trust_markers |
FindPOST /v1/find |
article, paragraph | search:lexical |
search:evidence |
metadata |
find_articles, find_passages, search_articles, search |
SearchPOST /v1/search |
keyword, vector, hybrid | retrieve:preview |
search:evidence |
preview |
— |
AnswerPOST /v1/answers |
json, stream | answer:generate |
answer:generate |
preview |
— |
Rolling answersPOST /v1/rolling-answers |
— | answer:rolling |
answer:generate |
preview |
— |
Read sourceGET /v1/chunks/:id |
— | source:chunk:read |
source:chunk:read |
chunk |
— |
Read full articleGET /v1/articles/:id/content |
— | source:chunk:read |
source:chunk:read |
chunk |
get_article_content, fetch |
GET /v1/customer/context returns the key's operation_ids, transports and mcp_tools, plus the same evaluated access matrix — one row per operation with plan, key, grant_ok and a combined effective flag. Call it before attempting a mode to check what will work, rather than discovering a 403 mid-task.
curl 'https://dev.bmj-content-hub.com/v1/customer/context' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}"
{
"ok": true,
"customer": { "id": "cus_...", "name": "...", "status": "active" },
"api_key": {
"id": "key_...",
"operation_ids": ["catalogue", "find", "search"],
"transports": ["rest", "mcp"],
"mcp_tools": ["list_articles", "find_articles"],
"scopes": ["customer:read", "quota:check", "search:evidence"]
},
"entitlements": ["retrieve:metadata", "search:lexical", "retrieve:preview"],
"content_grants": [{ "content_release_id": "...", "access_level": "preview" }],
"content_scope": { "journal_ids": [], "published_from": null, "published_to": null },
"access": [
{ "id": "find", "path": "/v1/find", "capability": "search:lexical", "scope": "search:evidence", "grant": "metadata", "plan": true, "key": true, "grant_ok": true, "effective": true },
{ "id": "chunks", "path": "/v1/chunks/:id", "capability": "source:chunk:read", "scope": "source:chunk:read", "grant": "chunk", "plan": false, "key": false, "grant_ok": false, "effective": false }
]
}
Modes
Five request-response modes plus MCP tool calls, each gated by its own row in the matrix above.
Find: lexical search, article level
POST /v1/find with level: "article" runs D1 FTS5 BM25 over titles and abstracts. Quote a phrase for an exact match. Fast (100–300 ms) and does not touch the retrieval/generation stack.
curl -X POST 'https://dev.bmj-content-hub.com/v1/find' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}" \
-H 'Content-Type: application/json' \
-H 'X-Request-Id: client-generated-unique-id' \
--data '{
"query": "\"treatment adherence\"",
"level": "article",
"limit": 10
}'
{
"ok": true,
"request_id": "req_...",
"level": "article",
"query": "\"treatment adherence\"",
"hits": [
{ "document_id": "doc_...", "title": "...", "snippet": "...<b>treatment adherence</b>...", "score": 4.9, "journal_id": "bmjonc", "published_at": "2026-03-01", "doi": "10...." }
],
"total": 12,
"next_cursor": 10,
"timings_ms": { "fts": 42, "total": 61 }
}
Search: keyword, vector, hybrid
POST /v1/search retrieves ranked evidence previews. retrieval selects keyword (D1 FTS5), vector (Workers AI query embedding against Vectorize) or hybrid (both, fused by reciprocal rank). An optional backend field overrides the gateway/AI-Search stack per request; results are memoised 5 minutes per query, scope and release.
curl -X POST 'https://dev.bmj-content-hub.com/v1/search' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}" \
-H 'Content-Type: application/json' \
-H 'X-Request-Id: client-generated-unique-id' \
--data '{
"query": "What evidence discusses treatment adherence?",
"max_results": 5,
"retrieval": "keyword"
}'
curl -X POST 'https://dev.bmj-content-hub.com/v1/search' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}" \
-H 'Content-Type: application/json' \
-H 'X-Request-Id: client-generated-unique-id' \
--data '{
"query": "What evidence discusses treatment adherence?",
"max_results": 5,
"retrieval": "vector"
}'
curl -X POST 'https://dev.bmj-content-hub.com/v1/search' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}" \
-H 'Content-Type: application/json' \
-H 'X-Request-Id: client-generated-unique-id' \
--data '{
"query": "What evidence discusses treatment adherence?",
"max_results": 5,
"retrieval": "hybrid",
"rerank": true
}'
Answers: JSON
POST /v1/answers retrieves, assembles bounded evidence context, generates with Workers AI and verifies citations. Requires the answer:generate capability and scope, a preview-or-higher content grant, and evidence inside the agreement collections. Empty retrieval returns HTTP 404 no_evidence rather than an empty or hallucinated answer.
curl -X POST 'https://dev.bmj-content-hub.com/v1/answers' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}" \
-H 'Content-Type: application/json' \
-H 'X-Request-Id: client-generated-unique-id' \
--data '{"query":"What does the evidence say about treatment adherence?"}'
{
"ok": true,
"request_id": "req_...",
"answer": "Evidence suggests ... [E1].",
"grounding": { "valid": true, "warnings": [] },
"sources": [ { "evidence_id": "para_...", "score": 0.81, "text": "..." } ],
"execution_context": { "retrieval": "hybrid", "generator_model": "@cf/zai-org/glm-4.7-flash", "generator_provider": "cloudflare-workers-ai" },
"usage": { "answer_requests": 1, "model_1k_tokens": 2 },
"timings_ms": { "retrieval": 210, "generation": 2480, "total": 2810 }
}
MCP: tool discovery and a tool call
The MCP endpoint is https://dev.bmj-content-hub.com/mcp. Both the access package and the presenting key must permit MCP. It uses the same bearer key, burst limiter, quota and audit as the REST routes, and needs no session: every request is self-contained. Send Accept: application/json, text/event-stream so the server can choose a complete response or a request-scoped stream. The new article pilot provides search_articles and get_article_content, subject to key grants; legacy read-only tools remain available to existing clients. tools/list exposes only tools selected for the key and allowed by its operation set. Unselected tools cannot execute.
tools/list
Discover the tools available to the authenticated customer before calling one.
curl -X POST 'https://dev.bmj-content-hub.com/mcp' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}" \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
--data '{
"jsonrpc": "2.0",
"id": "tools-1",
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "BMJ quick-start",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}'
Chunks: canonical source text
GET /v1/chunks/:evidence_id returns a stable BMJ paragraph or evidence-unit chunk, gated by the chunk content-grant level and source:chunk:read on plan and key. Accepts a para_… id from find_passages/search results or an ev_… evidence-unit id.
curl 'https://dev.bmj-content-hub.com/v1/chunks/para_0123456789abcdef01234567' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}"
{
"ok": true,
"request_id": "req_...",
"chunk": { "evidence_id": "para_...", "text": "...", "locator": { "kind": "paragraph", "ordinal": 4 }, "citation": { "doi": "10....", "journal_id": "bmjonc" }, "token_count": 210 },
"usage": { "licensed_content_1k_tokens": 1 }
}
Discover the permitted corpus
GET /v1/customer/corpus-summary reports scoped content-type, journal, date, document, evidence and topic coverage. GET /v1/documents browses only documents admitted to the customer's release and contract scope.
curl 'https://dev.bmj-content-hub.com/v1/customer/corpus-summary' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}"
curl 'https://dev.bmj-content-hub.com/v1/documents?journal_id=bmjonc&limit=25' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}"
Inspect article quality signals
The BMJ Article Trust Profile v1 combines versioned journal defaults with article overrides. Every assertion is typed and carries evidence, provenance and an explicit inherited flag. The schema and marker registry are public and require no key.
curl 'https://dev.bmj-content-hub.com/v1/trust/schema'
curl 'https://dev.bmj-content-hub.com/v1/trust/markers'
curl 'https://dev.bmj-content-hub.com/v1/trust/articles/doc_example'
curl 'https://dev.bmj-content-hub.com/v1/articles/doc_example/trust' \
-H "Authorization: Bearer ${BMJ_EVIDENCE_API_KEY}"
Errors and quota
| Status / code | Meaning | Agent action |
|---|---|---|
400 invalid_request | Body failed schema validation; response carries a field-level issues array | Fix the named fields; do not retry unchanged. |
401 unauthorized | Missing, invalid or revoked key | Stop and request a valid customer key. |
403 insufficient_scope | The API key's selected operation or derived scope does not cover this request | Ask an administrator to issue a key with the required operation. |
403 entitlement_denied | The package operation, connection method or content grant does not cover this request | Ask an administrator to review the customer's package or content grant. |
404 no_evidence | /v1/answers found no evidence inside content scope | Treat as a valid empty result; try broader terms or check corpus scope. Do not fall back to another corpus. |
429 rate_limited | Per-key burst limit tripped (30 req/10s) | Respect Retry-After: 10; do not rotate keys. |
429 quota_denied | Per-minute or per-day customer quota exhausted | Respect Retry-After (60s minute limit, 86400s day limit). |
503 d1_budget_exhausted / d1_budget_guard_unavailable | The global D1 row budget guard tripped, or could not be reached | Retry later; this is a global, not per-customer, stop. |
503 search_unavailable / answer_unavailable | The retrieval or generation pipeline failed; the reservation was refunded | Retry once with the same request ID, then report the code. |
{ "ok": false, "code": "rate_limited", "error": "..." }
# HTTP 429, Retry-After: 10
{ "ok": false, "code": "quota_denied", "request_id": "req_...", "decision": { "allowed": false, "reason": "minute_limit" } }
# HTTP 429, Retry-After: 60 (minute_limit) or 86400 (day_limit)
{
"ok": false,
"code": "invalid_request",
"error": "Invalid request body: query (body) String must contain at least 2 character(s)",
"issues": [ { "path": "query", "message": "String must contain at least 2 character(s)" } ]
}
{ "ok": false, "code": "no_evidence", "error": "No evidence inside your content scope matched the question...", "request_id": "req_..." }
# HTTP 404
{ "ok": false, "code": "d1_budget_exhausted", "error": "..." }
# HTTP 503
Request IDs and idempotency
Send X-Request-Id (8–128 safe characters) on POST /v1/find, /v1/search, /v1/answers and /v1/rolling-answers. Reuse the exact same ID only to retry the exact same logical request: the customer usage gate reserves and settles by request ID, so a retried ID with the same meter returns the original decision rather than double-charging. Omit it and the server generates one, returned as request_id in every response for correlation with logs and the request-audit record.
Usage counting and request retention
How usage is counted: customer and API-key views show settled meter units. Document and evidence-unit views show distinct settled request exposure, preventing a grounded answer's answer and token settlements from being counted as separate content uses.
Request-text retention: authenticated search and grounded-answer text, sanitized options, outcomes and evidence IDs are retained for 30 days in the separate request-audit service. Bearer tokens and arbitrary HTTP headers are never sent to it. Do not put credentials or personal data in a query.
Python client and agent skill
The typed bmj_content_gateway Python client (clients/python) wraps the REST API, the admin write API and the MCP transport, with bounded retry/backoff honouring Retry-After and typed exceptions carrying .code, .details and .retry_after. Use the workers.dev origin as its base URL.
Agent authors: download the focused agent skill. It tells an agent which mode to use for which need (find vs. search vs. answers), how to read the access array from /v1/customer/context before attempting a mode, and how to handle quota and cite execution provenance without learning physical Cloudflare bindings.
JATS dev pilot guide · Partner article API · Stateless MCP connection guide · Agent UI metadata · OpenAPI JSON