API pipeline: Submit your Telegram groups, find groups by keyword, place or rubric, vet them and attach them to a campaign
Index: API pipelines — index
Goal: choose the Telegram groups a campaign works in: submit your own groups by username, find more in the open index by keyword, place or rubric, vet them, attach groups or presets of groups to a campaign, and exclude groups from it.
Model
- A group is a
telegram_groupcard with handletgg.<username>. Group cards belong to Cabi.Chat: every signed-in user may read and use them; nobody edits them. Anonymous callers see no groups. - A preset is a workspace of groups; presets nest. You own presets, never group cards.
- A campaign's
tg_groupstakes group nonces, preset nonces, or both. Presets are expanded at run time to the groups the campaign owner can reach.
A. Submit your own groups
1. Submit usernames. One call creates a new preset and files every username.
POST /v0/my/telegram_groups
Idempotency-Key: groups-phuket-1
{"title": "Phuket expats", "project": "<project nonce>",
"telegram_group_handles": ["@example_expats", "https://t.me/example_market", "t.me/+AbCdEf"]}
→ {"workspace": {"nonce": "470350000000000001", "card_type": "workspace", "visibility": "Private", …},
"handles": [
{"handle": "t.me/+AbCdEf", "state": "rejected"},
{"handle": "example_expats", "state": "contained", "nonce": "470350000000000002"},
{"handle": "example_market", "state": "proposed", "nonce": "470350000000000003"}],
"proposals_workspace": "470350000000000005"}
- The body is a workspace create (
title,content,project,visibility, …) plustelegram_group_handles. - Usernames are trimmed,
https://,t.me/,@and a trailing/are stripped, then lowercased; valid:[a-z0-9_], 4+ characters. Invite links (t.me/+…,t.me/joinchat/…) andt.me/s/…arerejected. Duplicates collapse. contained: already in the index and now in your preset;nonceis the group.proposed: new to Cabi.Chat and queued for checking;nonceis the proposal, not a group.- Each call makes a new preset; add known groups to an existing preset by nonce (step 3).
2. Behind: checking (asynchronous). Cabi.Chat checks each proposed username: it must be a public group (not a broadcast channel), not marked scam. Groups that pass are added to the index and to your preset; the others are not added.
3. See the result.
GET /v0/my/workspaces/470350000000000001?contained_limit=200
→ {"contained": [{"card_type": "telegram_group", "nonce": "…", "title": "Example market", …}], "contained_next_offset": null}
One username: GET /v0/graph/resolve/@tgg.example_market answers once the group is in the index. Groups that pass the check show up in your preset.
Add a known group to any of your presets: POST /v0/my/workspaces/{ws}/{group nonce} → 204; remove it: DELETE on the same path.
B. Find groups in the index
Keyword:
POST /v0/search/graph
{"query": "phuket", "card_type": "telegram_group", "limit": 20, "page": 1}
→ {"hits": [{"card_type": "telegram_group", "nonce": "…", "handle": "tgg.example_phuket",
"title": "…", "item_count": 13934, "location": {…}, …}],
"total_hits": 115, "page": 1, "total_pages": 6}
/v0/search/graph combines keyword and meaning; /keyword and /vector run one side only. query is cut at 200 characters.
Place. Filters take place nonces (rubric cards of kind place), not geonameids or ISO codes:
GET /v0/search/geo/cities?q=phuket→{"geonameid": 1151254, "name": "Phuket", "country_iso": "TH", …}GET /v0/graph/resolve/@rub.geo-1151254→{"card_type": "rubric", "nonce": "<place nonce>", "kind": "place"}. A country is@rub.geo-<iso lowercase>, e.g.@rub.geo-th.{"query": "", "card_type": "telegram_group", "filters": {"locations": ["<place nonce>"], "exclude_terms": ["rent"]}}
locations and places_in match the place and everything inside it. Place nonces go only there; inside rubrics they match nothing.
Rubric: "filters": {"rubrics": [["<rubric nonce>"]]}; inner arrays OR, outer AND; rubrics_not excludes. Find a rubric: {"query": "dating", "card_type": "rubric", "filters": {"kind": "tag"}}.
Inside a preset: add "workspace": "<preset nonce>".
Allowed filter keys: rubrics, rubrics_not, rubrics_in, locations, places_in, tags_in, project, preference_countries, preference_city {nonce, lat, lon}, radius_km, roots_only, kind, card_type, exclude_terms; any other → 400 unknown filter: <key>.
Member count and ratings are on each group's overview (below). Recently added groups are found best by keyword.
Vet a group:
GET /v0/graph/telegram_groups/@tgg.example_phuket/overview
→ {"tile": {…}, "username": "example_phuket", "members_count": 13934,
"stats": {"spam": 10.4, "density": 13.8, "openness": 60.9, "moderation": 17.0,
"audience": 58.1, "pace": 34.0, "msgs_per_day": 221.5, "sample": 200, "rated_at": "…"},
"mine": {"totals": {"sent": 0, "deleted": 0, "replied": 0, "joined": 0, …}, "timeline": […], "campaigns": […], "replies": […]}}
Ratings are 0–100; stats is null until the group is rated. mine is your own record in that group. The group page without stats: GET /v0/graph/telegram_groups/{nonce | @tgg.<username>}.
Ready presets (GET /v0/graph/workspaces/{ident}?contained_limit=&contained_offset=): @ws.tgg.geo-<geonameid>.2026-09-12 (e.g. @ws.tgg.geo-1151254.2026-09-12, Phuket) and @ws.tgg.iab-<topic>.2026-09-12 (e.g. @ws.tgg.iab-travel.2026-09-12): snapshots of 2026-09-12.
Lists that keep themselves up to date. Some public group lists are maintained by Cabi.Chat automatically and change on their own, for example groups heavy with spam or groups where many messages get deleted. They carry the rubric «Auto-maintained» (find it with {"query": "Auto-maintained", "card_type": "rubric"}, then search workspaces with that rubric). Use them like presets, for example in not_tg_groups to keep a campaign out of such groups.
C. Put groups into a campaign
PATCH /v0/my/campaigns/<campaign nonce>
{"tg_groups": ["<preset nonce>", "<group nonce>"]}
- The list replaces the whole slot. Entries are numeric nonces as strings (not
@handles). Someone else's Private card → 400tg_groups: card <n> not found or not available to you. - The response returns the slot as
telegram_groups_list(nonces) andlists.telegram_groups(tiles). - Attach groups already in the index; groups still being checked join your preset once they pass. Kinds and lifecycle: API pipeline: Create, configure, start, stop, measure and delete a Telegram campaign of any kind
Exclude groups. not_tg_groups lists groups the campaign skips; it takes the same entries as tg_groups. Useful to drop a few groups from a ready preset:
PATCH /v0/my/campaigns/<campaign nonce>
{"tg_groups": ["<preset nonce>"], "not_tg_groups": ["<group nonce>", "<other preset nonce>"]}
- A group in both lists is skipped; a preset in the exclude list skips every group in it.
[]skips nothing and clears earlier exclusions; leaving the field out keeps the list as it is.- It comes back as
not_telegram_groups_list(nonces) andlists.not_telegram_groups(tiles).
API surface
| Endpoint | Purpose | FE | AI / oracle |
|---|---|---|---|
POST /v0/my/telegram_groups |
Submit usernames as a new preset | yes (Submit Telegram groups) | yes, the way to bring own groups |
GET /v0/my/workspaces/{ws} |
Read a preset; see which groups arrived | yes | yes |
POST / DELETE /v0/my/workspaces/{ws}/{group} |
Add / remove a known group | yes | yes |
GET /v0/graph/resolve/{@handle} |
@tgg.<username> → group; @rub.geo-<id> → place |
yes | yes |
POST /v0/search/graph (/keyword, /vector) |
Find groups by keyword, place, rubric, inside a preset | yes | yes, main discovery |
GET /v0/search/geo/cities, /countries |
City or country lookup | yes | yes, then resolve |
GET /v0/graph/telegram_groups/{ident}, /overview |
Group page; members, ratings, your record | yes | yes, vetting |
GET /v0/graph/workspaces/{ident} |
Ready presets | yes | yes |
POST / PATCH /v0/my/campaigns… tg_groups, not_tg_groups |
Attach or exclude groups or presets | yes | yes |