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 whole deployment uses

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

namein · type
startquery · string<date> | nullinclusive, UTC
endquery · string<date> | nullinclusive, UTC
limitquery · integerhow many commands to return

response

PlatformCommandsResponse

What the whole deployment's users actually type.

fieldtype
start requiredstring<date>
end requiredstring<date>
commands requiredPlatformCommandRow[]

PlatformCommandRow

One command across every group, with its reach as well as its volume.

fieldtype
command requiredstring
invocations requiredinteger
errors requiredinteger
groups requiredintegerdistinct groups that used it — one busy group is not reach
p95_latency_ms requiredinteger | null

refusals

status
400the window is reversed or longer than a year
401no bearer token, or one that did not verify
403not an owner of this deployment — or an owner whose token lacks the scope
422Validation Error

try it

curl -s "$CB_QA_API/admin/analytics/commands?limit=5" \
     -H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jq

GET/admin/analytics/dailybearer

One row per day, summed across every group

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

namein · type
startquery · string<date> | nullinclusive, UTC
endquery · string<date> | nullinclusive, UTC

response

PlatformDailyResponse

The deployment's activity, one row per day.

fieldtype
start requiredstring<date>
end requiredstring<date>
days requiredPlatformDayRow[]

PlatformDayRow

One day, summed across every group that was active on it.

fieldtype
day requiredstring<date>
groups requiredintegerhow many groups were active that day
messages requiredinteger
commands requiredinteger
joins requiredinteger
leaves requiredinteger
captcha_issued requiredinteger
captcha_solved requiredinteger
active_users requiredinteger
errors requiredinteger
p95_latency_ms requiredinteger | null
llm_tokens requiredinteger
llm_cost_usd requirednumber

refusals

status
400the window is reversed or longer than a year
401no bearer token, or one that did not verify
403not an owner of this deployment — or an owner whose token lacks the scope
422Validation Error

try it

curl -s "$CB_QA_API/admin/analytics/daily" \
     -H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jq

GET/admin/analytics/groupsbearer

The busiest groups in the window

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

namein · type
startquery · string<date> | nullinclusive, UTC
endquery · string<date> | nullinclusive, UTC
limitquery · integerhow 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.

fieldtype
start requiredstring<date>
end requiredstring<date>
groups requiredGroupActivityRow[]

GroupActivityRow

One group's totals across the window, for the leaderboard.

fieldtype
group_id requiredinteger
title requiredstring | null
username requiredstring | null
messages requiredinteger
commands requiredinteger
errors requiredinteger
peak_active_users requiredinteger
llm_cost_usd requirednumber

refusals

status
400the window is reversed or longer than a year
401no bearer token, or one that did not verify
403not an owner of this deployment — or an owner whose token lacks the scope
422Validation Error

try it

curl -s "$CB_QA_API/admin/analytics/groups?limit=5" \
     -H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jq

GET/admin/analytics/llmbearer

What every group's AI features cost together

Fleet-wide LLM spend, per provider and model, most expensive first.

parameters

namein · type
startquery · string<date> | nullinclusive, UTC
endquery · string<date> | nullinclusive, UTC

response

PlatformLlmResponse

Fleet-wide AI spend, most expensive model first.

fieldtype
start requiredstring<date>
end requiredstring<date>
total_cost_usd requirednumber
models requiredPlatformLlmRow[]

PlatformLlmRow

One provider/model's spend across every group.

fieldtype
provider requiredstring
model requiredstring
calls requiredinteger
input_tokens requiredinteger
output_tokens requiredinteger
cost_usd requirednumber
refusals requiredinteger
errors requiredinteger

refusals

status
400the window is reversed or longer than a year
401no bearer token, or one that did not verify
403not an owner of this deployment — or an owner whose token lacks the scope
422Validation Error

try it

curl -s "$CB_QA_API/admin/analytics/llm" \
     -H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jq

GET/admin/groupsbearer

Every group the bot knows, keyset-paginated

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

namein · type
limitquery · integergroups per page
afterquery · integer | nulllast group_id of the previous page
searchquery · string | nulltitle or @username
include_leftquery · booleanalso list groups the bot was removed from

response

DirectoryPage

A page of the directory, ordered by group_id.

fieldtype
groups requiredDirectoryRow[]
next_after integer | nullpass 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.

fieldtype
group_id requiredinteger
title requiredstring | null
username requiredstring | null
chat_type requiredstring
skin requiredstringwhich bot persona serves it (`core_botskins`)
joined_at requiredstring<date-time>
left_at string<date-time> | nullset when the bot was removed; the row is kept for its history
members requiredinteger
admins requiredinteger

refusals

status
401no bearer token, or one that did not verify
403not an owner of this deployment — or an owner whose token lacks the scope
422Validation Error

try it

curl -s "$CB_QA_API/admin/groups?limit=5" \
     -H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jq

