WeChat Official Account API in Python for RAG (2026)
WeChat's own API only serves accounts you administer, so for any public account this guide calls three okdata endpoints from Python: Official Account search, Official Account article history and Official Account article detail. The worked example is 14 calls and costs $0.84, with the search counted at a full page; a failed call is not charged.
Updated
What does the official WeChat API cover? An Official Account's server-side API starts with an access token, which you get from the account's AppID and AppSecret and which lasts 7,200 seconds. With it, the publishing API's freepublish_batchget returns the articles your account has published, and other calls return drafts, material and statistics. A third-party platform can make the same calls for an account that has authorised it. So if the account is yours, use the official API: it is free, complete and gives you the original HTML. What it does not do is read accounts you do not administer, which is what a research or competitor knowledge base needs.
This guide covers that case. You build a small Python client that finds an account by name, lists its recent articles, fetches each one's full text and writes it as a Markdown file with YAML front matter: title, account, date and source link. The files drop straight into a chunker and an embedding job, and every chunk can cite its source.
Step by step
Step 1: set up
Python 3.9 or newer, requests, and your key in an environment variable. No SDK, no WeChat login.
pip install requests
export OKDATA_API_KEY=okd_...
Each call is a GET to https://api.okdata.online plus the endpoint path with the key as a bearer token. Success is "code": "ok"; an error carries code, message, request_id and a retry hint (yes means try again after retry_after_seconds).
Step 2: find the account
Official Account search takes the account name as query. Each result has id (the original ID, gh_…), username (the WeChat ID), name and followers. Keep only an exact name match; names are not unique and the first hit is not always the account you mean.
Search is priced per account returned: $0.058 each, at most $0.58 a call. An exact name usually returns one or a few. Store the gh_ ID after the first run and you never need to search for that account again.
curl -G "https://api.okdata.online/v1/wechat_mp/accounts/search" \
-H "Authorization: Bearer $OKDATA_API_KEY" \
--data-urlencode 'query=少数派'
Step 3: list recent articles
Official Account article history takes the gh_ ID or WeChat ID as id and returns 10 articles per call, newest first, for $0.02. Each item has id, title, url, published_at and a summary in text. Page with cursor until page.has_more is false or your page cap is reached.
Step 4: fetch each article as text
Official Account article detail takes the article url and returns the body as plain text in text, with paragraphs and headings on their own lines and inline images removed, for $0.02. It also returns title, published_at and author (the account's gh_ ID and name). With view=full, ext.author_byline names the writer and ext.source_url points to the original when the piece is a repost.
curl -G "https://api.okdata.online/v1/wechat_mp/articles/get" \
-H "Authorization: Bearer $OKDATA_API_KEY" \
--data-urlencode 'url=https://mp.weixin.qq.com/s/Vt8nR2kLq5XwJ0cYhPa3dE' \
--data-urlencode 'view=full'
Step 5: the client
The script below runs the worked example. It searches once, reads up to MAX_PAGES of history, writes an index of everything it listed, then fetches up to MAX_ARTICLES articles it does not have on disk yet and saves each as kb/wechat/<id>.md. Run it again next week and it only pays for new articles.
import os
import pathlib
import re
import time
import uuid
import requests
API = "https://api.okdata.online"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['OKDATA_API_KEY']}"
OUT = pathlib.Path("kb/wechat")
MAX_PAGES = 3 # history pages, 10 articles each
MAX_ARTICLES = 10 # full-text calls per run
class ApiError(Exception):
def __init__(self, body):
super().__init__(f"{body['code']}: {body['message']} (request {body['request_id']})")
self.code = body["code"]
def call(path, **params):
key = str(uuid.uuid4()) # same key on every retry: a replay is not charged again
for attempt in range(3):
body = SESSION.get(API + path, params=params, headers={"Idempotency-Key": key}, timeout=90).json()
if body["code"] == "ok":
return body
if body.get("retry") == "yes" and attempt < 2:
time.sleep(body.get("retry_after_seconds") or 5)
continue
raise ApiError(body)
def find_account(name):
accounts = call("/v1/wechat_mp/accounts/search", query=name)["data"]["items"]
match = next((a for a in accounts if a["name"] == name), None)
if match is None:
raise SystemExit(f"no account named {name!r}; got {[a['name'] for a in accounts]}")
return match
def history(account_id):
cursor = None
for _ in range(MAX_PAGES):
body = call("/v1/wechat_mp/accounts/articles/list", id=account_id, **({"cursor": cursor} if cursor else {}))
yield from body["data"]["items"]
if not body["page"]["has_more"]:
return
cursor = body["page"]["next_cursor"]
def quoted(s):
return '"' + (s or "").replace("\\", "\\\\").replace('"', '\\"') + '"'
def to_markdown(article, account):
ext = article.get("ext", {})
front = [
"---",
f"title: {quoted(article['title'])}",
f"account: {quoted(account['name'])}",
f"account_id: {account['id']}",
f"byline: {quoted(ext.get('author_byline'))}",
f"published: {article['published_at']}",
f"source: {article['url']}",
"---",
]
return "\n".join(front) + f"\n\n# {article['title'] or ''}\n\n{article['text']}\n"
def main(name):
OUT.mkdir(parents=True, exist_ok=True)
account = find_account(name)
listed = list(history(account["id"])) # newest first
with open(OUT / "index.tsv", "w", encoding="utf-8") as f:
f.writelines(f"{a['published_at']}\t{a['title']}\t{a['url']}\n" for a in listed)
path_of = lambda a: OUT / (re.sub(r"[^\w-]", "_", a["id"]) + ".md")
todo = [a for a in listed if not path_of(a).exists()][:MAX_ARTICLES]
for a in todo:
try:
full = call("/v1/wechat_mp/articles/get", url=a["url"], view="full")["data"]["item"]
except ApiError as err:
if err.code == "not_found": # deleted since it was listed; not charged
continue
raise
path_of(a).write_text(to_markdown(full, account), encoding="utf-8")
print("saved", full["title"])
if __name__ == "__main__":
main("少数派")
The first run is the worked example: 1 search, 3 history pages, 10 articles. Backfilling the same account's last 300 articles would add $6.60.
Step 6: chunk and index
Each file is front matter plus the article. Split the body on blank lines into chunks of a few hundred Chinese characters, and copy title, account, published and source into each chunk's metadata so an answer can cite the article and link to it. Most loaders read YAML front matter directly; if yours does not, parse the block between the --- lines yourself.
Good to know
- Text, not layout. Detail returns plain text. Tables collapse into lines and inline images are gone; the cover URL is in
images. If you need the article as it looked, archive the HTML another way. - Reposts. Many accounts republish other people's pieces.
ext.source_urlandext.copyrighton the detail help you drop duplicates before they skew retrieval. - Deleted is free. Articles removed after listing return
not_foundand are not charged; the client skips them. - Retries are safe. The client reuses one
Idempotency-Keyacross retries, and the same request with the same key within 24 hours returns the stored result withmeta.replayed: trueand no new charge. - Untrusted text. Article text is written by strangers. Tell your model to treat retrieved passages as data, not instructions.
- Rights. Index for internal research, keep the source link with every chunk and do not serve whole articles back to users.
For the shorter version of the Markdown step, see turning WeChat articles into clean text for RAG. If you only have names and need IDs first, see finding an account's gh_ ID.
What the example costs
One search for 少数派 (sspai), three history pages (about 30 articles), then the 10 newest articles saved as Markdown files
| Endpoint | Calls | Price | Subtotal |
|---|---|---|---|
| Official Account search | 1 | $0.058 / item | $0.58 |
| Official Account article history | 3 | $0.02 | $0.06 |
| Official Account article detail | 10 | $0.02 | $0.20 |
| Total | 14 | $0.84 |
Prices in force when this page was built. A failed call costs nothing.
In your AI (MCP)
Connect the MCP server once, then say what you want. The agent finds these endpoints, reads their prices and calls them on your balance.
SayFind the WeChat Official Account 少数派, take its 10 newest articles and give me each one as Markdown with the title, account, publish date and source link at the top.
What the agent sends to the call tool first
{
"endpoint": "wechat_mp.accounts.search",
"params": {
"query": "36氪"
},
"idempotency_key": "<a new UUID>"
}