Tips & Tricks (Updated: 6/3/2026)

Build SaaS Integrations with Claude Code: API Keys, OAuth, Webhooks, and Audit Logs

A practical Claude Code guide to SaaS integrations: API keys, OAuth, webhooks, retries, idempotency, secrets, and logs.

Build SaaS Integrations with Claude Code: API Keys, OAuth, Webhooks, and Audit Logs

When you build SaaS integrations with Claude Code, the first question is not “which endpoint should I call?” The first question is how to authorize safely, retry without duplicate side effects, and prove later who did what. Skip that layer and even a simple Slack notification can become a security, billing, or operations problem.

This guide turns API keys, OAuth, webhooks, rate limits, retries, idempotency, audit logs, secrets, and test environments into a practical Claude Code workflow. A webhook is an event that a SaaS product sends to your server. Idempotency means a repeated operation does not create a duplicate result. An audit log is the record that lets you explain the automation afterward.

The examples assume Node.js 20 or later and are written so you can copy them into a small scripts/ directory.

Integration Architecture

Do not make Claude Code the only place where integration state lives. Let Claude Code plan, generate, and run commands, but keep tokens, retry state, and audit history in your connector 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]

The smallest useful version is a CLI wrapper: for example, node scripts/slack-notify.mjs called from Claude Code. If the workflow becomes common, promote it to an MCP server so the input schema, permissions, and error handling are reusable. Webhook receivers take more work, but they are required when Stripe, GitHub, Slack, or another SaaS product initiates the workflow.

Four Concrete Use Cases

The first use case is release operations. Claude Code reads unreleased GitHub commits, drafts release notes, and posts a compact summary to Slack. Slack Incoming Webhooks are fast to set up, but they are not the right tool for advanced message deletion or complex chat flows; move to the Slack Web API when you need that control.

The second use case is billing automation. A Stripe webhook receives checkout.session.completed, records the customer in an internal tool, and queues failed work for retry. Signature verification, idempotency keys, and strict separation between test mode and live mode are mandatory here.

The third use case is support triage. Google Workspace OAuth lets a backend read a support CSV, Claude Code classifies the rows, and GitHub issues are created for engineering follow-up. Because the workflow touches user data, OAuth with narrow read-only scopes is safer than a shared API key.

The fourth use case is an audit dashboard. Every SaaS action triggered by Claude Code is written as NDJSON so the team can see which actor, provider, action, target, and idempotency key were involved. This is useful even before you buy or build a full audit platform.

Choosing API Keys, OAuth, Webhooks, and Connectors

MethodPlain-English meaningBest forWatch out for
API keyA server-side shared credentialStripe server calls, internal Slack notificationsStore it in environment variables or a secret manager, never in source
OAuthA user or workspace grants accessGoogle Drive, GitHub Apps, user-specific actionsRefresh tokens and scopes need deliberate handling
WebhookSaaS sends an event to your endpointStripe payments, GitHub issues, Slack eventsVerify signatures, handle duplicate deliveries, ignore ordering assumptions
CLI/MCP connectorA stable tool Claude Code can callRunbooks, internal ops, cross-SaaS workflowsPut validation and logging in the connector

Google’s OAuth 2.0 documentation explains the access-token and refresh-token flow. GitHub and Slack integrations also need actor-aware logging, because “the bot did it” is not enough when you are debugging permissions later.

Environment Variable Checklist

Start with .env.example and share names only. Real values stay out of Git.

# .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/

Use this checklist before letting Claude Code run the integration:

  • Keep live keys and test keys in separate environments.
  • Never paste secrets into a Claude Code prompt.
  • Never print Authorization headers or webhook secrets in errors.
  • Start OAuth scopes as read-only and expand only when needed.
  • Register CI values as secrets, not plain repository variables.
  • Record who rotated a key and when.

Retry and Rate-Limit Wrapper

A rate limit is the maximum request pace a provider allows. GitHub REST APIs have primary and secondary limits, and rate-limit failures require respecting response headers. Slack returns 429 Too Many Requests with Retry-After. Do not leave that behavior to ad hoc prompting; put it in 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());
}

For APIs such as Stripe that support idempotency keys, the same POST can be retried safely. For your own workers, use a key like provider + event_id + action and skip processing if it already exists.

Webhook Verification Flow

Webhooks are public HTTP entry points. Without signature verification, anyone can send fake events. GitHub uses X-Hub-Signature-256; Stripe uses Stripe-Signature. The key rule is to verify the raw body before parsing JSON.

1. Read the raw body.
2. Read the signature header.
3. Compute HMAC with the shared secret.
4. Compare with a timing-safe comparison.
5. Save the delivery id as an idempotency key.
6. Return 202 quickly and process heavy work in a queue.
// 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"));

For Stripe, use the official SDK’s constructEvent() in production. Test and live webhook secrets are different, so keep STRIPE_WEBHOOK_SECRET environment-specific.

Audit Logs

An audit log is the record that lets you explain an automated action later. Claude Code chat history is not enough; you need timestamp, actor, provider, action, target, idempotency key, and 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",
  });
}

NDJSON is enough for the first version. You can import it later into BigQuery, DuckDB, or a spreadsheet.

Common Pitfalls

One common failure is trying to deliver webhooks directly to localhost. GitHub’s troubleshooting docs call out that webhook URLs need to be publicly reachable; use a forwarding service for local tests and HTTPS in production.

Another mistake is trusting webhook order. Providers can deliver events late or out of order. Use event timestamps, delivery ids, and current resource state instead of assuming the arrival sequence is truth.

A third problem is duplicate work on retry. Payments, issues, Slack posts, and emails are user-visible. Add idempotency keys to POST requests and store processed delivery ids in webhook workers.

The fourth pitfall is broad OAuth scope. Start with read-only access, one folder or workspace, and short-lived tokens. Expand only after you can explain the risk.

The fifth pitfall is pasting secrets into Claude Code. Ask Claude Code to use environment variable names; keep the real values in a secret manager, CI secret, or local .env.

When to Build a Connector Abstraction

The first script can be direct and boring. Once you repeat authorization, pagination, rate limiting, audit logging, and error formatting across three workflows, create a connector.

Name the abstraction in business terms: sendMessage(), createTicket(), recordPaymentEvent(), writeAudit(). A thin callSlackApi() wrapper is less useful to Claude Code than a function that says what outcome the workflow needs.

For adjacent implementation detail, pair this with Claude Code API Design Assistant and Claude Code API Testing. For risk review, read Claude Code Security Best Practices before production access.

Official References

If you are bringing Claude Code integrations into team workflows, prepare reusable templates before the first incident. The practical checklists are in /products/, and team enablement is available through /training/.

After testing this in practice, the integrations that included signature verification, idempotency, audit logs, and separate test environments from day one were much easier to extend. Even a small Slack notification becomes more reliable when failure can be traced and replayed deliberately.

#claude-code #saas #notion #slack #linear #github #figma #integration
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.