API
Authenticate with an API key and drive accounts, inbox, bots, analytics and workflows over HTTP
Everything the dashboard does, it does over this HTTP API. Every path, body and status code below was exercised against a live workspace before this page was published.
Base URL
https://telegramos.netRequests and responses are JSON (Content-Type: application/json). Exports return a file (.xlsx / .csv) and media downloads return bytes.
Authenticating
Create a key under Settings → API keys, then send it as the X-API-Key header:
curl -H "X-API-Key: tgp_your_key_here" \
https://telegramos.net/api/accountsThe key is shown once, at creation. It is stored as a SHA-256 hash, so nobody — including us — can recover it. Lose it and you create a new one.
| Property | Value |
|---|---|
| Header | X-API-Key |
| Format | tgp_ followed by 54 URL-safe characters |
| Default lifetime | 90 days (expires_days, 1–365) |
| Keys per workspace | 20 |
| Revocation | Instant, from Settings → API keys |
Changing your password, resetting it, disabling 2FA or logging out everywhere revokes every key issued before that moment, permanently.
A browser session uses a cookie instead. The two paths never mix: an X-API-Key request is authenticated purely by that key, and key management (/api-keys) deliberately refuses keys — a key cannot mint or revoke another key (403).
Permissions
The permissions you tick at creation are enforced on every request.
Verbs — one is always required:
| Permission | Grants |
|---|---|
read | GET, HEAD |
write | POST, PUT, PATCH, DELETE |
Areas — optional. A key that ticks no area reaches every area; a key that ticks one or more is confined to those:
| Permission | Covers |
|---|---|
accounts | Accounts, proxies, warm-up, scraping, niche finder, mass DM, lead packages |
inbox | Conversations, messages, groups, saved replies, auto-reply rules, notifications |
bots | Bots, flows, campaigns, channel syncs, bot database |
analytics | Analytics and AI endpoints |
tasks | Workflows and executions |
read alone is a read-only key over the whole platform. read + write + inbox can do anything in the inbox only — it gets 403 on /api/accounts.
Request ids
Every response carries an X-Request-ID. Send your own and we use it; send nothing and we generate one:
curl -i -H "X-API-Key: $KEY" -H "X-Request-ID: my-job-42" \
https://telegramos.net/api/accounts
...
x-request-id: my-job-42Each request is logged on our side with that id, the path, the status, the duration, the key prefix (never the key) and the workspace:
api.request service=account-service request_id=my-job-42 method=GET
path=/api/accounts status=200 duration_ms=144 auth=key:tgp_t24zIO user=<workspace>Log the id on your side. While the platform is in beta it is the fastest way for us to find your exact call — quote it in a support ticket and we can pull the whole request path without guessing.
Errors
| Status | Meaning |
|---|---|
400 | Malformed request, or a path id that isn't a UUID |
401 | Missing, unknown, expired or revoked key |
402 | Your plan doesn't include this feature, or the wallet can't cover the run |
403 | Valid key, but its permissions don't cover this call |
404 | No such resource, or it belongs to another workspace |
409 | The account can't do this right now (spam-limited, still warming) |
422 | Body failed validation — the response names the field |
429 | Too many requests, or the account hit its hourly/daily send budget |
503 | A dependency is down; retry |
Errors carry detail:
{"detail": "This API key is scoped to inbox — not 'accounts'."}Another workspace's object is 404, never 403 — the API never confirms that an id exists.
Conventions
- Ids are UUIDs. A non-UUID path segment is
400or404, never a 500. - Sending is queued, not synchronous. Endpoints that make an account act return
202with atask_id. The account then sends under the anti-ban pacing rules (see Limits & quotas). Poll the resource to see the result. - Everything is workspace-scoped. You never pass a workspace id; the key carries it.
- List endpoints take
limit/offsetorpage/page_sizedepending on the resource; both are optional.
Accounts
Area: accounts.
| Method | Path | Does |
|---|---|---|
GET | /api/accounts | List accounts |
POST | /api/accounts | Add an account |
GET | /api/accounts/{id} | One account |
PATCH | /api/accounts/{id} | Rename, re-tag, change settings |
DELETE | /api/accounts/{id} | Remove an account |
POST | /api/accounts/import | Import a session file |
POST | /api/accounts/batch-import | Import many at once |
POST | /api/accounts/login/phone/send-code | Start a phone login |
POST | /api/accounts/login/phone/verify-code | Submit the code |
POST | /api/accounts/login/phone/verify-2fa | Submit the 2FA password |
POST | /api/accounts/login/qr/start | Start a QR login |
POST | /api/accounts/login/qr/poll | Poll it (long-poll, ~20s) |
POST | /api/accounts/login/cancel | Abandon a login |
POST | /api/accounts/{id}/power | Sleep or wake the account |
POST | /api/accounts/{id}/profile | Set name, bio, photo |
POST | /api/accounts/{id}/clone-profile | Copy another account's profile |
GET | /api/accounts/{id}/privacy | Privacy settings |
GET | /api/accounts/{id}/photos | Profile photos |
GET | /api/accounts/{id}/stats | Messages sent/received today |
GET | /api/accounts/{id}/health-log | Health score history |
GET | /api/accounts/{id}/risk | Current ban-risk read |
GET | /api/accounts/{id}/connections | Connection audit log |
GET | /api/accounts/{id}/sessions | Login prompts — rented accounts only |
GET | /api/accounts/{id}/vault | Session-vault state |
GET POST | /api/accounts/{id}/backfill-history | Read / start an old-chat pull |
POST | /api/accounts/{id}/backfill-history/cancel | Stop a running pull |
POST | /api/accounts/{id}/reset-status | Clear an error state |
GET | /api/accounts/{id}/avatar | Profile photo, as bytes |
DELETE | /api/accounts/{id}/photos/{photoId} | Delete one profile photo |
POST | /api/accounts/{id}/username-check | Is a handle free, before you claim it |
POST | /api/accounts/{id}/contact-policy | Who may add this account to groups |
POST | /api/accounts/{id}/group-invites | Add people to a group — see the note below |
POST | /api/accounts/{id}/skip-warmup | Backdate the warm-up clock — owned accounts only |
GET | /api/accounts/{id}/session/export | Download the session — owned accounts only |
POST | /api/accounts/{id}/sessions/{hash}/decision | Approve or reject a login prompt |
POST | /api/accounts/bulk | Activate / deactivate / delete many at once |
POST | /api/accounts/bulk/listener | Turn the inbound listener on or off fleet-wide |
POST | /api/accounts/bulk/contact-policy | One contact policy across the fleet |
The session vault lives under the same account: POST /api/accounts/{id}/vault/enable, /disable, /lock, /unlock, /rotate, with GET /api/accounts/{id}/vault for the current state.
Note `/group-invites` is the strictest endpoint in the product, because adding people is the fastest way to lose an account. It refuses with `409` unless the account has finished warm-up, holds a clean @SpamBot verdict less than 36 hours old, and is not reply-only.
Note `/sessions` is for rented accounts. On an account you own it answers `404 no live allocation for this account on your workspace` — that is the correct answer, not a fault.
Proxies
| Method | Path | Does |
|---|---|---|
GET | /api/proxies | List proxies |
POST | /api/proxies | Add one |
POST | /api/proxies/bulk | Add many |
PATCH | /api/proxies/{id} | Edit |
POST | /api/proxies/{id}/rotate | Rotate the exit IP |
DELETE | /api/proxies/{id} | Delete |
PATCH DELETE | /api/proxies/{id}/assign | Attach / detach an account |
GET | /api/proxies/{id}/usage | Traffic used |
POST | /api/proxies/{id}/check | Connectivity test |
Warm-up
| Method | Path | Does |
|---|---|---|
GET PUT | /api/warmup/settings | Read / replace warm-up settings |
GET | /api/warmup/overview | Fleet warm-up state |
GET | /api/warmup/plan | The generated plan |
POST | /api/warmup/plan/apply | Apply it |
DELETE | /api/warmup/plan | Drop it |
POST | /api/warmup/dry-run | Preview without applying |
GET | /api/warmup/pairs | Which accounts talk to which |
GET | /api/warmup/groups | Groups in the plan |
GET | /api/warmup/events | What warm-up actually did |
GET POST | /api/warmup/channels | Channels the fleet joins |
DELETE | /api/warmup/channels/{id} | Remove one |
GET | /api/warmup/accounts | Per-account warm-up state |
POST | /api/warmup/accounts/{id}/social | Toggle social warm-up |
Scraping
| Method | Path | Does |
|---|---|---|
POST GET | /api/scraping/jobs | Start / list jobs |
GET | /api/scraping/jobs/{id} | Status |
POST | /api/scraping/jobs/{id}/cancel | Cancel |
GET | /api/scraping/jobs/{id}/results | Results as JSON |
GET | /api/scraping/jobs/{id}/export | Results as .xlsx |
GET | /api/scraping/jobs/{id}/export.csv | Results as CSV |
POST | /api/scraping/jobs/{id}/webhook | Push results to your URL |
GET | /api/scraping/usage | Quota used this period |
POST /api/scraping/jobs
{
"name": "leads",
"group_links": ["https://t.me/somepublicgroup"],
"limit_per_group": 300,
"max_accounts": 5,
"patterns": [],
"keyword_filter": []
}patterns empty means phone numbers. A paid deep run is billed per message scanned and returns 402 if the wallet can't cover the worst case.
Niche finder
| Method | Path | Does |
|---|---|---|
POST | /api/niche/suggest | Keyword suggestions |
POST GET | /api/niche/jobs | Start / list jobs |
GET | /api/niche/jobs/{id} | Status |
GET | /api/niche/jobs/{id}/sources | Which groups it read |
POST | /api/niche/jobs/{id}/cancel | Cancel |
GET | /api/niche/jobs/{id}/results | Discovered audience |
GET | /api/niche/jobs/{id}/export | The same, as .xlsx |
POST /api/niche/jobs
{
"name": "crypto traders",
"keywords": ["trading", "signals"],
"active_within_days": 30,
"msg_scan_limit": 3000,
"max_accounts": 5,
"extract_members": false
}extract_members dumps raw member lists; it is off by default because it is the ban-risky read.
Lead packages and watches
| Method | Path | Does |
|---|---|---|
GET | /api/packages | Packages on sale |
GET | /api/packages/{id} | One package |
POST | /api/packages/{id}/buy | Buy it (charges the wallet) |
GET | /api/packages/{id}/download | Download a bought one |
POST | /api/packages/{id}/publish | Put one of yours on sale |
DELETE | /api/packages/{id} | Take it down |
GET | /api/watches/plans | Recurring-scrape plans |
GET POST | /api/watches | List / create a watch |
GET | /api/watches/{id} | One watch |
POST | /api/watches/{id}/pause | Pause |
POST | /api/watches/{id}/resume | Resume |
DELETE | /api/watches/{id} | Delete |
Mass DM
| Method | Path | Does |
|---|---|---|
GET | /api/mass-dm/accounts | Accounts eligible to send |
POST GET | /api/mass-dm/jobs | Create / list campaigns |
GET | /api/mass-dm/jobs/{id} | Progress |
GET | /api/mass-dm/jobs/{id}/recipients | Per-recipient state |
GET | /api/mass-dm/jobs/{id}/replies | Who answered |
GET | /api/mass-dm/jobs/{id}/export | Everything, as .xlsx |
GET POST | /api/mass-dm/suppression | Do-not-contact list |
DELETE | /api/mass-dm/suppression/{tgUserId} | Remove one entry |
POST | /api/mass-dm/jobs/{id}/pause | Pause a running campaign |
POST | /api/mass-dm/jobs/{id}/resume | Resume a paused one |
POST | /api/mass-dm/jobs/{id}/cancel | Stop it for good |
GET | /api/mass-dm/contacts/{tgUserId} | What we know about a contact |
POST /api/mass-dm/jobs
{
"recipients": [{"tg_user_id": 123456789, "username": "someone"}],
"message_text": "hi {first_name}",
"account_ids": ["<account-uuid>"],
"per_account_daily_cap": 20,
"followups": []
}Supply either recipients or niche_job_id, not both. Suppression takes a list:
POST /api/mass-dm/suppression
{"tg_user_ids": [777000777], "reason": "opted out"}Inbox
Area: inbox.
| Method | Path | Does |
|---|---|---|
GET | /api/conversations | List conversations |
POST | /api/conversations | Start a new chat (202) |
GET | /api/conversations/search | Full-text search (?q=) |
GET | /api/conversations/{id}/messages | Messages in a thread |
POST | /api/conversations/{id}/messages | Send a reply (202) |
PATCH | /api/conversations/{id} | Assign, tag, resolve, mute |
POST | /api/conversations/{id}/mark-read | Mark read |
DELETE | /api/conversations/{id} | Remove the thread |
POST | /api/conversations/media | Upload an attachment, get a media_key |
GET | /api/conversations/{id}/messages/{msgId}/media | Download an attachment |
POST | /api/conversations/{id}/messages/{msgId}/fetch-media | Pull a placeholder's media |
GET POST | /api/conversations/{id}/notes | Internal notes |
DELETE | /api/conversations/{id}/notes/{noteId} | Delete a note |
POST | /api/conversations/{id}/block | Block the peer |
POST | /api/conversations/{id}/leave | Leave the group |
GET | /api/conversations/sla-stats | Response-time stats |
GET | /api/conversations/account-stats | Per-account volumes |
GET | /api/conversations/activity-series | Message volume over time |
GET | /api/conversations/reply-stats | Reply rate — needs ?source= |
Filters on the list: account_id, status, tags, unread_only, source (campaign / mass_dm), campaign_id, job_id, page, page_size.
reply-stats requires source=campaign&campaign_id=... or source=mass_dm&job_id=...; without them it answers 422/400 naming what's missing.
Following a thread
Inbound messages are only recorded for accounts whose message listener is on. An account with power_sender_only: true in GET /api/accounts sends but never receives, so its threads stay empty however often you poll — turn the listener on for it first.
GET /api/conversations/{id}/messages pages backwards by default (before, before_msg_id, limit up to 200) — that is what a UI wants when you scroll up.
A monitor wants the opposite. Pass after_msg_id with the telegram_msg_id of the newest message you already hold and you get only what arrived since, oldest first:
GET /api/conversations/{id}/messages?after_msg_id=19626585&limit=200telegram_msg_id rises monotonically inside a chat, so the cursor never repeats a message and never skips one. Keep the highest id you have seen, poll with it, and there is nothing to de-duplicate. after_msg_id overrides before/before_msg_id if you send both.
Each message carries direction: in for messages the account received, out for messages sent through this platform. Messages typed by hand in a Telegram client on the same account are not recorded — Telegram delivers them as your own outgoing updates and the listener ignores them.
For push instead of polling, see Webhooks and Real-time events below.
Sending
Reply into an existing thread:
POST /api/conversations/{id}/messages
{"content": "Thanks — one moment.", "media_key": null}Start a thread with someone not in the inbox yet:
POST /api/conversations
{
"account_id": "<account-uuid>",
"recipient_type": "username",
"recipient": "someone",
"content": "hello"
}recipient_type is username, phone or user_id. Both endpoints answer 202:
{"task_id": "...", "conversation_id": null, "queued": true,
"note": "Message queued. It will appear in the inbox once Telegram acknowledges the send."}The message appears in GET /api/conversations/{id}/messages once Telegram acknowledges it — typically seconds, longer if the account is at its hourly pacing cap.
Anti-ban guards answer before anything is queued, so handle them:
- A spam-limited account cold-starting a chat:
Telegram has limited this account, so it can't start new conversations right now — it can still reply to people who messaged it. - A warming account cannot initiate, only reply.
- The content filter rejects the text with
422and a bracketed code —[REPEATED_CHARS],[EXCESSIVE_CAPS],[TOO_MANY_LINKS],[BLOCKED_LINK],[SPAM_PATTERN]. Fix the text and resend; retrying the same body always fails. - The rolling send budget answers
429with[HOURLY_MESSAGE_LIMIT]or[DAILY_MESSAGE_LIMIT]. Each account may send 10 messages per hour and 40 per day; the hourly window rolls continuously, so back off rather than rewriting the message. An account whose health score has dropped gets a proportionally smaller budget.
Only a 202 means the send is on its way. If it still fails later — the recipient blocked the account, the account dropped offline — the reason arrives as an inbox.message_failed webhook carrying the same task_id.
Groups, replies, rules, notifications
| Method | Path | Does |
|---|---|---|
GET POST | /api/groups | Tracked groups and channels |
PATCH DELETE | /api/groups/{id} | Edit / untrack |
POST | /api/groups/{id}/sync | Refresh member and post counts |
GET | /api/groups/{id}/stats | Activity |
GET POST | /api/saved-replies | Canned replies |
PUT DELETE | /api/saved-replies/{id} | Edit / delete |
GET POST | /api/auto-reply-rules | Auto-reply rules |
POST | /api/auto-reply-rules/bulk | Create many |
PATCH DELETE | /api/auto-reply-rules/{id} | Edit / delete |
GET | /api/notifications | Notification feed |
GET | /api/notifications/unread-count | Badge count |
PATCH | /api/notifications/read-all | Mark all read |
GET PUT | /api/notifications/preferences | Per-category preferences |
PATCH | /api/notifications/{id}/read | Mark one read |
DELETE | /api/notifications/{id} | Dismiss one |
GET POST | /api/api-connectors | External reply connectors |
GET | /api/api-connectors/kinds | Connector types you can create |
PATCH DELETE | /api/api-connectors/{id} | Edit / remove |
POST | /api/api-connectors/{id}/test | Test one |
GET | /api/conversations/{id}/avatar | Chat photo, as bytes |
GET | /api/conversations/{id}/peer-avatar | The other side's photo |
POST /api/groups
{"identifier": "https://t.me/somepublicgroup", "type": "group", "via_account_id": "<uuid>"}
POST /api/saved-replies
{"title": "greeting", "content": "Hi {first_name}", "variables": ["first_name"]}
POST /api/auto-reply-rules
{
"account_id": "<account-uuid>",
"match_type": "contains",
"match_value": "price",
"reply_text": "Our pricing is on the site.",
"is_active": true,
"delay_min": 2,
"delay_max": 8
}match_type is one of any, exact, contains, regex.
Bots
Area: bots.
| Method | Path | Does |
|---|---|---|
GET POST | /api/bots | List / register bots |
DELETE | /api/bots/{id} | Remove |
POST | /api/bots/{id}/active | Enable / disable |
POST | /api/bots/{id}/delivery-mode | Polling or webhook |
POST | /api/bots/{id}/broadcast | Broadcast to its chats |
GET | /api/bots/{id}/chats | Chats it can reach |
GET POST | /api/bots/{id}/flows | Visual flows |
GET | /api/bots/flows/templates | Starter templates |
POST | /api/bots/{id}/flows/templates/{templateId}/instantiate | Create a flow from one |
GET PUT DELETE | /api/bots/{id}/flows/{flowId} | One flow |
POST | /api/bots/{id}/flows/{flowId}/test | Dry-run it |
GET | /api/bots/{id}/runs | Flow run history |
GET | /api/bots/{id}/runs/{runId}/steps | Per-step detail |
Campaigns
| Method | Path | Does |
|---|---|---|
GET POST | /api/campaigns | List / create (created in draft) |
GET | /api/campaigns/{id} | One campaign |
POST | /api/campaigns/{id}/start | Start |
POST | /api/campaigns/{id}/pause | Pause |
POST | /api/campaigns/{id}/stop | Stop |
PATCH | /api/campaigns/{id}/assignments | Re-assign accounts and groups |
POST | /api/campaigns/{id}/import-chatlist | Bulk-add target groups |
GET | /api/campaigns/{id}/metrics | Sends, failures, reach |
DELETE | /api/campaigns/{id} | Delete |
POST /api/campaigns
{
"name": "launch",
"message_a": {"text": "..."},
"message_b": {"link": "https://t.me/channel/123"},
"interval_minutes": 30,
"delay_min_seconds": 15,
"delay_max_seconds": 90,
"assignments": [{"account_id": "<uuid>", "group_chat_ids": [-1001234567890]}]
}Creating does not start it — call /start.
Channel syncs and the bot database
| Method | Path | Does |
|---|---|---|
GET POST | /api/channel-syncs | Channel-to-channel mirrors |
GET PATCH DELETE | /api/channel-syncs/{id} | One mirror |
POST | /api/channel-syncs/{id}/arm | Arm |
POST | /api/channel-syncs/{id}/disarm | Disarm |
POST | /api/channel-syncs/{id}/run | Run once now |
GET POST | /api/botdb/collections | Collections |
GET PUT DELETE | /api/botdb/collections/{id} | One collection |
GET POST | /api/botdb/collections/{id}/records | Records |
POST | /api/botdb/collections/{id}/query | Filter / sort / paginate |
PUT DELETE | /api/botdb/collections/{id}/records/{recordId} | One record |
GET POST | /api/botdb/collections/{id}/views | Saved views |
POST /api/botdb/collections
{"name": "leads", "schema": {"fields": [{"key": "email", "type": "text"}]}}
POST /api/botdb/collections/{id}/records
{"data": {"email": "a@b.com"}}Field definitions use key (not name), and a record whose data has an undeclared key is rejected with unknown field(s): ....
Analytics
Area: analytics.
| Method | Path | Does |
|---|---|---|
GET | /api/analytics/summary | Totals for the period |
GET | /api/analytics/daily | Day-by-day series |
GET | /api/analytics/timeline | Event timeline |
GET | /api/analytics/accounts/{id}/timeline | One account's timeline |
GET | /api/analytics/velocity | Send velocity |
GET | /api/analytics/anomalies | Detected anomalies |
GET | /api/analytics/anomalies/{accountId} | Anomalies for one account |
POST | /api/analytics/ai/anomalies/detect | Run detection now |
GET | /api/analytics/ai-insights | Narrative insights over the fleet |
GET | /api/analytics/report | Full report payload |
GET | /api/analytics/ai/summary | AI narrative summary |
GET | /api/analytics/ai/daily-digest | Daily digest (seconds, not ms) |
POST | /api/analytics/ai/suggest-replies | Reply suggestions |
POST | /api/analytics/ai/summarize-conversation | Summarise a thread |
POST | /api/analytics/ai/sentiment | Sentiment of a thread |
POST | /api/analytics/ai/send-risk | Ban-risk score for a draft |
POST | /api/analytics/ai/compose-message | Draft a message |
POST | /api/analytics/ai/suggest-posts | Content ideas |
POST | /api/analytics/ai/template-variations | Spin a template |
GET | /api/analytics/ai/quota | AI credits left |
With the workspace analytics switch off these return empty series rather than an error. AI endpoints draw on the AI quota: check /api/analytics/ai/quota first, and expect 429 once it's spent.
Workflows
Area: tasks.
| Method | Path | Does |
|---|---|---|
GET POST | /api/workflows | List / create |
GET PUT DELETE | /api/workflows/{id} | One workflow |
POST | /api/workflows/{id}/run | Run it now |
GET | /api/workflows/{id}/stats | Run counts and success rate |
POST DELETE | /api/workflows/{id}/schedule | Attach / remove a cron schedule |
GET | /api/executions | Run history |
POST | /api/executions | Start a run |
GET | /api/executions/{id} | One run |
GET | /api/executions/{id}/steps | Per-node state |
DELETE | /api/executions/{id} | Cancel a running execution |
POST /api/workflows
{"name": "nightly", "definition": {"nodes": [], "edges": []}}Support
No area — any key reaches these.
| Method | Path | Does |
|---|---|---|
GET POST | /api/support/tickets | List / open a ticket |
GET | /api/support/tickets/{id} | One ticket with its thread |
POST | /api/support/tickets/{id}/messages | Reply |
POST | /api/support/tickets/{id}/close | Close it |
POST | /api/support/tickets/{id}/seen | Mark the thread read |
GET | /api/support/unread-count | Badge count |
Identity
| Method | Path | Does |
|---|---|---|
GET | /auth/me | Who this key belongs to, and the plan |
Editing the profile (PATCH /auth/me), like key management, needs a browser session — a key answers 403.
Webhooks
A webhook is the durable way to receive events: we write the event to our database first, then POST it to your URL and keep retrying until you take it. A socket that was disconnected during your deploy loses the event; a webhook does not.
Manage them under Settings → API keys → Webhooks, or over the API (area inbox):
| Method | Path | Does |
|---|---|---|
GET | /api/webhooks | List your endpoints |
POST | /api/webhooks | Register one — the response carries the signing secret |
PATCH | /api/webhooks/{id} | Change the URL, the event list, or turn it off |
DELETE | /api/webhooks/{id} | Remove it |
GET | /api/webhooks/events | The event catalogue |
GET | /api/webhooks/{id}/secret | Read the signing secret back |
POST | /api/webhooks/{id}/rotate-secret | Issue a new secret |
POST | /api/webhooks/{id}/test | Queue a webhook.test delivery (202) |
GET | /api/webhooks/{id}/deliveries | The last deliveries, with status and error |
POST /api/webhooks
{"url": "https://your-server.example/telegramos",
"events": ["inbox.new_message"],
"description": "prod monitor"}An empty events list means every type. Ten endpoints per workspace; the URL must be a public http(s) address (private and loopback addresses are refused).
Events
| Type | Fires when |
|---|---|
inbox.new_message | A message arrived in, or was sent from, a conversation |
inbox.message_failed | A queued send did not go through — carries error_code, a human-readable message, the task_id and the recipient |
inbox.read | A conversation was marked read |
account.status_change | An account changed status (active, paused, error, banned) |
account.health_update | An account's health score changed |
account.trust_changed | An account's trust level changed |
account.power_changed | An account was put to sleep or woken |
account.presence | An account's worker connected to or dropped off Telegram |
notification.new | A platform notification was raised |
webhook.test | Only from the test button/endpoint |
Closing the loop on a send. A send answers 202 with a task_id and nothing else — the account still has to get it past Telegram's pacing. Subscribe to both inbox.new_message and inbox.message_failed and you learn which happened: the success arrives as a message with direction: "out", the failure as inbox.message_failed carrying the same task_id, an error_code (blocked_by_user, privacy_restricted, account_offline, cannot_write, account_banned, …) and a sentence you can show a user.
The request we send
POST /your-path
Content-Type: application/json
X-TGP-Event: inbox.new_message
X-TGP-Delivery: 82a91c75-7446-4ebe-8f79-3013cceaa1d7
X-TGP-Timestamp: 1787308800
X-TGP-Signature: v1=<hex>
{"id": "<delivery id>", "type": "inbox.new_message",
"created_at": "2026-08-20T10:27:04+00:00",
"data": {"user_id": "...", "conversation_id": "...",
"message": {"id": "...", "direction": "in", "content": "...", "sent_at": "..."},
"conversation": {"id": "...", "title": "...", "peer_id": 123}}}Verifying the signature. HMAC-SHA256 over {timestamp}.{raw body} with your secret, compared to the hex after v1=. Use the raw bytes — re-serializing the JSON changes them.
import hmac, hashlib
def verify(secret: str, headers, raw_body: bytes) -> bool:
ts = headers["X-TGP-Timestamp"]
expected = "v1=" + hmac.new(
secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(headers["X-TGP-Signature"], expected)The timestamp is inside the signed material, so reject anything older than a few minutes and a captured delivery can't be replayed at you later.
Delivery rules
- Any
2xxcounts as accepted. Answer fast and do the work afterwards — we give up on a request after 10 seconds and treat it as a failure. - Failures retry after 10s, 1m, 5m, 30m, 2h, 6h, then the delivery is marked
failed. - Five consecutive events that exhaust every retry switch the endpoint off; the dashboard shows why. Turning it back on clears the counter.
- Redirects are not followed. Point us at the final URL.
- The delivery id is stable across retries — key your de-duplication on it.
- Delivered records are kept 7 days, failed ones 30.
Real-time events
The dashboard streams live updates over Socket.IO:
wss://telegramos.net/wsAuthenticate with a session JWT (auth.token), the session cookie, or an API key — pass it as auth.apiKey in the handshake or as an X-API-Key header. A key needs read, and if it is area-scoped at all it must include inbox.
import { io } from 'socket.io-client'
const socket = io('https://telegramos.net', {
path: '/ws',
transports: ['websocket'],
auth: { apiKey: process.env.TGP_API_KEY },
})
socket.on('inbox.new_message', (e) => console.log(e.conversation_id, e.message.content))Events arrive as named Socket.IO messages: system.connected on connect, then inbox.new_message, notification.new, account.status_change, account.power_changed and friends; subscribe to one account with subscribe:account.
Two things to know before you build on it:
- Browsers are restricted. The handshake checks
Origin, and only our own domains are allowed. A server-side client sends noOriginand connects fine; a page on your own domain will be refused. Proxy through your backend. - A socket delivers once. Anything published while you are disconnected is gone. Use webhooks for delivery you can't afford to miss, or reconcile on reconnect with
after_msg_id.
What the API does not cover
- Teams, billing, wallet and the marketplace are session-only:
/wallet,/payments/*,/subscriptions/*,/billing/*,/bundles/purchase,/account-resale/*,/referrals/*and/teams/*answer401to an API key however it is scoped. There is no key path to spend money, rent an account or change seats — do those in the dashboard. The two exceptions are the public catalogue readsGET /listingsandGET /bundles/availability, which need no authentication at all. - No SDK. Plain HTTP; use whatever client you have.
- No per-key rate limit. Requests are cheap, but the actions behind them are paced per account by the anti-ban limits in Limits & quotas, so a burst of sends queues rather than fires.
A worked example
Reply to the newest unread conversation and wait for the send to land:
KEY="tgp_your_key_here"
BASE="https://telegramos.net"
CONV=$(curl -s -H "X-API-Key: $KEY" \
"$BASE/api/conversations?unread_only=true&page_size=1" \
| python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
curl -s -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-H "X-Request-ID: reply-$CONV" \
-d '{"content":"Thanks for reaching out — one moment."}' \
"$BASE/api/conversations/$CONV/messages"
# -> 202 {"task_id": "...", "queued": true, ...}
curl -s -H "X-API-Key: $KEY" "$BASE/api/conversations/$CONV/messages" | tail -c 400Monitoring a conversation
Three moving parts, in the order you set them up.
1. Backfill once, if you want the history. POST /api/accounts/{id}/backfill-history queues a one-shot import of existing chats (owned accounts, no media, up to 1000 messages per chat, broadcast channels skipped). Poll GET /api/accounts/{id}/backfill-history until state is done — the states are queued, running, done, cancelled, and idle means no job is recorded (either it never started or the status expired after 6 hours). Run it once per account; it is heavy on proxy data and everything after it arrives on its own.
2. Take the live stream. Register a webhook for inbox.new_message. Nothing else is needed for messages to keep flowing into the inbox — the account's worker is already listening.
3. Reconcile. After any outage on your side, walk each conversation forward with after_msg_id from the last id you stored. That closes the gap without re-reading the thread.
KEY="tgp_your_key_here"
BASE="https://telegramos.net"
curl -s -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"url":"https://your-server.example/telegramos","events":["inbox.new_message"]}' \
"$BASE/api/webhooks"
# -> 201 {"id": "...", "secret": "whsec_...", ...} <- store the secret
curl -s -H "X-API-Key: $KEY" \
"$BASE/api/conversations/$CONV/messages?after_msg_id=$LAST_SEEN&limit=200"