Use Cases (Aktualisiert: 2.6.2026)

Twilio SMS mit Claude Code: Notifications, Verify und Webhooks produktionsreif

Baue Twilio SMS mit Claude Code: E.164, Consent, Idempotenz, Retries, Verify und Status Callbacks.

Twilio SMS mit Claude Code: Notifications, Verify und Webhooks produktionsreif

SMS ist weiterhin nützlich, wenn Nutzer die App nicht geöffnet haben. Versandmeldungen, Termin-Erinnerungen, Incident Alerts, Login-Verifikation und Support-Bestätigungen sind typische Fälle, in denen E-Mail oder Push allein nicht reichen.

Eine Twilio-SMS-Integration wirkt klein, aber Produktionscode besteht nicht nur aus client.messages.create. Du brauchst Telefonnummernvalidierung, Consent-Nachweis, Schutz vor doppeltem Versand, gezielte Retries, Status Callbacks, Webhook-Signaturprüfung und Logs ohne unnötige personenbezogene Daten.

Dieser Guide zeigt, wie du Claude Code eine brauchbare Express- und TypeScript-Integration erzeugen lässt. Enthalten sind ausgehende SMS, Twilio Verify, Status Callbacks, Idempotenz, Retry-Verhalten, Logging, Security und typische Fehler. Für angrenzende Themen siehe Authentication Implementation, Webhook Implementation und Secrets Management.

Twilio SMS einfach erklärt

Twilio stellt Kommunikationsfunktionen als API bereit. Dein Backend sendet eine Anfrage an Twilio: diesen Text, von diesem Absender, an diese Telefonnummer. Twilio übergibt die Nachricht an das Carrier-Netz und liefert eine Message SID zurück. Diese SID ist wichtig für Support, Tracking und Fehleranalyse.

Telefonnummern sollten im E.164-Format gesendet werden: Pluszeichen, Ländercode und Nummer, zum Beispiel +15558675310 oder +819012345678. Das ist nicht unbedingt die lokale Schreibweise für Menschen, sondern die API-sichere Form. Twilios Hinweise zur internationalen Nummernformatierung sind hier die Primärquelle.

Die erste API-Antwort bedeutet noch nicht, dass die SMS angekommen ist. Der Status kann später queued, sent, delivered, undelivered oder failed werden. Twilio kann diese Änderungen an deinen Status Callback senden. Prüfe dazu die offiziellen Seiten zu Programmable Messaging, SMS mit Node.js, Messaging Webhooks und Outbound Status Callbacks.

Realistische Use Cases

Starte nicht mit einem generischen SMS-Helper. Definiere zuerst das Business Event und die Fehlerrisiken.

Use CaseWarum SMS hilftWorauf achten
Bestellung und VersandKunden verpassen E-Mail, brauchen aber StatusFalsche Tracking-URL, Doppelversand, Opt-out
TerminerinnerungReduziert No-shows und VerwirrungZeitzone, Ruhezeiten, Consent
Incident oder Admin AlertErreicht Bereitschaft auch ohne Slack/E-MailAlert-Flut, Rate Limits, Eskalation
Login und 2FASchützt AccountsTwilio Verify vor eigenem OTP prüfen
Support-BestätigungNutzer wissen, dass die Anfrage ankamKeine sensiblen Details im Text

Preise, unterstützte Länder, Sender-Registrierung, A2P-ähnliche Regeln und Compliance-Anforderungen ändern sich. Dieser Artikel schreibt solche Fakten nicht fest. Prüfe vor Launch die aktuelle Twilio Console, offizielle Docs und die zuständige Rechts- oder Compliance-Prüfung.

Prompt für Claude Code

Fordere Produktionsverhalten an, nicht nur einen API Call.

Implement Twilio SMS notifications in Express + TypeScript.

Requirements:
- Read Twilio credentials, sender number, and Verify Service SID from env vars
- Validate phone numbers in E.164 format with Zod
- Add POST /api/order-shipped-sms for order shipment SMS
- Use eventId as the idempotency key so duplicate events do not send twice
- Retry only 429 and 5xx-style transient failures
- Never log full phone numbers, full message bodies, Auth Tokens, or OTP codes
- Receive status callbacks at POST /twilio/status-callback
- Require Twilio signature validation in production
- Add Twilio Verify start/check endpoints
- Include .env.example, package.json, run commands, and curl examples

Idempotenz bedeutet, dass dasselbe Event erneut verarbeitet werden kann, ohne eine zweite externe Wirkung auszulösen. Bei SMS ist das zentral, weil Queue-Retries, Webhook-Redelivery, Batch-Replays und manuelle Support-Aktionen denselben Versand mehrfach anstoßen können.

