Searching the documentation programmatically
GET /api/public/docs/search?q=… searches the public corpus and returns brief records — never page bodies. It needs no key and holds no session. Filters for kind, status and channel apply server-side, limit runs 1 to 25, and the same rate limit of 240 requests a minute per address covers every route under /api/public/.
The request#
GET /api/public/docs/search?q=held+draft+approval&limit=5&status=available HTTP/1.1
Host: connectbyjbrh.com| Name | Rule | Effect |
|---|---|---|
q | Required, 2–200 characters | The query. Below two characters the request is rejected by validation, not answered emptily |
limit | 1–25, default 10 | Result count. Values above 25 are refused; the index caps at 25 regardless |
kind | Optional, ≤32 chars | Exact match on page kind — capability, workflow, protocol, developer, and the rest |
status | Optional, ≤32 chars | Exact match on one of the five status words |
channel | Optional, ≤32 chars | Keeps pages that list that channel: email, phone, whatsapp, sms, softphone |
Filters are applied after scoring rather than before, so a filter narrows the answer without changing the relative order of what survives. An empty result from a filtered search means nothing of that kind matched, not that the query was bad.
The response#
{
"query": "held draft approval",
"count": 2,
"results": [
{
"id": "C0142",
"kind": "capability",
"url": "https://connectbyjbrh.com/docs/email/held-drafts/",
"markdown": "https://connectbyjbrh.com/docs/email/held-drafts/index.md",
"title": "Held drafts",
"description": "…",
"summary": "A held draft is a finished reply that Connect wrote and did not send…",
"status": "available",
"audiences": ["both"],
"channels": ["email"],
"updated": "2026-09-10",
"score": 18.412
}
]
}summary is the page's answer-first paragraph and is often enough to answer with. score is a relative number for ordering within one response — it is not a similarity, not a percentage, and not comparable between queries. Do not threshold on it.
How ranking works, and why it behaves as it does#
The index is a dictionary, not a search engine — a thousand short documents is the wrong size for one. Text is normalised, lowercased and split into tokens; single characters and a small stopword list are dropped. Title terms are indexed three times over, so a title match beats a description match.
- Rare terms count for more. A term's weight is the square root of the corpus size divided by the number of pages holding it.
- An unknown term costs nothing. It contributes zero rather than excluding everything, so a two-word query with one unfamiliar word still answers on the word it knows.
- Prefixes match. If nothing holds the exact term, pages holding a term that starts with it are used — which is how
idempotfinds *idempotency*. - Coverage beats intensity. A page matching every term is multiplied ahead of one that matches a single rare term repeatedly; without that, multi-word product questions surface glossary pages.
- Ties break on path, so equal scores return in a stable order.
Page bodies are indexed at weight zero. That is what makes a question like "call recording" answerable: no page is titled after a capability that is not available, so without body indexing the honest page would be unfindable.
The rest of the prefix, and its limits#
| Route | Purpose |
|---|---|
GET /api/public/docs/search | This one |
GET /api/public/docs/page?ref=…&body=true | One page by URL, path or id; ref up to 300 characters |
GET /api/public/docs/list | Filtered enumeration: kind, status, channel, audience, limit 1–1000 |
GET /api/public/docs/status | Product and corpus status in one object |
GET /api/public/docs/manifest/{name} | One generated manifest, with a guaranteed content type |
Every response carries Cache-Control: public, max-age=300 and Access-Control-Allow-Origin: *, so a browser client can call these directly. Exceeding 240 requests a minute from one address returns 429 with {"error": {"code": "rate_limited", …}}; the limit is there to stop a loop rather than to ration access, and a documentation API that rate-limits a crawler into failure would defeat its own purpose.
Questions#
Why did a two-word query return a page that matches only one word?
Because an unrecognised term contributes nothing instead of filtering everything out. Ranking still favours the page that matches both, through the coverage multiplier, so the fuller match sits above the partial one rather than the partial one disappearing.
Can I use score to decide whether to answer at all?
No. It is a within-response ordering number built from corpus statistics, and the same page can score differently for two phrasings of the same question. If you need a confidence signal, fetch the top page and judge the text.
Does the search index include the changelog and the research notes?
It includes every page in the published corpus, and kind tells you which is which — filter on kind=research or kind=developer when you want one slice.