Library use¶
tgctl-go is not a Go library you import. It is a binary you exec or
pipe.
The gotd/td-backed client interface lives under internal/, so Go's
visibility rules intentionally prevent external packages from importing
it. The stable integration surface is the CLI contract: JSON envelopes,
exit codes, and append-only audit logs.
JSON envelope¶
Pass --json to force machine output:
Success:
{
"ok": true,
"command": "send",
"request_id": "req-abc12345",
"data": {
"message_id": "<new-message-id>"
},
"warnings": []
}
Failure:
{
"ok": false,
"command": "send",
"request_id": "req-xyz09876",
"error": {
"code": "WRITE_DISALLOWED",
"message": "write requires --allow-write"
}
}
Shell integration¶
Use jq for routing and assertions:
msg_id=$(
tg --account "$TG_ACCOUNT_NAME" send "$TG_CHAT_ID" "ack" --allow-write --json |
jq -r '.data.message_id'
)
tg --account "$TG_ACCOUNT_NAME" get-msg "$TG_CHAT_ID" "$msg_id" --json |
jq -e '.ok == true and .data.message.message_id == '"$msg_id"
Subprocess integration¶
Any language can call tg and parse the envelope.
import json
import os
import subprocess
result = subprocess.run(
["tg", "--account", os.environ["TG_ACCOUNT_NAME"], "show",
os.environ["TG_CHAT_ID"], "--limit", "5", "--json"],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
envelope = json.loads(result.stdout)
if result.returncode != 0 or not envelope["ok"]:
raise RuntimeError(envelope.get("error", result.stderr))
Agent subprocess pattern¶
For an agent, make every call explicit: account, JSON output, write gate, and idempotency key.
tg --account test backfill-entities --allow-write --json
tg --account test backfill "$TG_CHAT_ID" --max-messages 100 --allow-write --json
tg --account test search "$TG_CHAT_ID" "order" --limit 20 --json
tg --account test send "$TG_CHAT_ID" "agent draft reply" \
--allow-write \
--idempotency-key "agent-reply-001" \
--json
Reads do not need --allow-write. Local DB writes (discover,
backfill, sync-contacts, listen) and Telegram writes do. Treat
request_id as the correlation key for logs and audit entries.
Account selection is --account, then TG_ACCOUNT, then the persisted
accounts/.current selector, then default. Agents should pass --account
explicitly on every subprocess call. --read-only (or TG_READONLY=1) blocks
Telegram writes and all local-state writes or creation, even when
--allow-write is present.
Media subprocesses¶
For one message:
The TG_* variables below are synthetic placeholders populated from your own
account and destination.
tg --account "$TG_ACCOUNT_NAME" download-media \
"$TG_CHAT_ID" "$TG_MESSAGE_ID" \
--output "$TG_MEDIA_OUTPUT_DIR" \
--max-size-mb 100 --allow-write --json
For a capped bulk backfill into the selected account's default media tree:
tg --account "$TG_ACCOUNT_NAME" backfill "$TG_CHAT_ID" \
--max-messages 250 --download-media --max-media-size-mb 100 \
--allow-write --json
Supported current types are photo, video, video note, voice, audio, sticker,
animation, and generic document. Photos use Telegram's selected largest
downloadable image; document-class media uses the original file stream.
Single downloads return media_path, bytes, and skipped; bulk results
return media_downloaded, media_skipped, media_failed, and warnings.
Treat a nonzero envelope with committed: true as a partial commit. Its
bounded recovery metadata may include validated artifact paths and byte counts;
inspect those before deciding whether to retry. An anchored regular file at the
final name is a successful no-overwrite skip, not a content-hash match.
--overwrite or --overwrite-media explicitly opts into safe atomic
replacement; unsupported atomic replacement fails safely. Size limit 0 means
unlimited.
Exit codes¶
The process exit code matches the envelope error code family:
| Code | Name |
|---|---|
| 0 | OK |
| 1 | GENERIC |
| 2 | BAD_ARGS |
| 3 | NOT_AUTHED |
| 4 | NOT_FOUND |
| 5 | FLOOD_WAIT |
| 6 | WRITE_DISALLOWED |
| 7 | NEEDS_CONFIRM |
| 8 | LOCAL_RATE_LIMIT |
| 9 | PREMIUM_REQUIRED |
| 10 | PERMISSION_DENIED |
| 11 | ARCHIVE_MISSING |
| 12 | ARCHIVE_CHANGED |
| 13 | ARCHIVE_EXTRA |
Use both the exit code and .error.code. The numeric code is stable
for shells; the string is better for logs and metrics.
Request IDs¶
Every envelope includes request_id. Telegram write-pipeline audit entries use
the same ID for pre-call and final records:
tg --account "$TG_ACCOUNT_NAME" send "$TG_CHAT_ID" "trace me" --allow-write --json |
jq -r '.request_id'
Then correlate in accounts/<account-name>/audit.log:
Streaming¶
tg listen --json emits one envelope per update:
For tests, use one update and exit:
Versioning¶
The CLI contract is the API. New commands and fields are additive. Existing exit codes, JSON envelope keys, and safety gates are stable across the Go and Python ports.
See also¶
- Safety model — exit codes and audit log
- Multi-account — isolating agents from your real DMs
- Quickstart — agent-ready first run