flowchart LR
  A["Order update"] --> B["Idempotenzcheck"]
  B --> C["Twilio Messaging API"]
  C --> D["SMS-Zustellung"]
  C --> E["Message SID speichern"]
  D --> F["Status Callback"]
  F --> G["Signatur prüfen"]
  G --> H["Delivery Log aktualisieren"]
  I["Login Check"] --> J["Twilio Verify"]

Minimalprojekt erstellen

Dieses Projekt lässt sich kopieren und starten. Ohne echte Twilio-Zugangsdaten schlägt der Versand fehl, aber Environment-Parsing, Request-Validierung, Duplikatbehandlung und lokales Callback-Parsing lassen sich testen.

mkdir twilio-sms-demo
cd twilio-sms-demo
npm init -y
npm install express twilio dotenv zod
npm install -D typescript tsx @types/express
{
  "type": "module",
  "scripts": {
    "dev": "tsx src/app.ts"
  },
  "dependencies": {
    "dotenv": "latest",
    "express": "latest",
    "twilio": "latest",
    "zod": "latest"
  },
  "devDependencies": {
    "@types/express": "latest",
    "tsx": "latest",
    "typescript": "latest"
  }
}
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}
# .env.example
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=replace-with-your-auth-token
TWILIO_FROM_NUMBER=+15551234567
TWILIO_VERIFY_SERVICE_SID=VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PUBLIC_BASE_URL=https://example.ngrok-free.app
REQUIRE_TWILIO_SIGNATURE=true
PORT=3000

PUBLIC_BASE_URL muss eine HTTPS-URL sein, die Twilio erreichen kann. Lokal eignen sich ngrok oder Cloudflare Tunnel. Signaturprüfung hängt an der exakten URL, also prüfe Protokoll, Proxy, Query String und Slash am Ende.

SMS, Idempotenz und Callbacks implementieren

Erstelle src/app.ts und füge diesen Code ein. Das Demo nutzt eine In-Memory-Map; in Produktion brauchst du PostgreSQL, Redis, DynamoDB oder einen anderen dauerhaften Store mit Unique Constraint auf dem Idempotenzschlüssel.

import "dotenv/config";
import express from "express";
import twilio from "twilio";
import { z } from "zod";

const e164Schema = z.string().regex(/^\+[1-9]\d{1,14}$/, {
  message: "Use E.164 format, for example +819012345678.",
});

const envSchema = z.object({
  TWILIO_ACCOUNT_SID: z.string().regex(/^AC[a-fA-F0-9]{32}$/),
  TWILIO_AUTH_TOKEN: z.string().min(20),
  TWILIO_FROM_NUMBER: e164Schema,
  TWILIO_VERIFY_SERVICE_SID: z.string().regex(/^VA[a-fA-F0-9]{32}$/).optional(),
  PUBLIC_BASE_URL: z.string().url(),
  REQUIRE_TWILIO_SIGNATURE: z.enum(["true", "false"]).default("true"),
  PORT: z.coerce.number().int().positive().default(3000),
});

const env = envSchema.parse(process.env);
const client = twilio(env.TWILIO_ACCOUNT_SID, env.TWILIO_AUTH_TOKEN);
const app = express();

type Delivery = {
  status: "pending" | "sent" | "failed";
  attempts: number;
  updatedAt: string;
  sid?: string;
  error?: string;
};

const deliveries = new Map<string, Delivery>();

const orderSmsSchema = z.object({
  eventId: z.string().min(6).max(120),
  phone: e164Schema,
  orderId: z.string().min(1).max(80),
  trackingUrl: z.string().url().optional(),
  consentAt: z.string().datetime(),
});

const statusCallbackSchema = z.object({
  MessageSid: z.string().min(2),
  MessageStatus: z.string().min(2),
  To: z.string().optional(),
  ErrorCode: z.string().optional(),
}).passthrough();

function maskPhone(phone: string) {
  return phone.replace(/\d(?=\d{4})/g, "*");
}

function delay(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function getErrorStatus(error: unknown) {
  if (typeof error === "object" && error && "status" in error) {
    return Number((error as { status?: number }).status ?? 0);
  }
  return 0;
}

function getErrorMessage(error: unknown) {
  return error instanceof Error ? error.message : String(error);
}

function shouldRetry(error: unknown) {
  const status = getErrorStatus(error);
  return status === 429 || status >= 500;
}

async function sendSmsWithRetry(params: {
  to: string;
  body: string;
  statusCallback: string;
  maxAttempts?: number;
}) {
  const maxAttempts = params.maxAttempts ?? 3;

  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    try {
      const message = await client.messages.create({
        body: params.body,
        from: env.TWILIO_FROM_NUMBER,
        statusCallback: params.statusCallback,
        to: params.to,
      });

      return { sid: message.sid, attempts: attempt };
    } catch (error) {
      if (attempt === maxAttempts || !shouldRetry(error)) {
        throw error;
      }
      await delay(500 * attempt);
    }
  }

  throw new Error("SMS retry loop ended unexpectedly.");
}