GET/admin/overviewbearer

The whole deployment in one object: reach, the window's totals, the budget

Reach, the window's totals and the LLM budget, in one request.

parameters

namein · type
startquery · string<date> | nullinclusive, UTC
endquery · string<date> | nullinclusive, 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.

fieldtype
start requiredstring<date>
end requiredstring<date>
tenant_id requiredstring
display_name requiredstring
reach requiredReachBody
totals requiredPlatformSummary
budget requiredBudgetBody

ReachBody

Where the bot is right now — no window, because this is the present.

fieldtype
groups requiredintegergroups the bot is currently in
groups_left requiredintegergroups it was removed from, kept for their history
members requiredintegercurrent memberships, summed — a person in three groups is three
admins requiredinteger

PlatformSummary

The window's totals across every group, with the peaks called out rather than averaged — an average of daily percentiles means nothing.

fieldtype
days requiredintegerhow many days in the window had any activity at all
peak_groups requiredintegerthe busiest day's count of active groups
messages requiredinteger
commands requiredinteger
joins requiredinteger
leaves requiredinteger
errors requiredinteger
captcha_issued requiredinteger
captcha_solved requiredinteger
captcha_solve_rate number | nullnull when nobody was challenged anywhere
peak_active_users requiredinteger
worst_p95_latency_ms requiredinteger | null
llm_tokens requiredinteger
llm_cost_usd requirednumber

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.

fieldtype
monthly_llm_budget_usd number | nullnull when the tenant has no budget configured
spent_usd requirednumberthe requested window's spend, not the calendar month's
remaining_usd number | nullnull when there is no budget

refusals

status
400the window is reversed or longer than a year
401no bearer token, or one that did not verify
403not an owner of this deployment — or an owner whose token lacks the scope
422Validation Error

try it

curl -s "$CB_QA_API/admin/overview" \
     -H "Authorization: Bearer $CB_QA_OWNER_TOKEN" | jq

GET/admin/tenantbearer

How this deployment is configured

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.

fieldtype
tenant_id requiredstring
display_name requiredstring
handler_pack requiredstring
default_locale requiredstring
disabled_commands requiredstring[]
storage_prefix requiredstring
monthly_llm_budget_usd requirednumber | null
active requiredboolean
owner_ids requiredinteger[]who else can reach this router

refusals

status
401no bearer token, or one that did not verify
403not 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" | jq

analytics

Per-group rollups, keyset-paginated.

GET/groups/{group_id}/analytics/commandsbearer

Which commands the group actually uses, busiest first

Which commands this group actually uses, busiest first, totalled across the window rather than broken down per day.

parameters

namein · type
group_id requiredpath · integerthe Telegram chat id — negative, as Telegram writes it
startquery · string<date> | nullinclusive, UTC
endquery · string<date> | nullinclusive, UTC
limitquery · integerhow many commands to return

response

CommandsResponse

Which commands this group uses, busiest first.

fieldtype
group_id requiredinteger
start requiredstring<date>
end requiredstring<date>
commands requiredCommandRow[]

CommandRow

One command's totals across the window, not one row per day.

fieldtype
command requiredstring
invocations requiredinteger
errors requiredinteger
p95_latency_ms requiredinteger | null

refusals

status
400the window is reversed or longer than a year
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s "$CB_QA_API/groups/$CB_QA_GROUP/analytics/commands?limit=5" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

GET/groups/{group_id}/analytics/dailybearer

One row per day the group was active

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

namein · type
group_id requiredpath · integerthe Telegram chat id — negative, as Telegram writes it
startquery · string<date> | nullinclusive, UTC
endquery · string<date> | nullinclusive, UTC

response

DailyResponse

One row per day the group was active — a chart's x-axis.

fieldtype
group_id requiredinteger
start requiredstring<date>
end requiredstring<date>
days requiredDailyRow[]

DailyRow

One day. Days with no activity have no row at all.

fieldtype
day requiredstring<date>
messages requiredinteger
commands requiredinteger
joins requiredinteger
leaves requiredinteger
captcha_issued requiredinteger
captcha_solved requiredinteger
active_users requiredinteger
errors requiredinteger
p95_latency_ms requiredinteger | null
llm_tokens requiredinteger
llm_cost_usd requirednumber

refusals

status
400the window is reversed or longer than a year
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s "$CB_QA_API/groups/$CB_QA_GROUP/analytics/daily" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

GET/groups/{group_id}/analytics/llmbearer

What the group's AI features cost, per provider and model

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

namein · type
group_id requiredpath · integerthe Telegram chat id — negative, as Telegram writes it
startquery · string<date> | nullinclusive, UTC
endquery · string<date> | nullinclusive, 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.

fieldtype
group_id requiredinteger
start requiredstring<date>
end requiredstring<date>
total_cost_usd requirednumber
models requiredModelCostRow[]

