Cookiebot API v0.1.0
The HTTP surface behind the Telegram Mini App and the web console.
Generated from /openapi.json by
python scripts/cb.py api-docs. Every shape here is the one the
service actually serves — FastAPI derives both from the same annotations.
The variables in the examples come from uv run scripts/qa_setup.py env.
admin
The deployment's own numbers, for whoever runs it: reach, fleet-wide rollups, the group directory and the LLM budget. Owners only — the admin:read scope is granted to a session only when its subject is one, so an ordinary admin's token cannot reach these at all.
GET/admin/analytics/commandsbearer
Which commands the deployment uses, busiest first, with how many groups each one reaches — a command with 10,000 invocations in one group and one used everywhere are different facts, and a decision to retire a command should not confuse them.
parameters
| name | in · type | |
|---|---|---|
| start | query · string<date> | null | inclusive, UTC |
| end | query · string<date> | null | inclusive, UTC |
| limit | query · integer | how many commands to return |
response
PlatformCommandsResponse
What the whole deployment's users actually type.
| field | type | |
|---|---|---|
| start required | string<date> | |
| end required | string<date> | |
| commands required | PlatformCommandRow[] |
PlatformCommandRow
One command across every group, with its reach as well as its volume.
| field | type | |
|---|---|---|
| command required | string | |
| invocations required | integer | |
| errors required | integer | |
| groups required | integer | distinct groups that used it — one busy group is not reach |
| p95_latency_ms required | integer | null |
refusals
| status | |
|---|---|
| 400 | the window is reversed or longer than a year |
| 401 | no bearer token, or one that did not verify |
| 403 | not an owner of this deployment — or an owner whose token lacks the scope |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/admin/analytics/commands?limit=5" \
-H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jqGET/admin/analytics/dailybearer
One row per day, summed across every group that was active that day.
active_users is summed per group, so one person in three groups counts three times — deduplicating across groups would need the raw events rather than the rollups, which is a much more expensive question.
parameters
| name | in · type | |
|---|---|---|
| start | query · string<date> | null | inclusive, UTC |
| end | query · string<date> | null | inclusive, UTC |
response
PlatformDailyResponse
The deployment's activity, one row per day.
| field | type | |
|---|---|---|
| start required | string<date> | |
| end required | string<date> | |
| days required | PlatformDayRow[] |
PlatformDayRow
One day, summed across every group that was active on it.
| field | type | |
|---|---|---|
| day required | string<date> | |
| groups required | integer | how many groups were active that day |
| messages required | integer | |
| commands required | integer | |
| joins required | integer | |
| leaves required | integer | |
| captcha_issued required | integer | |
| captcha_solved required | integer | |
| active_users required | integer | |
| errors required | integer | |
| p95_latency_ms required | integer | null | |
| llm_tokens required | integer | |
| llm_cost_usd required | number |
refusals
| status | |
|---|---|
| 400 | the window is reversed or longer than a year |
| 401 | no bearer token, or one that did not verify |
| 403 | not an owner of this deployment — or an owner whose token lacks the scope |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/admin/analytics/daily" \
-H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jqGET/admin/analytics/groupsbearer
The busiest groups in the window, most messages first.
A leaderboard, not a directory: a group with no activity in the window is absent here and present in /admin/groups.
parameters
| name | in · type | |
|---|---|---|
| start | query · string<date> | null | inclusive, UTC |
| end | query · string<date> | null | inclusive, UTC |
| limit | query · integer | how many groups to return |
response
TopGroupsResponse
The busiest groups. A leaderboard, not a directory: a group with no activity in the window is absent here and present in /admin/groups.
| field | type | |
|---|---|---|
| start required | string<date> | |
| end required | string<date> | |
| groups required | GroupActivityRow[] |
GroupActivityRow
One group's totals across the window, for the leaderboard.
| field | type | |
|---|---|---|
| group_id required | integer | |
| title required | string | null | |
| username required | string | null | |
| messages required | integer | |
| commands required | integer | |
| errors required | integer | |
| peak_active_users required | integer | |
| llm_cost_usd required | number |
refusals
| status | |
|---|---|
| 400 | the window is reversed or longer than a year |
| 401 | no bearer token, or one that did not verify |
| 403 | not an owner of this deployment — or an owner whose token lacks the scope |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/admin/analytics/groups?limit=5" \
-H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jqGET/admin/analytics/llmbearer
Fleet-wide LLM spend, per provider and model, most expensive first.
parameters
| name | in · type | |
|---|---|---|
| start | query · string<date> | null | inclusive, UTC |
| end | query · string<date> | null | inclusive, UTC |
response
PlatformLlmResponse
Fleet-wide AI spend, most expensive model first.
| field | type | |
|---|---|---|
| start required | string<date> | |
| end required | string<date> | |
| total_cost_usd required | number | |
| models required | PlatformLlmRow[] |
PlatformLlmRow
One provider/model's spend across every group.
| field | type | |
|---|---|---|
| provider required | string | |
| model required | string | |
| calls required | integer | |
| input_tokens required | integer | |
| output_tokens required | integer | |
| cost_usd required | number | |
| refusals required | integer | |
| errors required | integer |
refusals
| status | |
|---|---|
| 400 | the window is reversed or longer than a year |
| 401 | no bearer token, or one that did not verify |
| 403 | not an owner of this deployment — or an owner whose token lacks the scope |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/admin/analytics/llm" \
-H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jqGET/admin/groupsbearer
The directory, ordered by group_id and keyset-paginated (D11).
The cursor is the id itself, which is stable and unique, so a group added between two pages cannot make a row repeat or vanish the way an OFFSET would.
parameters
| name | in · type | |
|---|---|---|
| limit | query · integer | groups per page |
| after | query · integer | null | last group_id of the previous page |
| search | query · string | null | title or @username |
| include_left | query · boolean | also list groups the bot was removed from |
response
DirectoryPage
A page of the directory, ordered by group_id.
| field | type | |
|---|---|---|
| groups required | DirectoryRow[] | |
| next_after | integer | null | pass as `after` for the following page; null on the last one |
DirectoryRow
One group the bot knows — what it is in, not what it did.
| field | type | |
|---|---|---|
| group_id required | integer | |
| title required | string | null | |
| username required | string | null | |
| chat_type required | string | |
| skin required | string | which bot persona serves it (`core_botskins`) |
| joined_at required | string<date-time> | |
| left_at | string<date-time> | null | set when the bot was removed; the row is kept for its history |
| members required | integer | |
| admins required | integer |
refusals
| status | |
|---|---|
| 401 | no bearer token, or one that did not verify |
| 403 | not an owner of this deployment — or an owner whose token lacks the scope |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/admin/groups?limit=5" \
-H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jqGET/admin/overviewbearer
Reach, the window's totals and the LLM budget, in one request.
parameters
| name | in · type | |
|---|---|---|
| start | query · string<date> | null | inclusive, UTC |
| end | query · string<date> | null | inclusive, UTC |
response
OverviewResponse
One request for the dashboard's first screen.
Three round trips server-side rather than three from the Mini App: a phone inside Telegram on a bad connection pays for each of them, and none of the three is useful without the others.
| field | type | |
|---|---|---|
| start required | string<date> | |
| end required | string<date> | |
| tenant_id required | string | |
| display_name required | string | |
| reach required | ReachBody | |
| totals required | PlatformSummary | |
| budget required | BudgetBody |
ReachBody
Where the bot is right now — no window, because this is the present.
| field | type | |
|---|---|---|
| groups required | integer | groups the bot is currently in |
| groups_left required | integer | groups it was removed from, kept for their history |
| members required | integer | current memberships, summed — a person in three groups is three |
| admins required | integer |
PlatformSummary
The window's totals across every group, with the peaks called out rather than averaged — an average of daily percentiles means nothing.
| field | type | |
|---|---|---|
| days required | integer | how many days in the window had any activity at all |
| peak_groups required | integer | the busiest day's count of active groups |
| messages required | integer | |
| commands required | integer | |
| joins required | integer | |
| leaves required | integer | |
| errors required | integer | |
| captcha_issued required | integer | |
| captcha_solved required | integer | |
| captcha_solve_rate | number | null | null when nobody was challenged anywhere |
| peak_active_users required | integer | |
| worst_p95_latency_ms required | integer | null | |
| llm_tokens required | integer | |
| llm_cost_usd required | number |
BudgetBody
The tenant's soft LLM budget against what the window actually cost.
monthly_llm_budget_usd is what cb_core.llm refuses chat past, so an owner watching this number is watching the thing that will silently turn the conversational features off.
| field | type | |
|---|---|---|
| monthly_llm_budget_usd | number | null | null when the tenant has no budget configured |
| spent_usd required | number | the requested window's spend, not the calendar month's |
| remaining_usd | number | null | null when there is no budget |
refusals
| status | |
|---|---|
| 400 | the window is reversed or longer than a year |
| 401 | no bearer token, or one that did not verify |
| 403 | not an owner of this deployment — or an owner whose token lacks the scope |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/admin/overview" \
-H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jqGET/admin/tenantbearer
The tenant's own row, read-only.
Read-only because every field here changes how the *bot* behaves — which commands exist, which model answers, where media is written — and a Mini App form that could flip active or empty owner_ids would be one mis-tap from an outage nobody could undo through the same API. Editing a tenant is a deliberate database change, and it stays one.
bot_tokens is deliberately absent: an owner who needs it has it already, and an endpoint that returns bot tokens is one stolen owner token away from being the whole deployment.
response
TenantResponse
What this deployment is configured as. Read-only on purpose — see the note on the endpoint.
| field | type | |
|---|---|---|
| tenant_id required | string | |
| display_name required | string | |
| handler_pack required | string | |
| default_locale required | string | |
| disabled_commands required | string[] | |
| storage_prefix required | string | |
| monthly_llm_budget_usd required | number | null | |
| active required | boolean | |
| owner_ids required | integer[] | who else can reach this router |
refusals
| status | |
|---|---|
| 401 | no bearer token, or one that did not verify |
| 403 | not an owner of this deployment — or an owner whose token lacks the scope |
try it
curl -s "$CB_QA_API/admin/tenant" \
-H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jqanalytics
Per-group rollups, keyset-paginated.
GET/groups/{group_id}/analytics/commandsbearer
Which commands this group actually uses, busiest first, totalled across the window rather than broken down per day.
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
| start | query · string<date> | null | inclusive, UTC |
| end | query · string<date> | null | inclusive, UTC |
| limit | query · integer | how many commands to return |
response
CommandsResponse
Which commands this group uses, busiest first.
| field | type | |
|---|---|---|
| group_id required | integer | |
| start required | string<date> | |
| end required | string<date> | |
| commands required | CommandRow[] |
CommandRow
One command's totals across the window, not one row per day.
| field | type | |
|---|---|---|
| command required | string | |
| invocations required | integer | |
| errors required | integer | |
| p95_latency_ms required | integer | null |
refusals
| status | |
|---|---|
| 400 | the window is reversed or longer than a year |
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/groups/$CB_QA_GROUP/analytics/commands?limit=5" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqGET/groups/{group_id}/analytics/dailybearer
One row per day the group was active. Days with no activity have no row — the rollup writes only what it saw, and inventing zeros here would be indistinguishable from a real quiet day.
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
| start | query · string<date> | null | inclusive, UTC |
| end | query · string<date> | null | inclusive, UTC |
response
DailyResponse
One row per day the group was active — a chart's x-axis.
| field | type | |
|---|---|---|
| group_id required | integer | |
| start required | string<date> | |
| end required | string<date> | |
| days required | DailyRow[] |
DailyRow
One day. Days with no activity have no row at all.
| field | type | |
|---|---|---|
| day required | string<date> | |
| messages required | integer | |
| commands required | integer | |
| joins required | integer | |
| leaves required | integer | |
| captcha_issued required | integer | |
| captcha_solved required | integer | |
| active_users required | integer | |
| errors required | integer | |
| p95_latency_ms required | integer | null | |
| llm_tokens required | integer | |
| llm_cost_usd required | number |
refusals
| status | |
|---|---|
| 400 | the window is reversed or longer than a year |
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/groups/$CB_QA_GROUP/analytics/daily" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqGET/groups/{group_id}/analytics/llmbearer
What this group's AI features cost, per provider and model.
The same numbers Tenant.monthly_llm_budget_usd is spent against, so an admin can see why the conversational AI started refusing before asking anyone.
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
| start | query · string<date> | null | inclusive, UTC |
| end | query · string<date> | null | inclusive, UTC |
response
LlmResponse
What this group's AI features cost — the same numbers the tenant's budget is spent against, so an admin can see why chat started refusing.
| field | type | |
|---|---|---|
| group_id required | integer | |
| start required | string<date> | |
| end required | string<date> | |
| total_cost_usd required | number | |
| models required | ModelCostRow[] |
ModelCostRow
One provider/model's usage and spend across the window.
| field | type | |
|---|---|---|
| provider required | string | |
| model required | string | |
| calls required | integer | |
| input_tokens required | integer | |
| output_tokens required | integer | |
| cost_usd required | number | |
| refusals required | integer | |
| errors required | integer |
refusals
| status | |
|---|---|
| 400 | the window is reversed or longer than a year |
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/groups/$CB_QA_GROUP/analytics/llm" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqGET/groups/{group_id}/analytics/summarybearer
The window in one object — totals, the peak day's active users, the worst day's p95, and the captcha solve rate (null when nobody was challenged, which is not the same fact as nobody solving it).
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
| start | query · string<date> | null | inclusive, UTC |
| end | query · string<date> | null | inclusive, UTC |
response
SummaryResponse
cb_core.analytics.summarise, plus the window.
| field | type | |
|---|---|---|
| group_id required | integer | |
| start required | string<date> | |
| end required | string<date> | |
| days required | integer | how many days had a row, not the window's length |
| messages required | integer | |
| commands required | integer | |
| joins required | integer | |
| leaves required | integer | |
| errors required | integer | |
| captcha_issued required | integer | |
| captcha_solved required | integer | |
| captcha_solve_rate | number | null | null when nobody was challenged — not the same fact as 0.0 |
| peak_active_users required | integer | |
| worst_p95_latency_ms | integer | null | the worst day's p95, never an average of percentiles |
| llm_tokens required | integer | |
| llm_cost_usd required | number |
refusals
| status | |
|---|---|
| 400 | the window is reversed or longer than a year |
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/groups/$CB_QA_GROUP/analytics/summary" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqauth
The OAuth2 token endpoint. A Mini App posts Telegram's initData, the web console posts the login widget's payload, both refresh. Discover it from /.well-known/openid-configuration.
POST/oauth2/revoke
RFC 7009. Revoking an unknown token is a success: the caller's goal is that the token cannot be used, and it cannot.
request body
{
"properties": {
"token": {
"type": "string",
"title": "Token",
"description": "an access or refresh token; unknown values succeed"
}
},
"type": "object",
"required": [
"token"
],
"title": "RevokeRequest"
}response
RevokeResponse
Always true, including for a token this deployment never issued (RFC 7009): the caller's goal is that it cannot be used, and it cannot.
| field | type | |
|---|---|---|
| revoked required | boolean |
try it
curl -s -X POST "$CB_QA_API/oauth2/revoke" | jq
POST/oauth2/token
Exchange a Telegram proof — or a refresh token — for an access token.
request body
{
"properties": {
"grant_type": {
"type": "string",
"title": "Grant Type",
"description": "urn:cookiebot:params:oauth:grant-type:telegram-miniapp | urn:cookiebot:params:oauth:grant-type:telegram-login | refresh_token"
},
"init_data": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Init Data",
"description": "`window.Telegram.WebApp.initData`, verbatim \u2014 reordering or re-encoding it breaks the signature. Required for the miniapp grant."
},
"auth_data": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Auth Data",
"description": "the Telegram login widget's payload, as an object or its JSON string. Required for the login grant."
},
"refresh_token": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Refresh Token",
"description": "a refresh token this endpoint issued. Required for `refresh_token`."
}
},
"type": "object",
"required": [
"grant_type"
],
"title": "TokenRequest",
"description": "The union of the three grants' bodies. Which fields are required depends\non `grant_type`, which is why they are all optional here and checked in the\nhandler \u2014 an OpenAPI schema cannot express \"this one, then that one\"\nwithout `oneOf` branches no generated client would read well."
}response
TokenResponse
RFC 6749 §5.1. The access token is the same RS256 JWT /login mints, verifiable against /.well-known/jwks.json.
| field | type | |
|---|---|---|
| access_token required | string | |
| token_type | string | |
| expires_in required | integer | access-token life in seconds |
| refresh_token required | string | rotates on every use; presenting a spent one revokes the whole family |
| scope required | string | space-separated |
refusals
| status | |
|---|---|
| 400 | `unsupported_grant_type`, `invalid_request` or `invalid_grant` |
try it
curl -s -X POST "$CB_QA_API/oauth2/token" | jq
groups
Everything /config does in a Telegram chat: settings, rules, the welcome message, and the audit trail. Every path carries the group, and only its admins (and the tenant's owners) get an answer.
GET/groups/{group_id}/auditbearer
The group's trail, newest first, keyset-paginated (D11 — no OFFSET, no unbounded list).
next_before is the cursor for the following page and is null on the last one. Filtering by action or actor_user_id narrows without changing the cursor's meaning.
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
| limit | query · integer | events per page |
| before | query · string<uuid> | null | last id of the previous page |
| action | query · string | null | only this action, e.g. `config.updated` |
| actor_user_id | query · integer | null | only changes this Telegram user made |
response
AuditPage
A page of the trail, newest first, with the cursor for the next one.
| field | type | |
|---|---|---|
| group_id required | integer | |
| events required | AuditEvent[] | |
| next_before | string | null | pass as `before` for the following page; null on the last one |
AuditEvent
One recorded change: what, who, from where, and both values.
| field | type | |
|---|---|---|
| id required | string | UUIDv7 — ordering by it is ordering by time |
| ts required | string<date-time> | |
| action required | string | `config.updated`, `rules.updated`, `welcome.updated`, … |
| surface required | string | `telegram`, `miniapp`, `api` or `system` |
| actor_user_id | integer | null | null when an anonymous admin made the change |
| actor_kind required | string | |
| summary required | string | null | |
| before required | object | null | |
| after required | object | null | |
| trace_id required | string | null |
refusals
| status | |
|---|---|
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/groups/$CB_QA_GROUP/audit?limit=5" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqGET/groups/{group_id}/configbearer
The group's effective settings — stored values over tenant defaults over v1's defaults, which is the same resolution the bot itself reads.
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
response
ConfigResponse
A group's effective settings: stored values over the tenant's defaults over v1's, which is the same resolution the bot itself reads.
| field | type | |
|---|---|---|
| group_id required | integer | |
| config required | GroupConfigValues |
GroupConfigValues
The group's effective settings — the same fifteen columns ConfigPatch writes, none of them optional here because a read always resolves to a value (stored, else the tenant's default, else v1's).
| field | type | |
|---|---|---|
| allow_furbots required | boolean | |
| sticker_spam_limit required | integer | |
| sticker_spam_window_s required | integer | |
| media_restrict_seconds required | integer | |
| captcha_timeout_seconds required | integer | |
| functions_fun required | boolean | |
| functions_utility required | boolean | |
| sfw required | boolean | |
| language required | string | |
| publisher_post required | boolean | |
| publisher_ask required | boolean | |
| publisher_members_only required | boolean | |
| thread_posts | string | null | the pinned topic id, or null for none (v1 wrote '9999') |
| max_posts required | integer | |
| doomlist_enabled required | boolean |
refusals
| status | |
|---|---|
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/groups/$CB_QA_GROUP/config" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqPATCH/groups/{group_id}/configbearer
Change some settings. Absent fields are left alone — this is a PATCH, not a PUT, because a Mini App form that round-trips every column would overwrite a change another admin made while it was open.
An empty patch is a 400 rather than a no-op success: a client that sent nothing meant to send something.
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
request body
ConfigPatch
The writable half of group_configs, every field optional.
Written out rather than derived from GroupConfig's dataclass fields: this is a public contract, and a column added to the table should not silently become writable over HTTP. The bounds are the ones the Telegram menu enforces by refusing to parse anything else, made explicit.
| field | type | |
|---|---|---|
| allow_furbots | boolean | null | |
| sticker_spam_limit | integer | null | |
| sticker_spam_window_s | integer | null | |
| media_restrict_seconds | integer | null | |
| captcha_timeout_seconds | integer | null | |
| functions_fun | boolean | null | |
| functions_utility | boolean | null | |
| sfw | boolean | null | |
| language | string | null | |
| publisher_post | boolean | null | |
| publisher_ask | boolean | null | |
| publisher_members_only | boolean | null | |
| thread_posts | string | null | |
| max_posts | integer | null | |
| doomlist_enabled | boolean | null |
response
ConfigUpdateResponse
The settings after the patch, and which of them actually moved.
| field | type | |
|---|---|---|
| group_id required | integer | |
| config required | GroupConfigValues | |
| changed required | string[] | the fields whose value actually moved — a patch that asks for the value a setting already has changes nothing and audits nothing |
GroupConfigValues
The group's effective settings — the same fifteen columns ConfigPatch writes, none of them optional here because a read always resolves to a value (stored, else the tenant's default, else v1's).
| field | type | |
|---|---|---|
| allow_furbots required | boolean | |
| sticker_spam_limit required | integer | |
| sticker_spam_window_s required | integer | |
| media_restrict_seconds required | integer | |
| captcha_timeout_seconds required | integer | |
| functions_fun required | boolean | |
| functions_utility required | boolean | |
| sfw required | boolean | |
| language required | string | |
| publisher_post required | boolean | |
| publisher_ask required | boolean | |
| publisher_members_only required | boolean | |
| thread_posts | string | null | the pinned topic id, or null for none (v1 wrote '9999') |
| max_posts required | integer | |
| doomlist_enabled required | boolean |
refusals
| status | |
|---|---|
| 400 | the patch carried no settings |
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s -X PATCH "$CB_QA_API/groups/$CB_QA_GROUP/config" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqGET/groups/{group_id}/rulesbearer
The text /rules prints in the chat, with who last set it and when.
A group that never set any reads back "body": null rather than 404 — not having rules is a normal state of a group, not a missing resource.
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
response
GroupTextResponse
The rules or the welcome message, with its provenance. body is null when the group never set one; that is the normal state, not a 404.
| field | type | |
|---|---|---|
| group_id required | integer | |
| body required | string | null | |
| updated_by required | integer | null | |
| updated_at required | string<date-time> | null |
refusals
| status | |
|---|---|
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/groups/$CB_QA_GROUP/rules" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqPUT/groups/{group_id}/rulesbearer
What /newrules sets, set from the Mini App instead. Same table, same upsert (cb_core.group_texts), so /rules in the chat shows this text immediately.
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
request body
TextBody
The rules, or a welcome message. Length is Telegram's message limit — text this endpoint accepts that the bot could never send would be a setting that saves and then fails silently in the chat.
| field | type | |
|---|---|---|
| body required | string |
response
GroupTextResponse
The rules or the welcome message, with its provenance. body is null when the group never set one; that is the normal state, not a 404.
| field | type | |
|---|---|---|
| group_id required | integer | |
| body required | string | null | |
| updated_by required | integer | null | |
| updated_at required | string<date-time> | null |
refusals
| status | |
|---|---|
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s -X PUT "$CB_QA_API/groups/$CB_QA_GROUP/rules" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqGET/groups/{group_id}/welcomebearer
The message new members are greeted with, exactly as stored.
The <user> placeholders are substituted when the bot sends it, not here, so what comes back is what /newwelcome was given. null when the group never set one.
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
response
GroupTextResponse
The rules or the welcome message, with its provenance. body is null when the group never set one; that is the normal state, not a 404.
| field | type | |
|---|---|---|
| group_id required | integer | |
| body required | string | null | |
| updated_by required | integer | null | |
| updated_at required | string<date-time> | null |
refusals
| status | |
|---|---|
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s "$CB_QA_API/groups/$CB_QA_GROUP/welcome" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqPUT/groups/{group_id}/welcomebearer
<user> and its eight sibling spellings are substituted when the message is sent, not here — the body is stored exactly as given, which is what /newwelcome does.
parameters
| name | in · type | |
|---|---|---|
| group_id required | path · integer | the Telegram chat id — negative, as Telegram writes it |
request body
TextBody
The rules, or a welcome message. Length is Telegram's message limit — text this endpoint accepts that the bot could never send would be a setting that saves and then fails silently in the chat.
| field | type | |
|---|---|---|
| body required | string |
response
GroupTextResponse
The rules or the welcome message, with its provenance. body is null when the group never set one; that is the normal state, not a 404.
| field | type | |
|---|---|---|
| group_id required | integer | |
| body required | string | null | |
| updated_by required | integer | null | |
| updated_at required | string<date-time> | null |
refusals
| status | |
|---|---|
| 401 | no bearer token, or one that did not verify |
| 403 | an admin whose token lacks the scope; ask /oauth2/token for a better one |
| 404 | no such group — or the caller does not administer it, which answers alike |
| 422 | Validation Error |
try it
curl -s -X PUT "$CB_QA_API/groups/$CB_QA_GROUP/welcome" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqGET/mebearer
Who the token says you are, what it lets you do, and which groups you administer.
The Mini App's first call: it has initData telling it who the user is, but not which of that user's groups this deployment knows about, nor whether they run the whole thing. The list comes from group_admins, which the gateway maintains — so a promotion made a minute ago appears once the bot has seen an admin-gated command in that group, and not before (cb_api.security's note on why this service never calls Telegram to refresh it).
response
MeResponse
Who the token says you are, what it lets you do, and where.
| field | type | |
|---|---|---|
| user_id required | integer | |
| scopes required | string[] | |
| audience | string | null | the token's `aud`, if it carries one |
| is_bot_admin | boolean | whether this caller runs the deployment — the Mini App draws its `/admin` screens from this rather than from a 403 it had to provoke |
| groups required | AdministeredGroup[] |
AdministeredGroup
One group the caller administers, as group_admins records it.
| field | type | |
|---|---|---|
| group_id required | integer | |
| title required | string | null | |
| username required | string | null | |
| chat_type required | string | null | |
| role required | string | what `group_admins` says: `creator` or `administrator` |
| anonymous required | boolean | null |
refusals
| status | |
|---|---|
| 401 | no bearer token, or one that did not verify |
try it
curl -s "$CB_QA_API/me" \
-H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jqhealth
Liveness and readiness, split on purpose: restart on /healthz, pull from the load balancer on /readyz. The only two endpoints that take no token.
GET/healthz
Process is up. No dependency checks — a DB blip must not trigger restarts.
response
Liveness
What a liveness probe gets: the process identifying itself.
| field | type | |
|---|---|---|
| status required | string | |
| service required | string | |
| version required | string | |
| cython required | boolean | whether the compiled hot path is in this build |
try it
curl -s "$CB_QA_API/healthz" | jq
GET/readyz
Dependencies reachable. Fails -> pulled from rotation, not restarted.
response
Readiness
Each dependency, separately. One flag for "should I get traffic" would make the two failures indistinguishable in the one place they need to be told apart.
| field | type | |
|---|---|---|
| ready required | boolean | |
| postgres required | boolean | |
| valkey required | boolean |
refusals
| status | |
|---|---|
| 503 | pulled from rotation, not restarted |
try it
curl -s "$CB_QA_API/readyz" | jq
webhub
v1's own surface, unchanged: the web console's /login, the service banner, and the two discovery documents every token here is verified against. /login's token carries no scopes and is read-only.
GET/
v1 Server.py:55-57. number_chats was the module constant NUMBER_CHATS = 1275 (:17) that nothing ever updated; here it is the real count.
response
ServiceBanner
v1's / — what COOKIEBOT-WebHub polls to decide the bot is alive.
| field | type | |
|---|---|---|
| status required | string | |
| number_chats required | integer | the real count; v1 returned a module constant nothing updated |
try it
curl -s "$CB_QA_API/" | jq
GET/.well-known/jwks.json
v1 Server.py:78-84. v1 published the key of whichever gunicorn worker answered; this publishes every key the deployment might have signed with, so a rotation can overlap.
response
JwksDocument
Every key the deployment might have signed with, so a rotation overlaps.
| field | type | |
|---|---|---|
| keys required | object[] | public JWKs, `kid` and all |
try it
curl -s "$CB_QA_API/.well-known/jwks.json" | jq
GET/.well-known/openid-configuration
v1 Server.py:86-95, plus what x_miniapp_auth added.
The five v1 keys are unchanged and in place; the rest describe the token endpoint, its two Telegram grants and the revocation endpoint, which is what a Mini App or an OAuth client library discovers this deployment with. Additive by construction: a consumer written against v1 reads the same five values it always did.
response
OpenIDConfiguration
What a Mini App or an OAuth client library discovers this deployment with. The first five keys are v1's, unchanged; the rest arrived with x_miniapp_auth.
| field | type | |
|---|---|---|
| issuer required | string | |
| jwks_uri required | string | |
| response_types_supported required | string[] | |
| subject_types_supported required | string[] | |
| id_token_signing_alg_values_supported required | string[] | |
| token_endpoint required | string | |
| revocation_endpoint required | string | |
| token_endpoint_auth_methods_supported required | string[] | |
| grant_types_supported required | string[] | |
| scopes_supported required | string[] |
try it
curl -s "$CB_QA_API/.well-known/openid-configuration" | jq
POST/login
Exchange a Telegram login-widget payload for a JWT.
v1 Server.py:59-76. The status codes and the two error bodies are v1's, because the client branches on them.
request body
{
"additionalProperties": true,
"type": "object",
"title": "Payload"
}response
LoginToken
v1's success body, key for key. COOKIEBOT-WebHub reads accessToken by name (src/lib/api/axios.ts), so neither name may move.
| field | type | |
|---|---|---|
| status required | string | |
| accessToken required | string | RS256, verifiable against `/.well-known/jwks.json`; carries no `scope` claim, which `cb_api.security` reads as read-only |
refusals
| status | |
|---|---|
| 400 | an empty payload — v1's `Missing data` |
| 401 | the signature did not verify, or the payload is stale where `CB_WEBHUB_AUTH_MAX_AGE_SECONDS` is set — one answer for both, so a forgery cannot learn which of the two it got closest to |
| 422 | Validation Error |
try it
curl -s -X POST "$CB_QA_API/login" | jq