function verifyTwilioSignature(req: express.Request) {
  const signature = req.header("x-twilio-signature") ?? "";
  const callbackUrl = new URL(req.originalUrl, env.PUBLIC_BASE_URL).toString();
  return twilio.validateRequest(env.TWILIO_AUTH_TOKEN, signature, callbackUrl, req.body);
}

app.use(express.json());

app.post("/api/order-shipped-sms", async (req, res) => {
  const parsed = orderSmsSchema.safeParse(req.body);

  if (!parsed.success) {
    return res.status(400).json({
      error: "invalid_request",
      details: parsed.error.flatten(),
    });
  }

  const input = parsed.data;
  const idempotencyKey = `order-shipped:${input.eventId}`;
  const existing = deliveries.get(idempotencyKey);

  if (existing?.status === "sent") {
    return res.status(200).json({
      duplicate: true,
      sid: existing.sid,
      status: existing.status,
    });
  }

  if (existing?.status === "pending") {
    return res.status(202).json({
      duplicate: true,
      status: existing.status,
    });
  }

  deliveries.set(idempotencyKey, {
    attempts: 0,
    status: "pending",
    updatedAt: new Date().toISOString(),
  });

  const trackingText = input.trackingUrl ? ` Tracking: ${input.trackingUrl}` : "";
  const body = `Your order ${input.orderId} has shipped.${trackingText}`;
  const statusCallback = new URL("/twilio/status-callback", env.PUBLIC_BASE_URL).toString();

  try {
    const result = await sendSmsWithRetry({
      body,
      statusCallback,
      to: input.phone,
    });

    deliveries.set(idempotencyKey, {
      attempts: result.attempts,
      sid: result.sid,
      status: "sent",
      updatedAt: new Date().toISOString(),
    });

    console.log("sms_sent", {
      idempotencyKey,
      sid: result.sid,
      to: maskPhone(input.phone),
    });

    return res.status(202).json({ accepted: true, sid: result.sid });
  } catch (error) {
    deliveries.set(idempotencyKey, {
      attempts: 3,
      error: getErrorMessage(error),
      status: "failed",
      updatedAt: new Date().toISOString(),
    });

    console.error("sms_failed", {
      idempotencyKey,
      message: getErrorMessage(error),
      status: getErrorStatus(error),
      to: maskPhone(input.phone),
    });

    return res.status(502).json({ error: "sms_delivery_failed" });
  }
});

app.post("/twilio/status-callback", express.urlencoded({ extended: false }), (req, res) => {
  if (env.REQUIRE_TWILIO_SIGNATURE === "true" && !verifyTwilioSignature(req)) {
    return res.status(403).send("invalid signature");
  }

  const parsed = statusCallbackSchema.safeParse(req.body);

  if (!parsed.success) {
    return res.status(400).send("invalid callback");
  }

  console.log("twilio_status", {
    errorCode: parsed.data.ErrorCode,
    sid: parsed.data.MessageSid,
    status: parsed.data.MessageStatus,
    to: parsed.data.To ? maskPhone(parsed.data.To) : undefined,
  });

  return res.status(204).send();
});

app.listen(env.PORT, () => {
  console.log(`Twilio SMS demo listening on http://localhost:${env.PORT}`);
});

Starte den Server und sende einen Request. Für echte Zustellung brauchst du gültige Twilio-Credentials, einen Absender, eine öffentliche Callback-URL und ein Ziel, das dein Account senden darf.

npm run dev
curl -X POST http://localhost:3000/api/order-shipped-sms \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "order_1001_shipped_v1",
    "phone": "+15558675310",
    "orderId": "1001",
    "trackingUrl": "https://example.com/track/1001",
    "consentAt": "2026-06-02T09:00:00.000Z"
  }'

Mit derselben eventId sendet die API keine zweite SMS, sondern gibt den vorhandenen Status zurück. In Produktion muss dieser Status dauerhaft gespeichert werden.

Für einen lokalen Callback-Test kannst du kurz REQUIRE_TWILIO_SIGNATURE=false setzen. In Produktion bleibt es true.

curl -X POST http://localhost:3000/twilio/status-callback \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "MessageSid=SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  --data-urlencode "MessageStatus=delivered" \
  --data-urlencode "To=+15558675310"

Twilio Verify für OTP nutzen

