Buffer GraphQL APIでSNS自動投稿パイプラインを構築した話
この記事を読むと何ができるか
Remotion で生成した Instagram Carousel 画像・Reel 動画を、Buffer の GraphQL API を使って Instagram / X / YouTube Shorts にスクリプトから自動投稿(予約投稿) できるようになる。
npx tsx scripts/schedule-post.ts \
--dir out/visa-article \
--platforms "instagram,x,youtube" \
--schedule "2026-04-01 12:00"
このワンコマンドで、3プラットフォームへの投稿予約が完了する。
前回の記事「Zenn記事をInstagram Carousel+Reel動画に自動変換する仕組みを作った」の続編だ。
📋 前提条件
- Node.js 18 以上
- TypeScript の基本がわかる
- Buffer アカウント(無料プランでOK)
- Instagram ビジネスアカウント + Facebook ページ連携済み
- X(Twitter)アカウント
- YouTube チャンネル
なぜ Buffer を選んだか
SNS の投稿を自動化する方法はいくつかある。各プラットフォームの API を直接叩く方法もあるが、認証フローがそれぞれ違って面倒だし、Instagram は Meta Business API の審査が必要で個人には敷居が高い。
Buffer を選んだ理由をまとめた:
| 選択肢 | 無料枠 | API公開 | 認証の手軽さ | マルチプラットフォーム |
|---|---|---|---|---|
| 各プラットフォーム直API | — | ✅ | ❌ 各社バラバラ | ❌ 個別実装 |
| IFTTT | あり | ❌ | — | ✅ |
| Zapier | 5タスク | ❌ プログラマブルでない | — | ✅ |
| Buffer | 3ch×10投稿 | ✅ GraphQL API | ✅ OAuth一回 | ✅ |
| Hootsuite | なし(有料) | 一部 | — | ✅ |
Buffer は無料プランで3チャネル(Instagram, X, YouTube)を管理でき、GraphQL API が公開されている。個人の SNS 運用ならこれで十分だ。初期コストゼロで始められる。
Buffer API の現状 — v1 REST は完全に死んでいる
ここ、最初に強く言っておきたい。
Buffer の v1 REST API(api.bufferapp.com/1/)は完全に廃止されている。
Google で「Buffer API」と検索すると、v1 REST API のドキュメントが上位に出てくる。Stack Overflow の回答もほとんどが v1 ベース。しかし 2026年3月時点で、v1 のエンドポイントはすべて HTTP 500 を返す。
# ❌ v1 REST API — 全エンドポイントが 500
curl https://api.bufferapp.com/1/profiles.json?access_token=xxx
# → 500 Internal Server Error
現在は GraphQL API(https://api.buffer.com) に一本化されている。
# ✅ GraphQL API — こちらが現在のエンドポイント
curl -X POST https://api.buffer.com \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ account { ... } }"}'
この情報、公式の移行ガイドは存在するが目立たない場所にあり、私は2時間ほどロスした。この記事を読んでいる方には同じ轍を踏んでほしくない。
アクセストークンの取得
方法1: Buffer 管理画面から直接取得(おすすめ)
一番手っ取り早いのは、Buffer のダッシュボードから直接トークンを取得する方法。
- Buffer にログイン
- Settings → Apps & Extras → Access Token へ進む
- トークンをコピーして
.envファイルに保存
# .env
BUFFER_ACCESS_TOKEN=1/xxxxxxxxxxxxxxxxxxxxxxxxxx
方法2: OAuth 2.0 フロー
アプリとして配布する場合は正式な OAuth フローが必要だが、個人利用なら方法1で十分。
チャネル ID の確認
投稿先のチャネル(Instagram, X, YouTube)の ID を GraphQL で取得する:
query {
account {
channels {
id
name
service
serverUrl
}
}
}
curl -s -X POST https://api.buffer.com \
-H "Authorization: Bearer $BUFFER_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query":"{ account { channels { id name service } } }"}' | jq
レスポンス例:
{
"data": {
"account": {
"channels": [
{ "id": "abc123", "name": "JodyCraft", "service": "instagram" },
{ "id": "def456", "name": "@jodycraft", "service": "twitter" },
{ "id": "ghi789", "name": "JodyCraft Tech", "service": "youtube" }
]
}
}
}
各 ID を .env に追加:
BUFFER_ACCESS_TOKEN=1/xxxxxxxxxxxxxxxxxxxxxxxxxx
BUFFER_PROFILE_INSTAGRAM=abc123
BUFFER_PROFILE_X=def456
BUFFER_PROFILE_YOUTUBE=ghi789
GraphQL で投稿を作成する — createPost mutation
Buffer の投稿作成は createPost mutation を使う。
テキストのみの投稿(X向け)
最もシンプルなパターン:
const mutation = `
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
dueAt
status
}
}
`;
const variables = {
input: {
channelIds: [process.env.BUFFER_PROFILE_X],
text: "C#でVISA計測器を制御する方法をZennに書きました 🔬\n詳しくは→ https://zenn.dev/jodycraft/articles/xxx",
mode: "customScheduled", // ⚠️ "customSchedule" ではない
dueAt: "2026-04-01T03:00:00.000Z", // UTC で指定
}
};
ハマりポイント: customScheduled の綴り
Buffer の GraphQL スキーマでは、スケジュール投稿のモード名が customScheduled(末尾に d がつく)。一部の古いドキュメントやブログ記事では customSchedule と書いてあるが、これだとバリデーションエラーになる。
// ❌ 間違い — バリデーションエラー
mode: "customSchedule"
// ✅ 正しい
mode: "customScheduled"
メディア付き投稿 — ここからが本番
テキストだけなら簡単だが、Instagram には画像か動画が必須。Buffer の GraphQL API でメディアを投稿するには、公開アクセス可能な URL を指定する必要がある。
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
status
}
}
{
"input": {
"channelIds": ["INSTAGRAM_CHANNEL_ID"],
"text": "キャプション",
"mode": "customScheduled",
"dueAt": "2026-04-01T03:00:00.000Z",
"assets": {
"images": [
{ "url": "https://example.com/slide-01.png" },
{ "url": "https://example.com/slide-02.png" }
]
},
"metadata": {
"instagram": {
"type": "post",
"shouldShareToFeed": true
}
}
}
}
プラットフォーム別のメディア要件
| プラットフォーム | 必須メディア |
metadata の設定 |
|---|---|---|
| Instagram Carousel |
assets.images[] に複数画像URL |
metadata.instagram.type: "post", shouldShareToFeed: true
|
| Instagram Reel |
assets.videos[].url に動画URL |
metadata.instagram.type: "reel", shouldShareToFeed: true
|
| YouTube Shorts |
assets.videos[].url に動画URL |
metadata.youtube.title(必須), categoryId(必須), privacy
|
| X (テキスト) | なし | なし |
| X (動画付き) |
assets.videos[].url に動画URL |
なし |
Instagram の Carousel と Reel は別々に投稿が必要
Instagram に Carousel(画像複数枚)と Reel(動画)を同時に出したい場合、1つの createPost で両方は送れない。2回の API コールが必要:
// 1回目: Carousel(画像6枚)
await createPost({
channelIds: [INSTAGRAM_ID],
assets: { images: carouselImageUrls },
metadata: { instagram: { type: "post", shouldShareToFeed: true } },
dueAt: "2026-04-01T03:00:00Z",
});
// 2回目: Reel(動画)— 5分後にスケジュール
await createPost({
channelIds: [INSTAGRAM_ID],
assets: { videos: [{ url: reelVideoUrl }] },
metadata: { instagram: { type: "reel", shouldShareToFeed: true } },
dueAt: "2026-04-01T03:05:00Z", // ← 5分オフセット
});
Reel を Carousel と同時刻に予約すると投稿が競合する可能性があるため、5分のオフセットを入れている。
メディアのアップロード問題
Buffer の API はメディアファイルの直接アップロードに対応していない。「公開 URL を指定して、Buffer 側がフェッチする」という設計だ。
つまり、ローカルにある slide-01.png を投稿するには、一時的にどこかのファイルホスティングに上げて、その URL を Buffer に渡す 必要がある。
一時ファイルホスティングの選択
いくつかのサービスを試した結果:
| サービス | 結果 | 有効期限 | 備考 |
|---|---|---|---|
| tmpfiles.org | ✅ 動作 | 1時間 | 24時間以内なら十分 |
| litterbox.catbox.moe | ✅ 動作 | 24時間指定可 | フォールバック用 |
| 0x0.st | ❌ HTTP 418 | — | Node.js からのリクエストを拒否される |
| imgur | △ | — | API キーが必要 |
0x0.st は最初に試したが、プログラムからのアップロードに対して HTTP 418(I'm a teapot) で「suspected malware client」と返してきた。人間からの利用のみを想定しているらしい。
最終的に、tmpfiles.org をメインに、litterbox をフォールバックとする2段構成にした。
アップロード関数の実装
async function uploadToFileHost(filePath: string): Promise<string> {
const file = fs.readFileSync(filePath);
const fileName = path.basename(filePath);
const form = new FormData();
// 1st try: tmpfiles.org
try {
form.append('file', new Blob([file]), fileName);
const res = await fetch('https://tmpfiles.org/api/v1/upload', {
method: 'POST',
body: form,
});
const json = await res.json();
if (json.status === 'success') {
// URL変換: tmpfiles.org/12345/file.png → tmpfiles.org/dl/12345/file.png
let url = json.data.url;
url = url.replace('tmpfiles.org/', 'tmpfiles.org/dl/');
return url.replace('http://', 'https://');
}
} catch (e) {
console.warn('tmpfiles.org failed, trying fallback...');
}
// 2nd try: litterbox.catbox.moe
try {
const form2 = new FormData();
form2.append('reqtype', 'fileupload');
form2.append('time', '24h');
form2.append('fileToUpload', new Blob([file]), fileName);
const res = await fetch('https://litterbox.catbox.moe/resources/internals/api.php', {
method: 'POST',
body: form2,
});
return await res.text();
} catch (e) {
throw new Error(`All upload services failed for ${fileName}`);
}
}
tmpfiles.org の URL 変換がちょっとクセがある。API のレスポンスでは http://tmpfiles.org/12345/file.png が返ってくるが、実際にファイルをダウンロードするには /dl/ を挿入した https://tmpfiles.org/dl/12345/file.png にアクセスする必要がある。これに気づかず、Buffer が「画像が取得できない」と言ってきて30分悩んだ。
統合スクリプト — schedule-post.ts
すべてを統合したスクリプトの全体フローはこうなっている:
1. 出力ディレクトリからキャプション・画像・動画を検出
2. プラットフォームごとに投稿を構築
├── Instagram: Carousel画像 → アップロード → createPost (type: "post")
├── Instagram: Reel動画 → アップロード → createPost (type: "reel", +5分)
├── X: テキスト or 動画付き → createPost
└── YouTube: 動画 → アップロード → createPost (title, categoryId 必須)
3. DRY RUN: 投稿内容を表示して確認
4. 実行: Buffer API を呼んで投稿予約
使い方
# DRY RUN(投稿内容の確認のみ)
npx tsx scripts/schedule-post.ts \
--dir out/visa-article \
--platforms "instagram,x,youtube" \
--schedule "2026-04-01 12:00" \
--dry-run
# 実行(実際に投稿予約)
npx tsx scripts/schedule-post.ts \
--dir out/visa-article \
--platforms "instagram,x,youtube" \
--schedule "2026-04-01 12:00"
DRY RUN では以下のような確認画面が表示される:
📋 === DRY RUN ===
📌 Instagram Carousel
📷 Images: 6 files
📝 Caption: C#でVISA計測器を制御するア...(127文字)
📅 Schedule: 2026-04-01 12:00 JST
🏷️ Type: post
📌 Instagram Reel
🎥 Video: reel-1080x1920.mp4
📅 Schedule: 2026-04-01 12:05 JST (+5min offset)
🏷️ Type: reel
📌 X (Twitter)
📝 Text: C#でVISA計測器を制御する...(240文字)
📅 Schedule: 2026-04-01 12:00 JST
📌 YouTube Shorts
🎥 Video: reel-1080x1920.mp4
📝 Title: C#でVISA計測器を制御する方法
📅 Schedule: 2026-04-01 12:00 JST
必ず DRY RUN で内容を確認してから実行するのをおすすめする。特にキャプションの文字数制限(X は 280文字、YouTube タイトルは 100文字)に引っかかっていないかの確認は大事。
注意点とレート制限
Buffer 無料プランの制限
| 項目 | 制限 |
|---|---|
| チャネル数 | 3 |
| チャネルあたり予約数 | 10投稿 |
| API レート制限 | 60リクエスト/分 |
無料プランでは1チャネルあたり10投稿まで。Instagram で Carousel + Reel を別投稿すると2枠消費するので、実質5セットしか予約できない。定期的に消化していく運用が必要だ。
時刻は UTC で指定
dueAt は ISO 8601 形式の UTC。JST(日本時間)から変換する場合は -9時間 する必要がある:
// JST 2026-04-01 12:00 → UTC 2026-04-01 03:00
const jstDate = new Date('2026-04-01T12:00:00+09:00');
const dueAt = jstDate.toISOString(); // "2026-04-01T03:00:00.000Z"
「dueAt must be in the future」エラー
指定した時刻がすでに過ぎていると dueAt must be in the future エラーになる。投稿予約は未来の時刻しか指定できない。当たり前だが、テスト中に何度かこれに引っかかった。
全体パイプライン — 記事公開からSNS投稿まで
前回の記事の仕組みと合わせると、Zenn 記事の公開から SNS への投稿までの全体フローはこうなる:
[1] Zenn記事を執筆・公開
[2] AI(Copilot CLI)が記事を分析
→ ArticleData JSON を生成
[3] Remotion でコンテンツ生成
→ Carousel PNG × 5〜8枚
→ Reel MP4 × 2本(1080p + 720p)
→ キャプション × 3ファイル
[4] Buffer API で投稿予約
→ Instagram Carousel + Reel
→ X(テキスト or 動画付き)
→ YouTube Shorts
[5] 指定時刻に自動投稿 ✅
ステップ 2〜4 は Agent Skill(Copilot CLI の自動化ルール)で一括実行できるようにしてある。「この記事の SNS コンテンツを作って投稿予約して」と指示するだけで、全部やってくれる。
やってみてわかったこと
Buffer の GraphQL API は情報が少ない
v1 REST API の情報はネット上にたくさんあるが、GraphQL API の情報は公式ドキュメント以外ほとんど見つからない。特に metadata の構造(instagram.type や youtube.categoryId)は、公式の API リファレンスを丁寧に読まないとわからなかった。
この記事の情報が、同じ苦労をする人を減らせたらうれしい。
ファイルホスティングが一番の不安定要素
パイプラインの中で最も脆弱なのが一時ファイルホスティング。無料サービスは突然使えなくなることがある(実際に 0x0.st は Node.js からのリクエストを拒否するようになった)。
対策として3サービスのフォールバックを実装しているが、将来的には AWS S3 や Cloudflare R2 のような信頼性の高いストレージに移行することを考えている。個人利用なら無料枠で十分収まるだろう。
投稿時間の最適化は後回しでいい
Instagram の最適投稿時間は「7:00-9:00」「12:00-13:00」「20:00-22:00」と言われるが、フォロワーが少ない初期段階では正直あまり関係ない。まずはコンスタントに投稿することが重要で、投稿時間の最適化はフォロワーが100人を超えてからでいい、というのが私の実感。
まとめ
- Buffer の v1 REST API は廃止済み。GraphQL API(
https://api.buffer.com) を使う - スケジュール投稿のモードは
customScheduled(d を忘れずに) - Instagram にはメディアが必須。Buffer は公開 URL からフェッチするため、一時ファイルホスティングが必要
- tmpfiles.org → litterbox → 0x0.st の 3サービスフォールバック で信頼性を確保
- Instagram の Carousel と Reel は別々の createPost が必要(5分オフセット推奨)
- YouTube Shorts は
metadata.youtube.titleとcategoryIdが必須 - DRY RUN で必ず確認してから実行する
前回の Remotion 編と合わせると、Zenn記事 → Carousel画像 + Reel動画 → Instagram / X / YouTube Shorts 自動投稿 という一気通貫のパイプラインが完成する。
📌 記事が役に立ったら「いいね」をもらえると励みになります。質問や改善案があれば、コメントで気軽にどうぞ!
よくある質問
Q. Buffer の無料プランで足りますか?
週に2〜3本のコンテンツを投稿する程度なら十分です。Instagram で Carousel + Reel を出すと1記事あたり2枠消費するので、常に5〜6セット分の予約枠がある計算。投稿が実行されれば枠は空くので、定期的に運用していれば問題ありません。
Q. Instagram のビジネスアカウントは必要ですか?
Buffer 経由で投稿する場合は、Instagram ビジネスアカウント(または クリエイターアカウント)が必要です。個人アカウントでは API 経由の投稿ができません。切り替えは Instagram の設定から無料でできます。
Q. 0x0.st が使えない理由は?
Node.js の fetch から multipart/form-data でアップロードすると、HTTP 418 で「suspected malware client」と返されます。Bot からのアップロードを制限しているようです。curl からなら問題なく使えるので、User-Agent で判定している可能性があります。
Q. セキュリティ上の懸念は?
一時ファイルホスティングに技術記事のスクリーンショットやコードの画像をアップロードすることになります。公開されても問題ない内容(Zenn で公開済みの記事が元)であれば問題ありませんが、機密情報を含む画像は絶対にアップロードしないでください。
Discussion