0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Buffer GraphQL APIでSNS自動投稿パイプラインを構築した話

0
Last updated at Posted at 2026-03-28

この記事を読むと何ができるか

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 は別々に投稿が必要

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 で公開済みの記事が元)であれば問題ありませんが、機密情報を含む画像は絶対にアップロードしないでください。


📝 この記事は Zenn で最初に公開されました。
最新版はZennをご覧ください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?