ModelCostRow

One provider/model's usage and spend across the window.

fieldtype
provider requiredstring
model requiredstring
calls requiredinteger
input_tokens requiredinteger
output_tokens requiredinteger
cost_usd requirednumber
refusals requiredinteger
errors requiredinteger

refusals

status
400the window is reversed or longer than a year
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s "$CB_QA_API/groups/$CB_QA_GROUP/analytics/llm" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

GET/groups/{group_id}/analytics/summarybearer

The whole window in one object

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

namein · type
group_id requiredpath · integerthe Telegram chat id — negative, as Telegram writes it
startquery · string<date> | nullinclusive, UTC
endquery · string<date> | nullinclusive, UTC

response

SummaryResponse

cb_core.analytics.summarise, plus the window.

fieldtype
group_id requiredinteger
start requiredstring<date>
end requiredstring<date>
days requiredintegerhow many days had a row, not the window's length
messages requiredinteger
commands requiredinteger
joins requiredinteger
leaves requiredinteger
errors requiredinteger
captcha_issued requiredinteger
captcha_solved requiredinteger
captcha_solve_rate number | nullnull when nobody was challenged — not the same fact as 0.0
peak_active_users requiredinteger
worst_p95_latency_ms integer | nullthe worst day's p95, never an average of percentiles
llm_tokens requiredinteger
llm_cost_usd requirednumber

refusals

status
400the window is reversed or longer than a year
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s "$CB_QA_API/groups/$CB_QA_GROUP/analytics/summary" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

auth

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

Revoke a refresh token (RFC 7009)

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.

fieldtype
revoked requiredboolean

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

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.

fieldtype
access_token requiredstring
token_type string
expires_in requiredintegeraccess-token life in seconds
refresh_token requiredstringrotates on every use; presenting a spent one revokes the whole family
scope requiredstringspace-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

Read the group's audit trail, newest first

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

namein · type
group_id requiredpath · integerthe Telegram chat id — negative, as Telegram writes it
limitquery · integerevents per page
beforequery · string<uuid> | nulllast id of the previous page
actionquery · string | nullonly this action, e.g. `config.updated`
actor_user_idquery · integer | nullonly changes this Telegram user made

response

AuditPage

A page of the trail, newest first, with the cursor for the next one.

fieldtype
group_id requiredinteger
events requiredAuditEvent[]
next_before string | nullpass as `before` for the following page; null on the last one

AuditEvent

One recorded change: what, who, from where, and both values.

fieldtype
id requiredstringUUIDv7 — ordering by it is ordering by time
ts requiredstring<date-time>
action requiredstring`config.updated`, `rules.updated`, `welcome.updated`, …
surface requiredstring`telegram`, `miniapp`, `api` or `system`
actor_user_id integer | nullnull when an anonymous admin made the change
actor_kind requiredstring
summary requiredstring | null
before requiredobject | null
after requiredobject | null
trace_id requiredstring | null

refusals

status
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s "$CB_QA_API/groups/$CB_QA_GROUP/audit?limit=5" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

GET/groups/{group_id}/configbearer

Read a group's settings

The group's effective settings — stored values over tenant defaults over v1's defaults, which is the same resolution the bot itself reads.

parameters

namein · type
group_id requiredpath · integerthe 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.

fieldtype
group_id requiredinteger
config requiredGroupConfigValues

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).

fieldtype
allow_furbots requiredboolean
sticker_spam_limit requiredinteger
sticker_spam_window_s requiredinteger
media_restrict_seconds requiredinteger
captcha_timeout_seconds requiredinteger
functions_fun requiredboolean
functions_utility requiredboolean
sfw requiredboolean
language requiredstring
publisher_post requiredboolean
publisher_ask requiredboolean
publisher_members_only requiredboolean
thread_posts string | nullthe pinned topic id, or null for none (v1 wrote '9999')
max_posts requiredinteger
doomlist_enabled requiredboolean

refusals

status
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s "$CB_QA_API/groups/$CB_QA_GROUP/config" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

PATCH/groups/{group_id}/configbearer

Change some of a group's settings

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

namein · type
group_id requiredpath · integerthe 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.

fieldtype
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.

fieldtype
group_id requiredinteger
config requiredGroupConfigValues
changed requiredstring[]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).

fieldtype
allow_furbots requiredboolean
sticker_spam_limit requiredinteger
sticker_spam_window_s requiredinteger
media_restrict_seconds requiredinteger
captcha_timeout_seconds requiredinteger
functions_fun requiredboolean
functions_utility requiredboolean
sfw requiredboolean
language requiredstring
publisher_post requiredboolean
publisher_ask requiredboolean
publisher_members_only requiredboolean
thread_posts string | nullthe pinned topic id, or null for none (v1 wrote '9999')
max_posts requiredinteger
doomlist_enabled requiredboolean

