Post from Telegram to WordPress — Automated CMS Publishing Bot
Publish blog posts, formatted HTML updates, and high-resolution photo attachments directly from a Telegram channel or chat to WordPress via the official /wp/v2/ REST API.

Technical Architecture: Reverse-Engineering the Telegram-to-WordPress Bridge
Publishing micro-blog updates or photo logs to a WordPress site from a mobile browser requires navigating a heavy admin dashboard. I built post-from-telegram-to-wordpress as a containerized Python daemon that listens to authorized Telegram channels, uploads binary media to /wp-json/wp/v2/media, and publishes structured posts to /wp-json/wp/v2/posts.
[ Telegram Channel / Chat ]
│
│ (Telegram Bot API / Event Listener)
▼
[ Python Listener Service ]
│
├── 1. Verify Chat ID Allowlist & Extract HTML/Markdown Entities
├── 2. Download Highest-Resolution Telegram Photo (photo[-1])
├── 3. Upload Binary Stream to WordPress (/wp-json/wp/v2/media)
│
▼ (WordPress Application Password Basic Auth)
[ WordPress REST API (/wp-json/wp/v2/posts) ]
│
▼
[ Live WordPress Post with Featured Image ]
1. WordPress Application Password Authentication
Instead of relying on cookie sessions or main user passwords, the bridge authenticates using WordPress Application Passwords (native since WordPress 5.6), encoding username:app_password as an RFC 7617 Basic Authorization header over HTTPS:
import base64
def get_wp_auth_header(username: str, app_password: str) -> dict:
credentials = f"{username}:{app_password}"
token = base64.b64encode(credentials.encode("utf-8")).decode("utf-8")
return {"Authorization": f"Basic {token}"}
2. Allowlisted Telegram Event Listener
Incoming updates are filtered against an explicit ALLOWED_CHAT_IDS set so only designated channel administrators or private chats can trigger WordPress publication:
from telegram import Update
from telegram.ext import ContextTypes
ALLOWED_CHAT_IDS = {-100123456789} # Target Telegram Channel / Admin ID
async def handle_incoming_post(update: Update, context: ContextTypes.DEFAULT_TYPE):
message = update.effective_message
if not message or message.chat.id not in ALLOWED_CHAT_IDS:
return
text_content = message.text_html or message.caption_html or ""
photo_attachments = message.photo
await process_and_publish(text_content, photo_attachments, context.bot)
3. Two-Stage Media & Post Publishing Pipeline
When a Telegram message includes a photo, the daemon fetches the highest-resolution variant (message.photo[-1]), streams the binary buffer to /wp-json/wp/v2/media with a Content-Disposition filename header, captures the returned attachment id, and binds it as featured_media on the new post:
import requests
def upload_photo_to_wordpress(wp_site_url: str, auth_headers: dict, photo_bytes: bytes, filename: str) -> int:
media_url = f"{wp_site_url}/wp-json/wp/v2/media"
headers = {
**auth_headers,
"Content-Disposition": f'attachment; filename="{filename}"',
"Content-Type": "image/jpeg",
}
response = requests.post(media_url, headers=headers, data=photo_bytes, timeout=30)
response.raise_for_status()
return response.json()["id"]
def create_wordpress_post(
wp_site_url: str,
auth_headers: dict,
title: str,
html_content: str,
media_id: int | None = None,
) -> dict:
posts_url = f"{wp_site_url}/wp-json/wp/v2/posts"
payload = {
"title": title,
"content": html_content,
"status": "publish", # Configurable: 'publish' or 'draft'
}
if media_id is not None:
payload["featured_media"] = media_id
response = requests.post(posts_url, headers=auth_headers, json=payload, timeout=30)
response.raise_for_status()
return response.json()
Configuration & Environment Reference
| Environment Variable | Example Value | Purpose |
|---|---|---|
TELEGRAM_BOT_TOKEN | 123456789:AAH... | Bot token issued by @BotFather |
ALLOWED_CHAT_ID | -100123456789 | Authorized Telegram channel or user chat ID |
WP_SITE_URL | https://example.com | Base URL of the target WordPress installation |
WP_USER | editor_bot | WordPress username with upload_files and publish_posts roles |
WP_APP_PASSWORD | xxxx xxxx xxxx xxxx | Revokable WordPress Application Password |
Security & Deployment Architecture
- Least-Privilege WordPress Credentials: Uses revokable Application Passwords scoped to a dedicated WordPress Author/Editor account rather than a Super Admin account.
- Strict Origin Filtering: Drops updates from any Telegram chat or user ID not explicitly listed in
ALLOWED_CHAT_IDS. - Stateless Container Execution: Runs inside a
python:3.11-slimDocker container without requiring local database volumes or persistent media folders.
Get notified about updates to this tool
No spam. Just high-quality updates about new features and guides.
Related Tools
Related Posts
Learn how to detect Google AdSense on any website using Python. Covers fast HTML scanning with BeautifulSoup, ads.txt validation, and headless Playwright automation to bypass bot protection and lazy loading.
Learn how to easily download YouTube video thumbnails using Python's requests library, without the need for heavy dependencies like Pytube. This guide covers extracting video IDs from various YouTube URL formats and fetching the highest quality thumbnail available.
This piece will introduce Make (formerly Integromat) as a direct competitor to n8n, highlighting its visual, diagram-like workflow builder. The comparison will focus on key differences in pricing models, UI/UX, and advanced features like branching logic, which are a major selling point for Make. Examples will show how to build the same complex workflow in both n8n and Make to illustrate their unique approaches.