📡

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 のダッシュボードから直接トークンを取得する方法。

  1. Buffer にログイン
  2. SettingsApps & ExtrasAccess Token へ進む
  3. トークンをコピーして .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(動画)を同時に出したい場合、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.typeyoutube.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.titlecategoryId が必須
  • 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

ログインするとコメントできます