For AI

How-to guides

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_url and ext.copyright on the detail help you drop duplicates before they skew retrieval.
  • Deleted is free. Articles removed after listing return not_found and are not charged; the client skips them.
  • Retries are safe. The client reuses one Idempotency-Key across retries, and the same request with the same key within 24 hours returns the stored result with meta.replayed: true and 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

EndpointCallsPriceSubtotal
Official Account search1$0.058 / item$0.58
Official Account article history3$0.02$0.06
Official Account article detail10$0.02$0.20
Total14$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>"
}

Set up the MCP server →

Sources

  1. 微信开放文档: 获取接口调用凭据 (getAccessToken)developers.weixin.qq.com · read
  2. 微信开放文档: 获取已发布的消息列表 (freepublish_batchget)developers.weixin.qq.com · read

Connect once, then just ask.

Every new key comes with $0.10 to try it. One sentence connects your agent.

Start free
Connect your AI

Paste this into your agent

Read https://okdata.online/agents.md and set up okdata for me; my key is in the OKDATA_API_KEY environment variable.

Claude Code, Cursor, Codex and other agents that can open a link set themselves up from it.

MCP serverhttps://api.okdata.online/mcp