Use Cases (Diperbarui: 2/6/2026)

Twilio SMS dengan Claude Code: notifikasi, Verify, dan Webhook produksi

Bangun Twilio SMS dengan Claude Code: E.164, consent, idempotency, retry, Verify, dan status callback.

Twilio SMS dengan Claude Code: notifikasi, Verify, dan Webhook produksi

SMS masih berguna saat pengguna tidak membuka aplikasi. Pengiriman pesanan, pengingat janji, alert insiden, verifikasi login, dan balasan awal support sering membutuhkan channel yang lebih langsung daripada email atau push notification.

Integrasi Twilio SMS terlihat kecil, tetapi produksi tidak cukup dengan client.messages.create. Anda perlu validasi nomor, catatan consent, pencegahan duplikasi, retry yang selektif, status callback, validasi tanda tangan Webhook, dan log yang tidak membocorkan data pribadi.

Panduan ini menunjukkan cara meminta Claude Code membuat integrasi Twilio SMS yang praktis dengan Express + TypeScript. Contohnya mencakup outbound SMS, Twilio Verify, status callback, idempotency, retry, logging, security, dan kesalahan yang sering terjadi. Untuk konteks terkait, baca juga implementasi autentikasi, implementasi Webhook, dan secrets management.

Twilio SMS dengan bahasa sederhana

Twilio menyediakan komunikasi sebagai API. Backend Anda meminta Twilio mengirim teks ini, dari sender ini, ke nomor ini. Twilio meneruskan pesan ke jaringan operator dan mengembalikan Message SID, yaitu ID yang dipakai untuk support dan pelacakan status.

Nomor telepon sebaiknya dikirim dalam format E.164: tanda plus, kode negara, lalu nomor, misalnya +15558675310 atau +819012345678. Ini format aman untuk API, bukan selalu format lokal yang dilihat pengguna. Gunakan panduan resmi Twilio tentang format nomor internasional.

Respons API pertama belum berarti SMS sudah diterima. Status bisa berubah menjadi queued, sent, delivered, undelivered, atau failed. Twilio dapat mengirim perubahan itu ke endpoint status callback Anda. Saat mengadaptasi kode, rujuk Programmable Messaging, tutorial SMS dengan Node.js, Messaging Webhooks, dan panduan outbound status callbacks.

Use case yang realistis

Jangan mulai dari helper generik “send SMS”. Mulai dari event bisnis dan aturan kegagalannya.

Use caseMengapa SMS bergunaYang perlu dijaga
Order dan pengirimanPelanggan mungkin melewatkan email, tapi butuh statusURL tracking salah, duplikasi, opt-out
Pengingat janjiMengurangi no-show dan kebingunganZona waktu, jam sensitif, consent
Alert insiden/adminMenjangkau on-call di luar Slack atau emailAlert storm, rate limit, eskalasi
Login dan 2FAMembantu melindungi akunPertimbangkan Twilio Verify, bukan OTP buatan sendiri
Konfirmasi supportPengguna tahu request sudah diterimaJangan masukkan data sensitif ke body

Harga, negara yang didukung, registrasi sender, aturan seperti A2P, dan kewajiban compliance dapat berubah. Artikel ini tidak mengunci klaim tersebut. Sebelum launch, cek Twilio Console, dokumentasi resmi terbaru, dan review legal atau compliance.

Prompt untuk Claude Code

Minta perilaku operasional, bukan hanya pemanggilan API.

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

Idempotency berarti event bisnis yang sama bisa diproses ulang tanpa efek eksternal kedua. Dalam SMS, ini penting karena retry queue, Webhook redelivery, batch replay, atau tindakan manual support bisa memicu event yang sama.

flowchart LR
  A["Order update"] --> B["Idempotency check"]
  B --> C["Twilio Messaging API"]
  C --> D["SMS delivery"]
  C --> E["Store Message SID"]
  D --> F["Status Callback"]
  F --> G["Signature check"]
  G --> H["Delivery log update"]
  I["Login check"] --> J["Twilio Verify"]

Membuat proyek minimal