refusals

status
400the patch carried no settings
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s -X PATCH "$CB_QA_API/groups/$CB_QA_GROUP/config" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

GET/groups/{group_id}/rulesbearer

Read the group's rules

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

namein · type
group_id requiredpath · integerthe 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.

fieldtype
group_id requiredinteger
body requiredstring | null
updated_by requiredinteger | null
updated_at requiredstring<date-time> | null

refusals

status
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s "$CB_QA_API/groups/$CB_QA_GROUP/rules" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

PUT/groups/{group_id}/rulesbearer

Set the group's rules — what /newrules does, over HTTP

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

namein · type
group_id requiredpath · integerthe 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.

fieldtype
body requiredstring

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.

fieldtype
group_id requiredinteger
body requiredstring | null
updated_by requiredinteger | null
updated_at requiredstring<date-time> | null

refusals

status
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s -X PUT "$CB_QA_API/groups/$CB_QA_GROUP/rules" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

GET/groups/{group_id}/welcomebearer

Read the group's welcome message

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

namein · type
group_id requiredpath · integerthe 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.

fieldtype
group_id requiredinteger
body requiredstring | null
updated_by requiredinteger | null
updated_at requiredstring<date-time> | null

refusals

status
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s "$CB_QA_API/groups/$CB_QA_GROUP/welcome" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

PUT/groups/{group_id}/welcomebearer

Set the group's welcome message — what /newwelcome does, over HTTP

<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

namein · type
group_id requiredpath · integerthe 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.

fieldtype
body requiredstring

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.

fieldtype
group_id requiredinteger
body requiredstring | null
updated_by requiredinteger | null
updated_at requiredstring<date-time> | null

refusals

status
401no bearer token, or one that did not verify
403an admin whose token lacks the scope; ask /oauth2/token for a better one
404no such group — or the caller does not administer it, which answers alike
422Validation Error

try it

curl -s -X PUT "$CB_QA_API/groups/$CB_QA_GROUP/welcome" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

GET/mebearer

The caller, their scopes, and the groups they administer

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.

fieldtype
user_id requiredinteger
scopes requiredstring[]
audience string | nullthe token's `aud`, if it carries one
is_bot_admin booleanwhether this caller runs the deployment — the Mini App draws its `/admin` screens from this rather than from a 403 it had to provoke
groups requiredAdministeredGroup[]

AdministeredGroup

One group the caller administers, as group_admins records it.

fieldtype
group_id requiredinteger
title requiredstring | null
username requiredstring | null
chat_type requiredstring | null
role requiredstringwhat `group_admins` says: `creator` or `administrator`
anonymous requiredboolean | null

refusals

status
401no bearer token, or one that did not verify

try it

curl -s "$CB_QA_API/me" \
     -H "Authorization: Bearer $CB_QA_ADMIN_TOKEN" | jq

health

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

Is the process up?

Process is up. No dependency checks — a DB blip must not trigger restarts.

response

Liveness

What a liveness probe gets: the process identifying itself.

fieldtype
status requiredstring
service requiredstring
version requiredstring
cython requiredbooleanwhether the compiled hot path is in this build

try it

curl -s "$CB_QA_API/healthz" | jq

GET/readyz

Are this process's dependencies reachable?

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.

fieldtype
ready requiredboolean
postgres requiredboolean
valkey requiredboolean

refusals

status
503pulled 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/

Is the bot online, and in how many groups?

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.

fieldtype
status requiredstring
number_chats requiredintegerthe real count; v1 returned a module constant nothing updated

try it

curl -s "$CB_QA_API/" | jq

GET/.well-known/jwks.json

The signing keys every token here verifies against

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.

fieldtype
keys requiredobject[]public JWKs, `kid` and all

try it

curl -s "$CB_QA_API/.well-known/jwks.json" | jq

GET/.well-known/openid-configuration

Discovery — where the token endpoint is and which grants it takes

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.

fieldtype
issuer requiredstring
jwks_uri requiredstring
response_types_supported requiredstring[]
subject_types_supported requiredstring[]
id_token_signing_alg_values_supported requiredstring[]
token_endpoint requiredstring
revocation_endpoint requiredstring
token_endpoint_auth_methods_supported requiredstring[]
grant_types_supported requiredstring[]
scopes_supported requiredstring[]

try it

curl -s "$CB_QA_API/.well-known/openid-configuration" | jq

POST/login

Exchange a Telegram login-widget payload for a JWT (v1's endpoint)

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.

fieldtype
status requiredstring
accessToken requiredstringRS256, verifiable against `/.well-known/jwks.json`; carries no `scope` claim, which `cb_api.security` reads as read-only

refusals

status
400an empty payload — v1's `Missing data`
401the 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
422Validation Error

try it

curl -s -X POST "$CB_QA_API/login" | jq