wechat-official-account について
`curl + jq` で WeChat Official Account API を呼び出すシェルスキルです。記事の下書き・公開、フォロワー管理、カスタマーサービスメッセージ、統計取得に対応し、アカウント権限や利用枠、公開前の確認手順も説明します。
主な機能
- 01記事下書きの作成・一覧・更新
- 02下書きの公開と状態確認
- 03メニューとフォロワー記録の管理
- 04カスタマーサービスメッセージの送信
- 05フォロワー増加と閲覧統計の取得
- 06トークン処理と API エラーへの対応
活用シーン
- 01レビュー用の記事下書きを準備する
- 02確認済みの下書きを公開して URL を取得する
- 03フォロワーのプロフィールと増減を調べる
- 04条件を満たすフォロワーにメッセージを送る
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ acedatacloud/ skills --skill wechat-official-accountインストール後、Claude Code で/wechat-official-accountを入力して読み込まれたか確認します。
詳細
curl + jq を使って WeChat MP サーバーサイド API を呼び出します。OAuth ベアラー方式のコネクターとは異なり、WeChat MP では次の 2 段階のフローを使用します。
AppID + AppSecretをaccess_tokenと交換します(TTL は 7200s、アプリごとの全体上限は 1 日あたり約 2000 回)。- それ以外のすべての呼び出しで、その
access_tokenをクエリ文字列パラメーターとして渡します。
ユーザーの認証情報は $WECHAT_APP_ID と $WECHAT_APP_SECRET にあります。$WECHAT_APP_SECRET をログに記録したり、出力したりしないでください。パスワードとして扱ってください。
WeChat MP API は標準の JSON を返します。エラーでも HTTP 200 が返されます。本文は {"errcode": 40013, "errmsg": "invalid appid"} のような形式です。errcode == 0 は成功を意味します。0 以外のコードの場合は、元の errmsg をそのままユーザーに表示してください。
重要な制約 — ユーザーが予期せぬ事態に直面する前に伝えてください
- IP ホワイトリスト:すべての API 呼び出しの送信元 IP が、このアプリの IP ホワイトリスト(公众平台 → 设置与开发 → 基本配置 → IP 白名单)に登録されている必要があります。
errcode 40164(「invalid ip」)が表示された場合、ワーカーの送信 IP がホワイトリストに登録されていません。errmsgに表示されている IP を追加して、再試行するようユーザーに伝えてください。 - 公開には認証済みアカウントが必要です:2025-07 時点では、認証済み(已认证)の法人名義アカウントのみが
freepublish/*とmass/*を呼び出せます。個人名義アカウントと未認証の法人アカウントでは、権限エラーが発生します。通常、下書き(draft/*)とカスタマーサービスメッセージ(message/custom/*)は認証なしで利用できます。 - 一斉送信の上限は厳しいです:サービスアカウント = 月 4 回、購読アカウント = 1 日 1 回です。
freepublish/submitとmass/sendallは破壊的な操作として扱い、「公開して」と指示されていても、呼び出す前に必ずユーザーに確認してください。デフォルトでは下書きを作成し、下書きの URL を貼り付けてください。 - カスタマーサービスの利用可能時間は 48 時間です:
message/custom/sendが機能するのは、過去 48 時間以内にアカウントとやり取りしたフォロワーのopenidに対してのみです。この時間を過ぎると、errcode 45015が返されます。
レシピ
ステップ 0 — access_token を取得する(最初に実行し、結果をキャッシュします)
# Cache to /tmp so subsequent calls in the same session reuse it.
TOKEN_CACHE="/tmp/wx-mp-token-${WECHAT_APP_ID}.json"
# Reuse cached token if it's still valid (we conservatively refresh
# 5 minutes early to avoid edge-of-window failures).
NOW=$(date +%s)
if [ -f "$TOKEN_CACHE" ] && [ "$(jq -r '.exp_at // 0' "$TOKEN_CACHE")" -gt "$((NOW + 300))" ]; then
WECHAT_ACCESS_TOKEN=$(jq -r '.access_token' "$TOKEN_CACHE")
else
RESP=$(curl -sS "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${WECHAT_APP_ID}&secret=${WECHAT_APP_SECRET}")
WECHAT_ACCESS_TOKEN=$(echo "$RESP" | jq -r '.access_token // empty')
if [ -z "$WECHAT_ACCESS_TOKEN" ]; then
echo "Failed to fetch access_token: $RESP" >&2
exit 1
fi
EXPIRES=$(echo "$RESP" | jq -r '.expires_in // 7200')
jq -nc --arg t "$WECHAT_ACCESS_TOKEN" --argjson e "$((NOW + EXPIRES))" \
'{access_token:$t, exp_at:$e}' > "$TOKEN_CACHE"
fi
echo "OK token=${WECHAT_ACCESS_TOKEN:0:8}…"
レスポンスが {"errcode": 40164, ...}(IP が無効)または {"errcode": 40013, ...}(appid が無効)の場合は、そのエラーをユーザーに伝えてください。これはリクエストではなく、コネクターのセットアップ自体に問題があることを意味します。
接続を確認する(複雑な操作の前に必ず 1 回実行します)
# Pull basic public-account self-info; cheapest call that proves the token works.
curl -sS "https://api.weixin.qq.com/cgi-bin/account/getaccountbasicinfo?access_token=${WECHAT_ACCESS_TOKEN}" | jq
記事本文内で使用する画像をアップロードする(公開 URL を返します)
記事 HTML 内の <img src="..."> タグで使用する画像です。返された url は Tencent でホストされ、公開済みの記事で使用できます。
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${WECHAT_ACCESS_TOKEN}" \
-F "media=@/path/to/your-image.jpg"
# → {"url": "http://mmbiz.qpic.cn/..."}
制限:1 MB 以下、JPG/PNG のみ。このエンドポイントには件数制限がありません。
サムネイルをアップロードする(記事のカバーとして必要です)
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${WECHAT_ACCESS_TOKEN}&type=thumb" \
-F "media=@/path/to/cover.jpg" | jq '{media_id, url}'
# → {"media_id": "MEDIA_ID", "url": "http://mmbiz.qpic.cn/..."}
取得した media_id は、下書きの作成時に thumb_media_id として渡します。
推奨カバーサイズ:900×500 px、JPG、できれば 64 KB 未満です。
下書きを作成する(安全なデフォルト — 直接公開しないでください)
TITLE="Q1 product update"
AUTHOR="Acme Inc."
THUMB_MEDIA_ID="MEDIA_ID_FROM_PREVIOUS_STEP"
CONTENT_HTML='<p>欢迎关注我们的最新动态。</p><p><img src="http://mmbiz.qpic.cn/..."></p><p>更多内容请见底部「阅读原文」。</p>'
DIGEST="Q1 has been a wild ride — here's what shipped."
SOURCE_URL="https://example.com/q1-recap" # optional; populates 「阅读原文」, leave empty to omit
PAYLOAD=$(jq -nc \
--arg title "$TITLE" \
--arg author "$AUTHOR" \
--arg thumb "$THUMB_MEDIA_ID" \
--arg content "$CONTENT_HTML" \
--arg digest "$DIGEST" \
--arg source_url "$SOURCE_URL" \
'{articles: [{
article_type: "news",
title: $title,
author: $author,
thumb_media_id: $thumb,
content: $content,
digest: $digest,
content_source_url: $source_url,
need_open_comment: 0,
only_fans_can_comment: 0
}]}')
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$PAYLOAD" | jq
# → {"media_id": "DRAFT_MEDIA_ID"}
ユーザーに次のように伝えてください:"下書きを作成しました。https://mp.weixin.qq.com → 草稿箱 を開いて確認し、そこで「发表」をタップしてください。" — ユーザー自身が WeChat の UI で公開を確定するため、これが最も安全な方法です。
下書きを一覧表示する(新しい順)
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/draft/batchget?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw '{"offset": 0, "count": 20, "no_content": 1}' \
| jq '.item[] | {media_id, update_time, title: .content.news_item[0].title}'
1 件の下書きの全コンテンツを取得する
DRAFT_MEDIA_ID="..."
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/draft/get?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg m "$DRAFT_MEDIA_ID" '{media_id: $m}')" | jq
既存の下書きを更新する
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/draft/update?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg m "$DRAFT_MEDIA_ID" --arg t "Updated title" --arg c "<p>new body</p>" --arg th "$THUMB_MEDIA_ID" '
{media_id: $m, index: 0, articles: {
title: $t, content: $c, thumb_media_id: $th
}}')" | jq
下書きを「发表记录」に公開する — 破壊的操作。呼び出す前に確認してください
# Eats one of the monthly publish slots. Always echo back to the user
# what's about to go live and require an explicit "yes" confirmation
# in conversation BEFORE invoking this.
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/freepublish/submit?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg m "$DRAFT_MEDIA_ID" '{media_id: $m}')" | jq
# → {"errcode": 0, "msg_data_id": ..., "publish_id": "..."}
その後、公開ステータスをポーリングします(公開処理は非同期で、約 5~30 秒かかります)。
PUBLISH_ID="..."
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/freepublish/get?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg p "$PUBLISH_ID" '{publish_id: $p}')" | jq '{publish_status, fail_idx, article_id, article_url: .article_detail.item[0].article_url}'
# publish_status: 0 = success, 1 = publishing, 2 = original-check-failed,
# 3 = failed, 4 = published-but-removed, 5 = unverified-removed
publish_status == 0 の場合は、article_detail.item[0].article_url をユーザーに返してください。これが正規の https://mp.weixin.qq.com/s/... URL です。
公開済みの記事を一覧表示する
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/freepublish/batchget?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw '{"offset": 0, "count": 20, "no_content": 1}' \
| jq '.item[] | {article_id, update_time, title: .content.news_item[0].title, url: .content.news_item[0].url}'
カスタマーサービスメッセージを送信する(特定のフォロワー 1 人、48 時間以内)
OPENID="oXXXXXXXXXXXXXXXXX" # the recipient follower's openid
TEXT="Hi! Your subscription has been renewed. Thanks for sticking with us."
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg u "$OPENID" --arg t "$TEXT" '
{touser: $u, msgtype: "text", text: {content: $t}}')" | jq
errcode 45015 = 宛先が過去 48 時間以内にアカウントへメッセージを送っていません。このことをユーザーに説明してください。API で解決する方法はありません。
カスタマーメッセージで記事カードを送信するには、msgtype: "mpnews" と mpnews: {media_id: "..."} に変更します(最初に material/add_news を使って、永続的なニュースの media_id を作成してください)。
フォロワー増加数と閲覧統計を取得する
BEGIN="2026-04-25"
END="2026-05-01" # max 7-day window for getusersummary
# New / unsubscribed / cumulative followers per day
curl -sS -X POST \
"https://api.weixin.qq.com/datacube/getusersummary?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg b "$BEGIN" --arg e "$END" '{begin_date: $b, end_date: $e}')" \
| jq '.list[] | {date: .ref_date, new: .new_user, lost: .cancel_user, source: .user_source}'
# Article reads per day (max 3-day window for getuserread)
curl -sS -X POST \
"https://api.weixin.qq.com/datacube/getuserread?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$(jq -nc --arg b "$BEGIN" --arg e "$BEGIN" '{begin_date: $b, end_date: $e}')" | jq
datacube/* には、データキューブ権限を持つ認証済みアカウントが必要です。
ボトムメニューを管理する
# Read current menu
curl -sS "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=${WECHAT_ACCESS_TOKEN}" | jq
# Replace the menu (max 3 top-level buttons; each top-level button can have ≤ 5 sub-buttons)
MENU=$(jq -nc '{
button: [
{type: "click", name: "今日推荐", key: "DAILY_REC"},
{name: "更多",
sub_button: [
{type: "view", name: "官网", url: "https://example.com"},
{type: "view", name: "联系我们", url: "https://example.com/contact"}
]}
]
}')
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw "$MENU" | jq
# → {"errcode": 0, "errmsg": "ok"}
フォロワーリストを取得する(next_openid でページネーション)
NEXT=""
curl -sS "https://api.weixin.qq.com/cgi-bin/user/get?access_token=${WECHAT_ACCESS_TOKEN}&next_openid=${NEXT}" \
| jq '{total, count, openids: .data.openid, next: .next_openid}'
# Loop: pass the returned `next_openid` as NEXT until count < 10000.
各 openid について、プロフィールを一括取得します(1 回あたり ≤ 100 件)。
curl -sS -X POST \
"https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token=${WECHAT_ACCESS_TOKEN}" \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw '{"user_list": [{"openid": "OPENID1", "lang": "zh_CN"}]}' \
| jq '.user_info_list[] | {openid, nickname, subscribe_time, tagid_list}'
記事の content に関する HTML ルール
draft/add の content フィールドで使用できるのは、完全な Web HTML ではなく、HTML の サブセット です。
- 使用可能:
<p>、<span>、<strong>、<em>、<a href>、<img src>、<br>、<h1>~<h3>、<ul>/<ol>/<li>、<blockquote>、<section>。 - インラインスタイルのみ(
<p style="...">)。<style>ブロックや、外部 CSS への<link>は使用できません。 - すべての
<img>のsrcURL は、事前にアップロードしたmmbiz.qpic.cnURL(media/uploadimgから取得)か、すでに公開済みのmmbizURL である必要があります。WeChat はサードパーティのドメインでホストされた画像を削除します。 - コンテンツ全体のサイズ上限:約 20 K 文字(HTML を含む)。
よくある errcode の早見表
| errcode | 意味 | ユーザーへの案内 |
|---|---|---|
| 0 | 成功 | — |
| 40001 | access_token が無効 | 呼び出し中にトークンの有効期限が切れました。$TOKEN_CACHE をクリアして再試行してください |
| 40013 | appid が無効 | コネクターの AppID が正しくありません。接続を追加し直してください |
| 40164 | 接続元 IP がホワイトリストにありません | errmsg に表示された IP を 公众平台 → 设置与开发 → 基本配置 → IP白名单 に追加してください |
| 41001 | access_token がありません | バグです。?access_token=... を渡し忘れています |
| 45009 | API の日次クォータ超過 | 明日もう一度お試しください。アプリごとの日次クォータに達しました |
| 45015 | 返信可能な 48 時間を超過 | この openid のカスタマーサービス対応期間は終了しています。API では復旧できません |
| 48001 | API の利用権限がありません | このアカウントにはこのエンドポイントの権限がありません(例:認証なしでの公開)。errmsg 内のドキュメント URL を確認してください |
| 61450 | システムエラー | Tencent 側の一時的な問題です。1 秒待ってから一度だけ再試行してください |
SDK ではなく、シンプルな curl + jq を使う理由
スキルの本文は、シェルだけで完結する必要があります。サンドボックスでは pip install wechatpy の呼び出しが許可されていません。また、ここで扱う API は約 15 個のエンドポイントと少数で、2018 年以降安定しています。Python SDK(wechatpy、werobot)はパラメーターの形式を確認する参考資料として役立ちますが、上記の各レシピは、ドキュメントに記載されたエンドポイントを直接呼び出しています。SDK は同じ JSON を扱う薄いラッパーにすぎません。