Proyek berikut bisa disalin dan dijalankan. Tanpa kredensial Twilio asli, pengiriman SMS tidak berhasil, tetapi parsing environment, validasi request, penanganan duplikasi, dan callback lokal tetap bisa diuji.

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 harus berupa URL HTTPS yang dapat diakses Twilio. Untuk lokal, gunakan ngrok atau Cloudflare Tunnel. Validasi tanda tangan Twilio sensitif terhadap URL persis, jadi periksa protokol, proxy, query string, dan trailing slash.

Implementasi SMS, idempotency, dan callbacks

Buat src/app.ts lalu tempel kode ini. Demo memakai Map di memori; produksi sebaiknya memakai PostgreSQL, Redis, DynamoDB, atau store durable lain dengan unique constraint pada idempotency key.

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}`);
});

Jalankan server dan kirim request. Pengiriman nyata membutuhkan kredensial Twilio valid, sender, callback publik, dan nomor tujuan yang diizinkan oleh akun.

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"
  }'

Jika eventId yang sama dikirim lagi, API mengembalikan state yang sudah ada, bukan mengirim SMS kedua. Di produksi, simpan state ini di database durable.

Untuk menguji bentuk callback lokal, sementara gunakan REQUIRE_TWILIO_SIGNATURE=false. Di produksi, tetap gunakan 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"

Gunakan Twilio Verify untuk OTP

Untuk login dan 2FA, jangan mulai dari membuat enam digit sendiri. OTP mencakup masa berlaku, batas resend, perlindungan brute-force, channel, perubahan nomor, dan audit log. Twilio Verify dan Verification API dibuat untuk kebutuhan itu.

Tambahkan kode berikut sebelum app.listen di src/app.ts.

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 });
});

Setelah Verify disetujui, update tabel user Anda, misalnya phoneVerifiedAt atau mfaEnabledAt. Untuk boundary autentikasi lengkap, gabungkan dengan panduan autentikasi dan panduan Zod validation.

SMS langsung masuk ke nomor pribadi. Catat apa yang disetujui pengguna, di mana consent terjadi, dan bagaimana opt-out atau suppression diproses. Kewajiban berubah menurut negara, tipe sender, dan isi pesan, jadi gunakan dokumentasi Twilio terbaru dan review legal.

Jangan menaruh Account SID, Auth Token, OTP, nomor lengkap, atau isi pesan lengkap di code, prompt, screenshot, atau log. .env tidak masuk Git; produksi menginjeksikan secret lewat platform atau secret manager. Log biasanya cukup berisi Message SID, event ID, tipe pesan, nomor yang dimasking, kode error Twilio, jumlah percobaan, dan timestamp.

Kesalahan umum

Kesalahan yang sering terjadi: mengirim nomor lokal tanpa E.164, queue retry menggandakan SMS, callback publik tanpa signature validation, membuat OTP sendiri, dan log tanpa Message SID. Untuk desain async, baca queue systems; untuk sisi pertahanan, baca security best practices.

Prompt review Claude Code

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

Topik ini juga cocok untuk konten yang menghasilkan revenue karena pembaca biasanya butuh lebih dari fungsi SMS: auth, queues, Webhooks, logs, CLAUDE.md, dan review gate. ClaudeCodeLab dapat membantu menjadikannya workflow repo nyata melalui Claude Code training dan konsultasi.

Ringkasan

Twilio SMS dimulai dari API call pendek, tetapi kualitas produksi bergantung pada E.164, consent, idempotency, retry, signature callback, dan log yang menjaga privasi. Berikan requirement ini kepada Claude Code sejak prompt pertama dan review hasilnya sebagai integrasi operasional.

Dalam pengecekan praktis untuk artikel ini, alur lokal memvalidasi E.164, penanganan eventId duplikat, parsing Status Callback, dan bentuk log yang dimasking. Pengiriman SMS nyata tetap bergantung pada kredensial Twilio, setup sender, negara tujuan, dan aturan Twilio saat ini; sebelum launch, uji dengan nomor terkontrol dan ikuti Message SID sampai callback.

#Claude Code #Twilio #SMS #notifikasi #integrasi API
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.