Für Login und 2FA solltest du nicht mit sechs eigenen Zufallsziffern starten. OTP braucht Ablaufzeiten, Resend Limits, Schutz gegen Brute Force, Kanalregeln, Nummernwechsel und Audit Logs. Twilio Verify und die Verification API sind dafür vorgesehen.

Füge diesen Code vor app.listen in src/app.ts ein.

const verifyStartSchema = z.object({
  phone: e164Schema,
});

const verifyCheckSchema = z.object({
  code: z.string().min(4).max(10),
  phone: e164Schema,
});

function requireVerifyServiceSid() {
  if (!env.TWILIO_VERIFY_SERVICE_SID) {
    throw new Error("TWILIO_VERIFY_SERVICE_SID is required for Verify.");
  }
  return env.TWILIO_VERIFY_SERVICE_SID;
}

app.post("/api/verify/start", async (req, res) => {
  const parsed = verifyStartSchema.safeParse(req.body);

  if (!parsed.success) {
    return res.status(400).json({ error: "invalid_request" });
  }

  const verification = await client.verify.v2
    .services(requireVerifyServiceSid())
    .verifications.create({
      channel: "sms",
      to: parsed.data.phone,
    });

  return res.status(202).json({ sid: verification.sid, status: verification.status });
});

app.post("/api/verify/check", async (req, res) => {
  const parsed = verifyCheckSchema.safeParse(req.body);

  if (!parsed.success) {
    return res.status(400).json({ error: "invalid_request" });
  }

  const check = await client.verify.v2
    .services(requireVerifyServiceSid())
    .verificationChecks.create({
      code: parsed.data.code,
      to: parsed.data.phone,
    });

  return res.json({ approved: check.status === "approved", status: check.status });
});

Nach erfolgreicher Verify-Prüfung aktualisierst du deine eigene User-Tabelle, etwa phoneVerifiedAt oder mfaEnabledAt. Für den gesamten Auth-Bereich helfen der Authentication Guide und der Zod Validation Guide.

SMS geht direkt an eine persönliche Telefonnummer. Speichere, welchem Zweck der Nutzer zugestimmt hat, wo diese Zustimmung erfolgte und wie Opt-out oder Suppression verarbeitet werden. Anforderungen variieren nach Land, Sender-Typ und Inhalt; nutze aktuelle Twilio-Dokumentation und rechtliche Prüfung.

Echte Account SID, Auth Token, OTP, vollständige Telefonnummern und komplette Nachrichten gehören nicht in Code, Prompts, Screenshots oder Logs. .env bleibt aus Git heraus, Produktion bekommt Secrets über Plattform oder Secret Manager. Logs brauchen meist Message SID, Event ID, Nachrichtentyp, maskierte Nummer, Twilio Error Code, Versuchszahl und Timestamp.

Häufige Fehler

Typische Fehler sind lokale Nummern ohne E.164, Queue-Retries ohne Idempotenz, öffentliche Callbacks ohne Signaturprüfung, selbst gebaute OTP-Flows und Logs ohne Message SID. Für asynchrone Verarbeitung siehe Queue Systems, für Abwehrmaßnahmen Security Best Practices.

Review-Prompt

Review this Twilio SMS implementation before production.

Check:
- E.164 validation always runs before sending
- Consent timestamp and message purpose are tracked
- eventId idempotency holds under parallel requests
- Only 429 and 5xx transient failures are retried
- Twilio status callback signature validation is required in production
- Auth Tokens, OTP codes, full phone numbers, and full bodies never reach logs
- Pricing, countries, or regulatory rules are not hard-coded in comments
- Support can trace a failure by Message SID

Dieses Thema eignet sich für monetarisierbaren Content, weil Leser meist mehr brauchen als eine Funktion: Auth, Queues, Webhooks, Logs, CLAUDE.md und Review Gates. ClaudeCodeLab kann das über Claude Code Training und Beratung in einen repository-spezifischen Workflow übersetzen.

Fazit

Twilio SMS beginnt mit einem kurzen API Call, aber Produktionsqualität hängt an E.164, Consent, Idempotenz, Retries, Callback-Signatur und datensparsamen Logs. Gib Claude Code diese Anforderungen direkt im ersten Prompt und prüfe das Ergebnis als operative Integration.

Im praktischen Check zu diesem Artikel wurden lokale E.164-Validierung, doppeltes eventId-Handling, Status-Callback-Parsing und maskierte Logs geprüft. Echte SMS-Zustellung hängt weiter von Twilio-Credentials, Sender-Setup, Zielland und aktuellen Twilio-Regeln ab. Vor Launch mit einer kontrollierten Testnummer Message SID und Callback-Status nachverfolgen.

#Claude Code #Twilio #SMS #Benachrichtigungen #API-Integration
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.