Claude CodeでSaaS連携を作る: APIキー、OAuth、Webhook、監査ログまで
Claude CodeでSaaS連携を実装する手順。APIキー、OAuth、Webhook検証、リトライ、監査ログまで実例で解説。
Claude CodeでSaaS連携を作るとき、最初に考えるべきことは「どのAPIを呼ぶか」ではありません。安全に認可し、失敗しても二重実行せず、あとから誰が何をしたか追える形にすることです。ここを飛ばすと、Slack通知のような小さな自動化でも、請求、権限、監査の問題にすぐぶつかります。
この記事では、APIキー、OAuth、Webhook、レート制限、リトライ、冪等性、監査ログ、シークレット管理、テスト環境を、Claude Codeから扱いやすいSaaS連携の形に落とし込みます。専門用語も初出で言い換えます。たとえばWebhookは「SaaS側から自分のサーバーへ届く通知」、冪等性は「同じ処理を再実行しても結果が重複しない性質」です。
想定する読者は、Claude Codeで社内ツール、運用スクリプト、顧客対応の自動化を作りたい開発者です。コードはNode.js 20以上ならコピーして試せる形にしています。
全体アーキテクチャ
SaaS連携は、Claude Codeから直接APIを叩く一枚板にしないほうが安定します。Claude Codeは要件整理、コード生成、実行の指示に強い一方で、認可情報、リトライ状態、監査ログはアプリ側に置くべきです。
flowchart LR
A[Claude Code] --> B[CLI or MCP connector]
B --> C[Auth and secret store]
B --> D[Retry and rate-limit wrapper]
D --> E[SaaS API]
E --> F[Webhook receiver]
F --> G[Queue]
G --> H[Worker]
H --> I[Audit log]
最小構成はCLIラッパーです。node scripts/slack-notify.mjs のように小さなコマンドを作り、Claude Codeから実行します。頻繁に使う連携はMCPサーバー化すると、入力スキーマ、権限、エラー処理を再利用できます。Webhook受信は実装コストが上がりますが、Stripeの支払い完了、GitHubのIssue作成、Slackの操作イベントのように、SaaS側を起点にしたい場合に必要です。
3つ以上の具体的なユースケース
1つ目は、リリース運用です。Claude CodeがGitHubの未リリースコミットを読み、リリースノートを作り、Slackへ要約を投稿します。SlackのIncoming Webhookは導入が速い反面、投稿後の削除など複雑な操作には向かないため、必要ならWeb APIへ進みます。
2つ目は、請求イベントの処理です。StripeのWebhookで checkout.session.completed を受け、顧客管理ツールへ登録し、失敗時は再処理キューに積みます。ここでは署名検証、冪等性キー、テストモードと本番モードの分離が必須です。
3つ目は、サポートと開発の橋渡しです。Google WorkspaceのOAuthで問い合わせCSVを読み、Claude Codeが分類し、GitHub Issueを作成します。ユーザーのデータへアクセスするため、APIキーではなくOAuthを使い、スコープを読み取り専用から始めます。
4つ目を挙げるなら、監査ダッシュボードです。Claude Codeが実行したSaaS操作をすべてNDJSONで保存し、「誰の権限で、どのプロバイダーに、何をしたか」を追えるようにします。小さな社内自動化でも、あとから原因調査できることは大きな差になります。
認可方式の選び方
| 方式 | 平易な説明 | 向いている処理 | 注意点 |
|---|---|---|---|
| APIキー | サーバー用の合い鍵 | Stripeのサーバー処理、社内専用のSlack通知 | 漏れると強い権限を持つため、環境変数かSecret Managerへ置く |
| OAuth | ユーザーに許可をもらう認可 | Google Drive、GitHub App、ユーザー別の操作 | refresh tokenの保管とスコープ設計が必要 |
| Webhook | SaaSから届くイベント通知 | Stripe決済、GitHub Issue、Slackイベント | 署名検証、重複配送、順序ずれを考える |
| CLI/MCP | Claude Codeから呼ぶ足場 | 定型作業、社内運用、複数SaaSの横断処理 | 入力検証とログをコネクタ側へ寄せる |
GoogleのOAuth 2.0ドキュメントでは、アクセストークンの期限切れ後にrefresh tokenで更新する流れが説明されています。GitHubやSlackでも、ユーザー権限で操作する連携は「誰の権限で実行されたか」をログに残してください。
環境変数チェックリスト
まずは .env.example を作り、値の名前だけを共有します。本物の値はコミットしません。
# .env.example
INTEGRATION_ENV=sandbox
SAAS_API_TOKEN=
SLACK_WEBHOOK_URL=
GITHUB_WEBHOOK_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
AUDIT_LOG_PATH=logs/saas-audit.ndjson
# .gitignore
.env
.env.*
!.env.example
logs/
確認リストは次の通りです。
- 本番キーとテストキーを同じファイルに混ぜない
- Claude Codeのプロンプトへ秘密値を貼らない
- エラー文に
AuthorizationヘッダーやWebhook secretを出さない - OAuthのスコープは読み取り専用から始める
- CIではリポジトリ変数ではなくSecretとして登録する
- キーをローテーションした日付と担当者を残す
リトライとレート制限の共通ラッパー
レート制限は「短時間に呼びすぎないための上限」です。GitHub REST APIにはプライマリ制限とセカンダリ制限があり、制限超過時は 403 または 429 と関連ヘッダーを見て待つ必要があります。Slackも 429 Too Many Requests と Retry-After を返します。Claude Codeに「失敗したらもう一度」と任せるだけでは危険なので、再試行はコード側で制御します。
// scripts/saas-request.mjs
import crypto from "node:crypto";
import { pathToFileURL } from "node:url";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export async function saasRequest(url, options = {}) {
const {
method = "GET",
token = process.env.SAAS_API_TOKEN,
body,
idempotencyKey = method === "POST" ? crypto.randomUUID() : undefined,
maxRetries = 4,
headers = {},
} = options;
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
const res = await fetch(url, {
method,
headers: {
Accept: "application/json",
...(body ? { "Content-Type": "application/json" } : {}),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
...headers,
},
body: body ? JSON.stringify(body) : undefined,
});
if (res.ok) return res;
const retryAfter = Number(res.headers.get("retry-after"));
const shouldRetry = [408, 409, 425, 429, 500, 502, 503, 504].includes(res.status);
if (!shouldRetry || attempt === maxRetries) {
const text = await res.text();
throw new Error(`SaaS request failed: ${res.status} ${text.slice(0, 200)}`);
}
const backoffMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: Math.min(30000, 500 * 2 ** attempt);
await sleep(backoffMs);
}
throw new Error("unreachable");
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const url = process.argv[2];
if (!url) throw new Error("Usage: node scripts/saas-request.mjs <url>");
const res = await saasRequest(url);
console.log(await res.text());
}
Stripeのように冪等性キーをサポートするAPIでは、同じPOSTを安全に再試行できます。自前APIやWebhookワーカーでも、provider + event_id + action をキーにして、処理済みならスキップする実装にしておくと事故が減ります。
Webhook検証の流れ
Webhookは便利ですが、公開URLにリクエストが届く仕組みです。署名検証なしで処理すると、第三者が偽イベントを投げられます。GitHubは X-Hub-Signature-256、Stripeは Stripe-Signature を使った検証方法を公式に説明しています。重要なのは、JSONへパースする前のraw bodyで検証することです。
1. raw bodyを読む
2. 署名ヘッダーを取り出す
3. 共有シークレットでHMACを計算する
4. timing-safeな比較で一致を確認する
5. delivery idを冪等性キーとして保存する
6. すぐ202を返し、重い処理はキューで実行する
// scripts/verify-github-webhook.mjs
import crypto from "node:crypto";
import http from "node:http";
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!secret) throw new Error("Set GITHUB_WEBHOOK_SECRET");
function verifyGitHubSignature(rawBody, signatureHeader) {
const received = Array.isArray(signatureHeader)
? signatureHeader[0]
: signatureHeader ?? "";
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const receivedBytes = Buffer.from(received);
const expectedBytes = Buffer.from(expected);
return (
receivedBytes.length === expectedBytes.length &&
crypto.timingSafeEqual(receivedBytes, expectedBytes)
);
}
http
.createServer(async (req, res) => {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const rawBody = Buffer.concat(chunks);
if (!verifyGitHubSignature(rawBody, req.headers["x-hub-signature-256"])) {
res.writeHead(401);
res.end("invalid signature");
return;
}
const event = req.headers["x-github-event"];
const delivery = req.headers["x-github-delivery"];
console.log(JSON.stringify({ event, delivery, receivedAt: new Date().toISOString() }));
res.writeHead(202, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
})
.listen(3000, () => console.log("Listening on http://localhost:3000"));
Stripeの場合は公式SDKの constructEvent() を使うのが実務では安全です。署名シークレットはテスト環境と本番環境で別になるため、STRIPE_WEBHOOK_SECRET を環境ごとに分けます。
監査ログを残す
監査ログは「あとで説明できる操作履歴」です。Claude CodeからSaaSを動かす場合、プロンプトの履歴だけに頼ると、トークン、実行者、対象ID、失敗理由が追えません。最低限、時刻、actor、provider、action、target、idempotencyKey、statusを残します。
{
"ts": "2026-06-03T09:15:00.000Z",
"actor": "claude-code",
"provider": "github",
"action": "create_issue",
"target": "owner/repo#123",
"idempotencyKey": "github:issue:customer-42:2026-06-03",
"status": "succeeded"
}
// scripts/audit-log.mjs
import { appendFile, mkdir } from "node:fs/promises";
import { dirname } from "node:path";
export async function writeAudit(event) {
const record = {
ts: new Date().toISOString(),
actor: event.actor ?? "claude-code",
provider: event.provider,
action: event.action,
target: event.target,
idempotencyKey: event.idempotencyKey,
status: event.status ?? "started",
};
const file = process.env.AUDIT_LOG_PATH ?? "logs/saas-audit.ndjson";
await mkdir(dirname(file), { recursive: true });
await appendFile(file, `${JSON.stringify(record)}\n`, "utf8");
}
if (process.argv[1]?.endsWith("audit-log.mjs")) {
await writeAudit({
provider: "slack",
action: "post_message",
target: "#release",
idempotencyKey: "demo-2026-06-03",
status: "succeeded",
});
}
この形式なら、あとでBigQuery、DuckDB、スプレッドシートに取り込めます。高価な監査基盤を最初から作る必要はありません。
よくある落とし穴
Webhookをlocalhostに直接届けようとして詰まることがあります。GitHubのトラブルシューティングにもある通り、公開URLが必要です。ローカル検証では転送サービスを使い、本番ではHTTPSの受信エンドポイントを用意します。
次に、Webhookの順序を信じる落とし穴があります。SaaSによってはイベントが作成順に届くとは限りません。イベント内の時刻、delivery id、対象リソースの最新状態を見て処理します。
三つ目は、リトライで重複を作ることです。決済、Issue作成、Slack投稿、メール送信は、二重実行するとユーザーに見えます。POSTには冪等性キーを付け、Webhookワーカーでは処理済みdelivery idを保存します。
四つ目は、スコープの広げすぎです。最初からGoogle Drive全体の読み書きを許すより、対象フォルダ、読み取り専用、短いトークン期限から始めるほうが安全です。
五つ目は、Claude Codeへ秘密値を貼ることです。Claude Codeには「環境変数名を使って実行して」と依頼し、実値はSecret Manager、CIのSecret、ローカル .env に閉じ込めます。
コネクタ抽象化を作るタイミング
最初の1本はベタ書きで構いません。ただし、同じ処理を3回以上書いたらコネクタに分けます。目安は、認可、レート制限、ページネーション、監査ログ、エラー整形が複数SaaSで重なり始めたときです。
抽象化するなら、sendMessage(), createTicket(), recordPaymentEvent(), writeAudit() のように業務語彙で切ります。callSlackApi() だけを薄く包むより、Claude Codeから見て「何をしたいか」が伝わる関数名にしたほうが、プロンプトもテストも安定します。
また、連携の品質を上げたい場合は Claude Code API設計アシスタント と Claude Code APIテスト の観点も合わせて使えます。セキュリティ面は Claude Codeセキュリティベストプラクティス を先に読むと事故を減らせます。
参考にした公式ドキュメント
- Stripe: Webhook署名の検証
- Stripe: Idempotent requests
- Slack: Incoming Webhooks
- Slack: Web API rate limits
- GitHub: Webhook deliveryの検証
- GitHub: REST API rate limits
- Google: OAuth 2.0 for Google APIs
Claude Code連携を業務で使うなら、実装テンプレートやレビュー観点を先に用意しておくと手戻りが減ります。実務用のチェックリストは /products/ に、チーム向けの導入支援は /training/ にまとめています。
実際に試した結果、最初から「署名検証、冪等性、監査ログ、テスト環境」を入れた連携は、あとでSaaSを追加しても壊れにくいことが分かりました。Slack通知だけの小さな自動化でも、この4点を入れておくと、失敗時に原因を追えるので運用がかなり楽になります。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Code権限セーフティラダー: 初心者がallowを広げる順番
Claude Codeの権限をread-onlyからbuild、限定編集、deploy確認まで段階的に広げる安全な運用手順。
Claude Code Small PR Proof Pack: 小さなPRをレビュー可能にする証拠セット
Claude Codeの小さなPRに、差分・検証・公開URL・CTA・rollbackを添える実務チェックリスト。
Claude Codeのコミット前レビューゲート: 差分、テスト、CTAをまとめて止める型
Claude Codeでcommit前に差分をレビューする実践手順。build、公開URL、CTA、Gumroadリンク、未翻訳本文を検知します。