毎月同じ手順を踏む仕事が、どこにでもある。
相手から「今月の金額が確定しました」という連絡が来る。スプレッドシートに書く。請求書を作る。PDFをダウンロードして送る。完了を報告する。
フリーランスでも、複数の取引先を持つ経営者でも、社内の経理担当者でも、この流れは同じだ。毎月同じことをやる。ミスると面倒なので集中する。集中が必要な作業を毎月やり続けるのはコスパが悪い。
請求書は自動で作って、PDFにして、送れる。 そのための実装をまとめた。
この記事ではChatworkをトリガーに使っているが、トリガーをGmailに変えるだけでSlackでもLINE WORKSでも同じ構造で動く。「Chatworkじゃないから関係ない」ではなく、月次確定処理のパターンとして読んでほしい。
パターンを図にすると
月次確定処理の典型的な流れはこうだ。
[確定連絡]
Chatwork or Gmail に金額・明細が届く
↓
[記録]
スプレッドシートの該当月列を更新
↓
[発行]
請求書API(Misocaなど)で請求書を作成
PDFをDriveに保存
↓
[通知]
関係者にChatworkで送る(複数ルームのことも多い)
Gmailで相手方に請求書を送付
↓
[完了]
この流れ自体は、フリーランスでも中小企業でも、レベニューシェア契約でも受託でも、だいたい同じだと思う。違うのは「どこから来るか」「どこに送るか」「請求書の品目が何か」だけ。
変わらない骨格があるなら、コードにできる。
最小構成で動かす
まず1案件、最小構成で作る。
① Chatworkからトリガーを拾う
担当者が「2月分が確定しました。売上: ○○円、経費: ○○円」のようなメッセージを送ってくる前提。それを拾う。
const CW_API_TOKEN = PropertiesService.getScriptProperties().getProperty('CW_TOKEN');
const ROOM_ID = 'xxxxx'; // 担当者とのルームID
const TARGET_AID = 'xxxxx'; // 担当者のアカウントID
function fetchLatestReport() {
const res = UrlFetchApp.fetch(
`https://api.chatwork.com/v2/rooms/${ROOM_ID}/messages?force=1`,
{ method: 'get', headers: { 'X-ChatWorkToken': CW_API_TOKEN } }
);
const messages = JSON.parse(res.getContentText());
// 新しい順に「確定」を含む相手のメッセージを探す
return [...messages].reverse().find(m =>
String(m.account.account_id) === TARGET_AID && m.body.includes('が確定')
);
}
force=1 をつけないと既読メッセージが返ってこない。#2で触れたやつ。
② テキストから金額を抜く
メッセージフォーマットは毎月少し揺れる。コロンが全角だったり、スペースが入ったり。正規表現で複数パターンを試す。
function extractAmount(text, keyword) {
const patterns = [
new RegExp(keyword + '\\s*[::]\\s*([\\d,,]+)'),
new RegExp(keyword + '[^\\d\\n]*([\\d,,]+)'),
];
for (const p of patterns) {
const m = text.match(p);
if (m) return parseInt(m[1].replace(/[,,]/g, ''));
}
return null;
}
// 使い方
const netAmount = extractAmount(messageBody, '差し引き') || extractAmount(messageBody, '差引');
③ Misocaで請求書を作る
税込金額が来る場合、Misocaに渡すのは税抜金額。ここで注意点がある。
taxExcl = Math.floor(incomeAmount / 1.1) だと1〜2円ずれることがある。Misocaは消費税を切り捨てで計算するため、taxExcl + floor(taxExcl × 0.1) = incomeAmount になる値を逆算する必要がある。
function toTaxExcl(taxIncl) {
let s = Math.floor(taxIncl / 1.1);
// 切り捨て誤差を補正(最大2円のズレを確認)
for (; s <= Math.floor(taxIncl / 1.1) + 2; s++) {
if (s + Math.floor(s * 0.1) === taxIncl) return s;
}
return Math.floor(taxIncl / 1.1); // フォールバック
}
請求書作成は通常のPOSTリクエスト。
function createInvoice(targetYear, month, taxInclAmount) {
const service = getMisocaService(); // OAuth2認証済みのサービス
const token = service.getAccessToken();
const taxExcl = toTaxExcl(taxInclAmount);
const res = UrlFetchApp.fetch('https://app.misoca.jp/api/v3/invoice', {
method: 'post',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
payload: JSON.stringify({
issue_date: Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd'),
contact_id: CONTACT_ID, // 請求先の取引先ID
subject: `${targetYear}年${month}月分 レベニューシェア`,
body: { tax_option: 'EXCLUDE' },
items: [{
name: '分配金',
quantity: 1,
unit_price: taxExcl,
unit_name: '式',
tax_type: 'STANDARD_TAX_10',
}],
}),
});
return JSON.parse(res.getContentText()).id; // 請求書ID
}
④ GASからChatworkにPDFを添付送信する(ここが一番詰まった)
GASからChatworkにPDFを添付送信するのが地味にハマりポイントだった。UrlFetchApp はmultipart/form-dataを直接サポートしていない。バイト配列を手動で組み立てる。
function sendPdfToChatwork(roomId, pdfBlob, message) {
const token = PropertiesService.getScriptProperties().getProperty('CW_TOKEN');
const boundary = 'BOUNDARY' + Date.now();
const CRLF = '\r\n';
// multipart/form-data を手組み
const header = `--${boundary}${CRLF}` +
`Content-Disposition: form-data; name="file"; filename="${pdfBlob.getName()}"${CRLF}` +
`Content-Type: application/pdf${CRLF}${CRLF}`;
const footer = `${CRLF}--${boundary}${CRLF}` +
`Content-Disposition: form-data; name="message"${CRLF}${CRLF}` +
`${message}${CRLF}--${boundary}--`;
const payload = Utilities.newBlob(header).getBytes()
.concat(pdfBlob.getBytes())
.concat(Utilities.newBlob(footer).getBytes());
UrlFetchApp.fetch(`https://api.chatwork.com/v2/rooms/${roomId}/files`, {
method: 'post',
headers: { 'X-ChatWorkToken': token, 'Content-Type': `multipart/form-data; boundary=${boundary}` },
payload: payload,
});
}
文字列部分を getBytes() でバイト変換して、Blobの生バイトと連結している。
「GAS Chatwork PDF 添付」で検索してもサンプルがほとんど出てこない。Chatwork APIの /rooms/{room_id}/files エンドポイントにmultipartでPOSTする実装例が少ないのはこの手組みが面倒だからだと思う。ハマった人の参考になれば。
⑤ 再実行を安全にする
途中でエラーが出て再実行するとき、請求書が二重に発行されると困る。請求書IDをスクリプトプロパティに保存して、既存のものがあればそのIDを使いまわす。
const invKey = `INV_${targetYear}_${month}`;
const existing = PropertiesService.getScriptProperties().getProperty(invKey);
let invoiceId;
if (existing) {
invoiceId = existing; // 既存IDを再利用
} else {
invoiceId = createInvoice(targetYear, month, amount);
PropertiesService.getScriptProperties().setProperty(invKey, String(invoiceId));
}
これでどこで止まっても再実行できる。Misocaの請求書は増えない。
freeeで同じことをやろうとして沼った
案件Aはfreeeにも請求書を作る必要があった。MisocaのAPIはシンプルで、POSTに必要なパラメータが揃っていれば動いた。同じノリでfreeeに行ったら全然違った。
認証フローが複雑で、エンドポイントのバージョンが複数あって、どれが請求書発行に使えるか判断が難しい。ドキュメントを読むコストが高い。
最終的にやったこと:GASでfreee APIを叩いて請求書のドラフトを作成するところまで自動化して、PDFダウンロードと添付送信だけ手動にした。完全自動化はあきらめて、「ダイアログに手順と直リンクを出して、クリックするだけ」 に着地させた。
// freee請求書作成後、ダイアログで手動手順を案内する
if (freeeInvoiceNumber) {
// 完了ダイアログ内に手順を表示
html += '<div>① freee請求書(' + freeeInvoiceNumber + ')→ PDFダウンロード</div>'
+ '<div>② DriveフォルダにPDF保存</div>'
+ '<div>③ 担当者にChatworkでPDF添付送信</div>';
}
ボタンを押せばfreeeとDriveのフォルダが自動で開く。あとはダイアログの指示通りに動くだけ。3ステップが手動に残ったけど、迷いがなくなった分ミスが減った。
freee MCP、早く請求書発行に対応してくれ。対応したらここは全自動になる。
実際に組んだもの
うちの場合は3案件あった。
| 案件 | トリガー | 請求先 | 送り先 |
|---|---|---|---|
| A | Chatworkのメッセージ | MisocaとfreeeW | Chatwork複数ルーム |
| B | Gmailの添付メール | Misoca | Chatwork + Gmail返信 |
| C | 別スプレッドシートから自動取得 | Misoca | Chatwork |
骨格は同じ。トリガーとAPIの向き先が違うだけ。
1つのGASプロジェクト(Code.gs + InvoiceB.gs + InvoiceC.gs)にまとめて、共通処理(Chatwork送信、OAuth2、税計算)は Code.gs に集約した。スプレッドシートのメニューから各案件を選んで実行する。
請求書処理
├── 案件A 実行 / 案件A テスト実行
├── 案件B 実行 / 案件B テスト実行
└── 案件C 実行 / 案件C テスト実行
テスト実行は本番に触れない。Chatworkはマイチャットに飛んで、Misocaは作らない。実装中に本番に飛ばして冷や汗をかかないためのやつ。
プレビューダイアログ
「実行」の前に確認画面を出している。
function buildPreviewHtml(parsedData, isTest) {
const html = `
<p>対象月: ${parsedData.targetMonth}</p>
<p>差し引き: ${parsedData.netAmount.toLocaleString()}円</p>
<button onclick="google.script.run.executeProcess()">実行</button>
`;
return HtmlService.createHtmlOutput(html).setWidth(500).setHeight(300);
}
実際はもう少し凝っているが、骨格はこれ。数字がパースできていれば「実行」ボタンが有効になる。パース失敗なら赤字で警告が出て、実行できない。
組んで変わったこと
21ステップが、確認ダイアログを見てボタンを押すだけになった。
「ミスが怖いから集中する」というモードが不要になった。確認画面の数字を見て問題なければ実行する。それだけ。月次処理で消耗していた集中力が、他のことに使えるようになった。
Chatworkって「メッセージが来たら人間が読んで動く」という使い方しかしていなかった。
でも構造としては、「特定のメッセージが来たらGASが動く」というトリガーとして使える。毎月くる定型連絡を「人間への通知」ではなく「処理のスタートライン」として扱うだけ。
月次処理だけじゃなく、「承認が来たら次のステップに進む」「集計完了のメッセージが来たらレポートを生成する」みたいな使い方もできる。Chatworkに情報が集まっている会社なら、自動化のトリガーとしてそのまま使えばいい。
792万IDが使っているのに、APIで遊んでいる人がほとんどいない。だからまだ発見がある。
こういう人に刺さると思う
- 毎月Chatworkで「確定しました」系の連絡を受け取っている
- Misocaかfreeeで請求書を作っている
- 請求書をPDFにしてChatworkやメールで送っている
- それを複数案件やっている(1案件でも十分元は取れる)
GASとChatwork APIのトークンがあれば始められる。Misocaは無料プランでもAPIが使える。
Chatworkシリーズ
- #1 なぜ2026年にまだChatworkを使い倒しているのか
- #2 chatwork-client-gas、ぶっちゃけいるの?
- #3 ルームの参加者データだけで、組織の人間関係マップを作った
- #4 「Chatworkに確定連絡が来たら請求書を送る」をGASで自動化する(この記事)
- #5 Chatwork MCPを繋いだら、17ルームの未読が10秒で片付いた
- #6 MCP vs GAS — Chatwork自動化の「正解」はどっちか
- #7 コンタクト承認をn8nで自動化しようとしたら、3つの罠にハマった
- #8 ChatworkにAIチームを住まわせたら、勝手に会話が始まった
- #9 Chatwork 8ルームの全メッセージからFAQ429件を自動抽出した
- #10 Webhook署名検証を入れたら全メッセージが消えた
- #11 過去メッセージを全件取得しようとしたら、APIの「100件の壁」にハマった
- #12 Chatwork APIの「既読」は自分で制御できる
- #13 Chatwork APIのファイル機能、使ったことある?
- #14 n8nで全ルーム巡回
- #15 タスク機能をAPIで使い倒す
- #16 MCPを2アカウント同時接続したら、仕事用と事務局用が1画面で回った
- #17 【世界初かもしれない】ChatworkでClaude Code Channelsを実装してみた
- #18 Chatwork × Dify × GASで問い合わせ回答を自動提案する
- #19 RelationMapを夜間バッチで毎日自動更新する
- #20 17記事書いて見えた、Chatwork APIエコシステムに足りないもの
- #21 Googleフォームの回答をChatworkに自動投稿するGAS
- #22 Chatworkの会話を毎日AIが要約してくれる仕組みをn8nで作った話
- #23 chatwork-cliを入れたら、シェルからChatworkが操作できて世界が変わった
- #24 ChatworkのWebhookをn8nで受けるなら、HMAC署名検証は必ずやれ
- #25 Chatwork × GAS × Claude Codeで会員制講座の運用を自動化した