お悩み女子スプレッドシートに書いた文章を、決まった時間にXへ自動で投稿したい。でも、botは規約違反で凍結されると聞いて不安……
X APIとGASを連携したbotでは、スプレッドシートの「投稿予定」に書いた文章を、予定の時刻になったらGASがXへ投稿します。作る前に、Xの自動化ルールに沿った使い方かどうかを確かめます。
規約に沿ったbotの条件と、GASからXへ予約投稿するコードの組み立て方を紹介します。規約と手順は、公式ドキュメントで2026年10月に確かめた内容です。GASとX APIはこの環境では動かせないため、実際の投稿は確かめていません。OAuth 1.0aの署名の計算だけは、公式ドキュメントの例と同じ結果になることをNode.jsで確かめました。
- Xの自動化ルールで、許可される使い方と許可されない使い方
- シートの予定を承認してから投稿する全体の構成
- アプリの権限設定と、キーをスクリプトプロパティに入れる方法
- GASからXへ投稿するコードと、確かめ方
- トリガー、失敗時の通知、費用の管理
X APIとGASのbotで守る規約
Xのbotは何を自動にしてよいかが決まっています。公式の開発者ガイドラインの例から、この記事で扱う範囲を先に決めます。
公式の許可・不許可の例
| やりたいこと | 公式の扱い | この記事 |
|---|---|---|
| 決まった時刻にニュースや更新情報を投稿する | 許可(情報提供で、無差別なメンションがない) | 扱う |
| ブログの更新をRSSのように投稿する | 許可 | 扱う |
| キーワードを含む投稿へ自動で返信する | 不可(求められていない接触) | 扱わない |
| 自分の投稿に返信した人へ自動で返信する | 条件つき(相手が先に関わった場合、返信は1回まで) | 扱わない |
| 新しいフォロワーへの自動DM、いいねやフォローの自動化 | 不可 | 扱わない |
| AIで返信を作って投稿する | X社の事前承認が必要 | 扱わない |
| 同じ内容を複数のアカウントで投稿する | 不可(スパム) | 扱わない |
この記事で作るのは、上の2行にあたる予約投稿だけです。返信やDMのように、相手が求めていない接触になる機能は作りません。
botとして守る条件
公式のガイドラインは自動化したアカウントに次の対応を求めています。
- 「Automated」のラベルを付ける:botのアカウントの設定(Settings)で、Your account の Automation を選び、管理する人のアカウントを紐づけます。
- プロフィールに明記する:botであることと、運営者を書きます。
- 公式APIだけを使う:スクレイピングやブラウザの自動操作は、永久停止の対象です。
- 停止の依頼にすぐ従う:「やめて」と言われたら、その相手への働きかけをやめます。
ルールに反すると、アプリの停止、API利用の取り消し、アカウントの永久停止につながると公式に書かれています。作る前に、公式の開発者ガイドラインで最新の内容を確かめてください。
X APIとGASの連携|人が承認してから予約投稿する流れ
構成はシート、GAS、X APIの3つだけです。投稿の前に人の承認を挟み、1回の実行で投稿するのは1件にします。
- シート「投稿予定」に、予定日時と本文を書く
- 人が内容を読み、承認の欄にチェックを入れる
- GASが時間ごとに起動し、承認済みで予定時刻を過ぎた最初の1件を探す
- X APIへ投稿し、結果をシートに書き戻す
| 列 | 名前 | 中身 |
|---|---|---|
| A | 予定日時 | 投稿してよい日時 |
| B | 本文 | 投稿する文章 |
| C | 承認 | 人が確認したらチェックを入れる(チェックボックス) |
| D | 状態 | 空欄なら未投稿。コードが「投稿済み」「エラー」を書く |
| E | 結果 | HTTPのステータスコード |
AIで本文の下書きを作る場合も、人が読んで承認したものだけが投稿される形にします。公式が事前承認を求めているのはAIが作る返信で、予約投稿とは別の話です。ただ、人が確認する段があると、公開前に誤りや不要な@を見つけられます。作文の進め方はSNS投稿下書きを半自動で作る方法でも扱っています。
X APIとGASの準備|アプリの権限とキーの保管
コードの前に、X側の設定とキーの置き場所を決めます。X APIの取得そのものはX APIの取得方法と初期設定で扱っています。
アプリの権限とトークン
自分のアカウントとして投稿するので、公式が個人のbotや自分の自動化向けと説明しているAccess Token & Secret(OAuth 1.0a)を使います。アプリの権限は投稿に必要な「Read and write」にします。
キーをスクリプトプロパティに入れる
GASではキーをスクリプトプロパティに入れます。エディタの「プロジェクトの設定」から手で登録できます(公式によると50個まで)。シートのセルやコードには書きません。登録の考え方はPropertiesServiceでAPIキーを保存する方法にあります。
| プロパティ名 | 入れる値 |
|---|---|
| X_API_KEY | API Key |
| X_API_SECRET | API Key Secret |
| X_ACCESS_TOKEN | Access Token |
| X_ACCESS_TOKEN_SECRET | Access Token Secret |
GASからXへ投稿するコード|OAuth 1.0aの署名と予約投稿
コードは3つの部分に分かれます。上から順に、同じプロジェクトへ貼ります。
署名とヘッダーを作る関数
OAuth 1.0aではリクエストごとに署名を付けます。パラメーターを並べ替えてエンコードし、元になる文字列を作り、HMAC-SHA1で計算してBase64にします。
function percentEncode(text) {
return encodeURIComponent(text).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
}
function oauthSignature(method, url, params, consumerSecret, tokenSecret) {
const paramString = Object.keys(params)
.map((k) => [percentEncode(k), percentEncode(params[k])])
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
.map(([k, v]) => `${k}=${v}`)
.join("&");
const baseString = [method.toUpperCase(), percentEncode(url), percentEncode(paramString)].join("&");
const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;
const bytes = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_1, baseString, signingKey);
return Utilities.base64Encode(bytes);
}
function authorizationHeader(oauthParams) {
const items = Object.keys(oauthParams).sort().map((k) => `${percentEncode(k)}="${percentEncode(oauthParams[k])}"`);
return "OAuth " + items.join(", ");
}
Xへ投稿する関数
function postToX(text) {
const props = PropertiesService.getScriptProperties();
const url = "https://api.x.com/2/tweets";
const oauth = {
oauth_consumer_key: props.getProperty("X_API_KEY"),
oauth_nonce: Utilities.getUuid().replace(/-/g, ""),
oauth_signature_method: "HMAC-SHA1",
oauth_timestamp: String(Math.floor(Date.now() / 1000)),
oauth_token: props.getProperty("X_ACCESS_TOKEN"),
oauth_version: "1.0",
};
// 本文がJSONのときは、署名の計算に本文を含めない
oauth.oauth_signature = oauthSignature(
"POST", url, oauth, props.getProperty("X_API_SECRET"), props.getProperty("X_ACCESS_TOKEN_SECRET"));
const res = UrlFetchApp.fetch(url, {
method: "post",
contentType: "application/json",
headers: { Authorization: authorizationHeader(oauth) },
payload: JSON.stringify({ text: text }),
muteHttpExceptions: true,
});
return { status: res.getResponseCode(), body: res.getContentText() };
}
本文がJSONのときは、署名の計算に本文を含めません。OAuth 1.0aの仕様(RFC 5849)が、本文を含めるのはフォーム形式のときだけと定めているためです。成功すると、ステータスコードは201で返ります。
シートの予定を読んで1件だけ投稿する関数
function runScheduledPost() {
const sheet = SpreadsheetApp.getActive().getSheetByName("投稿予定");
const rows = sheet.getDataRange().getValues(); // 1行目は見出し
for (let i = 1; i < rows.length; i++) {
const [due, text, approved, status] = rows[i];
if (approved !== true || status !== "" || !text || new Date(due) > new Date()) continue;
if (/[@@]/.test(text)) {
sheet.getRange(i + 1, 4).setValue("要確認(@を含む)");
continue;
}
const result = postToX(String(text));
const state = result.status === 201 ? "投稿済み" : "エラー";
sheet.getRange(i + 1, 4, 1, 2).setValues([[state, `HTTP ${result.status}`]]);
return; // 1回の実行で投稿するのは1件だけ
}
}
投稿の条件は、承認がチェック済み、状態が空欄、予定時刻を過ぎているの3つです。本文に@が入っている行は、求められていないメンションになるおそれがあるため、投稿せずに「要確認」で止めます。
確認した結果と最初の試し方
次の4点を、偽の関数を使ってNode.jsで確かめました(Node.js v24)。GASにも、Xにも送っていません。
- 署名の関数が公式ドキュメントの例と同じ署名(
Ls93hJiZbQ3akF3HF3x1Bz8/zU4=)を返す - 投稿の関数がURL・ヘッダーの7項目・JSONの本文を、期待どおりに組み立てる
- 承認済みで予定時刻を過ぎた最初の1件だけを投稿し、@を含む行は止める
- ステータスが201以外のときは、状態に「エラー」と書く
実際に動かす最初の1回は、公開しても問題のない内容を1行だけ入れて、runScheduledPost を手で実行します。シートの状態列とXの投稿を見比べ、ステータスコードの見方は取得方法の記事で確かめます。投稿には、公式の単価に従って費用がかかります。
X APIとGASのbot運用|トリガー・失敗時・費用の管理
動いたあとは、実行する時刻を決め、失敗に気づく仕組みと費用を確認する仕組みを整えます。
時間主導型トリガー
エディタ左側の「トリガー」から「トリガーを追加」を選び、実行する関数に runScheduledPost、イベントのソースに「時間主導型」を指定します。1回に投稿するのは1件なので、投稿の間隔はトリガーの間隔より短くなりません。公式によると、時刻は少しずれる場合があります。設定はGASトリガーの基本で扱っています。
トリガーで動く処理が失敗すると、公式の説明では、「Summary of failures for Apps Script」という件名のメールが届きます。原因はエディタ左側の「Executions」で、失敗した実行のエラーを見て調べます。
費用の見積もり
公式の料金ページ(2026年10月時点)では、Postの作成は1リクエストあたり0.015ドル、URL付きは0.200ドルです。この単価で、毎日1件を30日続けた場合を計算しました。
| 投稿の種類 | 1件の単価 | 30件の合計 |
|---|---|---|
| URLなし | 0.015ドル | 0.45ドル |
| URL付き | 0.200ドル | 6.00ドル |
単価は変更されることがあるので、開発者コンソールで現在の金額を確かめ、支出の上限を設定しておきます。上限に達すると、リクエストは止まります。403や429が続くときは、再送を繰り返さず、トリガーを止めて原因を調べます。
X APIとGASのbotに関するよくある質問
botの作り方について、よくある疑問をまとめました。
- 特定のキーワードに反応して、自動で返信するbotは作れますか?
-
公式の開発者ガイドラインでは、キーワードを含む投稿へ自動で返信することは「求められていない接触」として許可されていません。返信できるのは、相手が先に関わった場合に限られ、1回までです。
- AIで書いた文章を投稿してもよいですか?
-
公式ガイドラインが事前承認を求めているのは、AIが作る返信です。予約投稿の本文にAIを使うことについて、公式ドキュメントからは可否を確かめられませんでした。判断に迷うときは、公式のPolicy Support formで問い合わせます。
- 複数のアカウントに同じ内容を投稿できますか?
-
同じ、または似た内容を複数のアカウントへ投稿することは、スパムとして禁止されています。地域ごとに内容が違うなど、重複しない目的のアカウントは例外として挙げられています。
X APIとGASの予約投稿と人の承認
X APIとGASを連携したbotのポイントを整理します。
作る範囲:決まった時刻の予約投稿だけ。返信、DM、いいね、フォローの自動化は作らない。
守る条件:「Automated」ラベル、プロフィールへの明記、公式APIのみ、停止の依頼に従う。
構成:シートの予定を人が承認し、GASが1回に1件だけ投稿する。キーはスクリプトプロパティに入れる。
動かした範囲:署名の計算は公式の例と一致。GASとX APIへの実際の送信は未実行。
最初は公開して問題のない1行で試し、結果をシートと画面で見比べてください。規約と単価は変わることがあるので、運用の前に公式ページで最新の内容を確かめます。
X APIとGASの関連する話題は、次の記事でも扱っています。
参考資料
Developer Guidelines | X Developer Platform
X Developer Policy | X Developer Platform
Apps | X Developer Platform
Creating a signature | X Developer Platform
X API pay-per-usage pricing and credits | X Developer Platform
Class UrlFetchApp | Apps Script
RFC 5849: The OAuth 1.0 Protocol
