Search Telegram channels by keyword
Finds public Telegram channels by keyword, so you can pick the channels to read with /v1/telegram/profile and /v1/telegram/profile/posts. Channels are discovered through a public web search index limited to t.me pages, so this finds what that index has already seen: it is not real time, a channel or message from the last few days may not appear yet, and private channels and invite-only groups never appear. Returns one row per channel, in the order the search found them, on the same Author shape /v1/telegram/profile returns: author.username is the public handle (pass it as handle), author.url is https://t.me/<handle>, and author.display_name is the channel's title when the search result carried it (null when the result showed only the handle). author.ext.search_hit says how the channel was found: match is channel_page (the channel's own page) or post_in_channel (a message or the message preview), title and snippet are the hit as the index showed it, evidence_url is the first page that matched, and matched_urls lists every t.me page the search matched for that channel, so you can open the exact messages. The snippet is the index's text, not the channel's description, so author.bio stays null on a plain call, as do author.followers, author.avatar_url and author.verified. Send include=details and every channel is joined, in the same call, to the profile lookup: the exact subscriber count (author.followers), the description (author.bio), the avatar, the verified flag and, where it was null, the title land on each row. Words in query match loosely; put a phrase in double quotes to require it, and add -word to exclude a word. Cost: 2 credits for the page, plus 1 credit per channel filled from a fresh lookup with include=details (at most 10 a page, so 12 credits at most); channels already in cache are free, channels that could not be filled are refunded, and a search that finds no public channel is a 404 at 0 credits. Time: a plain page takes about 2 to 12 seconds; include=details adds a few seconds on a fresh page (the lookups run in parallel and the call waits for the slowest, never more than 10 seconds) and nothing when the channels are already cached. The response carries a hydration block itemising rows, lookups, cache hits, credits and milliseconds. A page holds up to 10 channels, fewer when several hits belong to one channel. Forward pagination.next_cursor as cursor for the next page; the same channel can appear again on a later page. Keyword hits include coupon and reseller channels that merely contain your words: send relevance=filter to drop the channels that are not about your query, or add relevant_to= with a sentence describing the channels you want (for example shops selling cheap or replica watches) to judge them against that instead; each channel is judged on its title and the search snippet.
Request
- Base URL: https://www.socialcrawl.dev/v1
- URL: https://www.socialcrawl.dev/v1/telegram/search
- Auth: API key in header x-api-key
Query parameters
Keywords describing the channels or messages to find, for example byd spare parts. Put a phrase in double quotes to require it. Keywords only: the search is already limited to public Telegram pages, so a site: operator is refused before billing.
Page number, starting at 1. Forwarding pagination.next_cursor as cursor does the same. Native upstream cursor param. Send the universal cursor instead: the API maps it to this name for you.
Only pages the search index dates within this window: last-hour, last-day, last-week, last-month or last-year. Useful for monitoring new activity.
Two-letter country code (US, AE, IN, KR) whose results to prefer.
Return at most this many channels from the page (1 to 10). With include=details it also lowers the credits held, one per channel.
Set to details (one token only) to fill, on every channel in this one call, author.followers (the exact subscriber count), author.bio (the description), author.avatar_url, author.verified, and the title where it was null. Holds 1 credit per channel (at most 10 a page) and keeps only the channels filled from a fresh lookup; channels already in cache and channels the lookup cannot fill are refunded, so a page is never more than 12 credits. Read data.hydration for the rows, the credits held and kept, and the time. Without it the call is unchanged.
Optional. Without this param every row already carries computed.relevance against your query, free ({ p, sense, depth, spam }: is this row about what your query means, or a different thing that shares its words?), and nothing is dropped or reordered. score asks for it explicitly and waits for every row; filter also drops the rows that are not about your query, and lists their ids in data.relevance.dropped_ids. Your query is the topic; nothing to configure. A row that could not be judged is never dropped and carries relevance: null. Pagination is unchanged, so a filtered page can hold fewer rows. Free with your query as the topic; with relevant_to it adds 1 credit per started 25 newly judged rows (rows already judged for the same topic are free, and so is a cached page).
Optional, only with relevance. The probability (0 to 1) a row must reach to be kept by relevance=filter. Default 0.5. Lower keeps more rows, higher keeps fewer.
Optional, only with relevance. Up to 200 characters describing what you mean, used as the topic instead of the query. Use it when the query is ambiguous, for example query=cleopatra with relevant_to=Cleopatra, the IGT slot game.
Optional, on (the default) or off. By default every row gains free SocialCrawl judgments (computed.labels, and computed.relevance on search endpoints), reported in data.labels (mode default) and data.relevance (origin default), each with a status (complete, partial or skipped) and pending: the rows still being judged when the page was sent, which carry null now and are filled on your next call or cached read. Default judgments never add credits, never change an existing field, and never drop or reorder a row. off returns the page exactly as before, with none of those keys. label=none does the same.
Optional. When 1, return a cost preview for this labelled or relevance-filtered request without fetching the page or judging any row. data.estimate reports rows_expected, rows_cached, label_credits_min, label_credits_max and base_credits. 0 credits charged.
Optional. When goal, keep the rows and fields needed for the goal you pass in goal= (plus any that are uncertain, and the first and last), and replace the rest with a stub. data.held_back lists the held ids and a recall id that re-reads the full page from cache at no extra charge. Without this param the page is unchanged.
Required by fit=goal. What you are trying to do, in your own words, up to 300 characters.
Optional, only with fit=goal. Soft cap on how much of the page to keep, in tokens. Uncertain blocks and the first and last block are kept even if they exceed it.
Universal pagination cursor. Send pagination.next_cursor from the previous response back verbatim: the API maps it to this endpoint's native page (page style). You never construct, decode, or look up a cursor. Omit it for page 1.
Headers
Send no-cache to bypass the response cache and force a live fetch. Billed at the normal endpoint cost; the fresh result is written back to cache for the next caller. Only the no-cache directive triggers this. See the Response Schema guide for details.
Optional UUID that makes the request safely retriable. The key is reserved atomically per account before billing. A concurrent retry receives 409 IDEMPOTENCY_IN_PROGRESS with a Retry-After of 1 second while the original is young, growing to a quarter of the time it has been running (at most 30); a crashed reservation expires after 5 minutes. A replay keeps the cached payload immutable except for billing metadata: credits_used becomes 0, idempotent_replay becomes true, and credits_remaining is refreshed to the current balance. A known current balance appears in both the body and X-Credits-Remaining header; no balance row resolves to 0. On a transient lookup failure, body credits_remaining is null and X-Credits-Remaining is omitted. Completed outcomes expire after 24 hours. If the claim store is unavailable, the request fails closed before billing. GET endpoints do not combine idempotent replay with Server-Sent Events: use Accept: application/json when sending this header. The unsupported key-plus-SSE combination is rejected before billing.
Response
Successful response
Example response
{
"success": true,
"platform": "telegram",
"endpoint": "/v1/telegram/search",
"credits_used": 1,
"credits_remaining": 4999,
"request_id": "req-abc123",
"idempotent_replay": true,
"meta": {
"hint": {
"code": "search_multi",
"message": "You searched \"matcha\" on 3 platforms one call at a time; /v1/search/multi returns each platform's own search rows in one call at the same price per platform, and a platform with no rows is not charged.",
"method": "GET",
"path": "/v1/search/multi?query=matcha&platforms=instagram,tiktok,youtube"
}
},
"pagination": {
"next_cursor": "sc.eyJ2IjoyLCJjIjoiMiIsInAiOiJwYWdlIn0",
"has_more": true,
"page_size": 30,
"stopped_at": "known_id"
}
}Changes
No recorded changes to this operation across all 1 revision of this API.