Skip to content
Back to skills

Stacks Sms

ASecurity

Use when implementing SMS in Stacks - sending text messages, the SmsBuilder fluent API, SMS templates, phone verification (OTP/2FA), bulk sending, Twilio/Vonage drivers, E.164 formatting, or the SMS facade. Covers @stacksjs/sms and config/sms.ts.

  • 631 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 5, 2026
developmenttypescriptgobashawsapi

Works with

  • api

Security analysis

A100/100

Scanned September 3, 2026

npx -y skills add stacksjs/stacks --skill stacks-sms --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Stacks Sms?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Stacks Sms
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/stacksjs-stacks-sms/badge)](https://www.skillsdirectory.com/skills/stacksjs-stacks-sms)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: stacks-sms
description: Use when implementing SMS in Stacks - sending text messages, the SmsBuilder fluent API, SMS templates, phone verification (OTP/2FA), bulk sending, Twilio/Vonage drivers, E.164 formatting, or the SMS facade. Covers @stacksjs/sms and config/sms.ts.
license: MIT
compatibility: Bun >= 1.3.0, TypeScript
allowed-tools: Read Edit Write Bash Grep Glob
---

# Stacks SMS

Multi-driver SMS system with verification (OTP), templates, bulk sending, and a fluent builder API. Two fully implemented drivers: Twilio and Vonage.

## Key Paths
- Core package: `storage/framework/core/sms/src/`
- SMS facade and builder: `storage/framework/core/sms/src/sms.ts`
- Twilio driver: `storage/framework/core/sms/src/drivers/twilio.ts`
- Vonage driver: `storage/framework/core/sms/src/drivers/vonage.ts`
- Drivers index: `storage/framework/core/sms/src/drivers/index.ts`
- Configuration: `config/sms.ts`

## Package Exports

```typescript
// Main facade and functions
import SMS, { sms, send, sendSms, sendBulk, sendTemplate } from '@stacksjs/sms'
import { startVerification, checkVerification, cancelVerification } from '@stacksjs/sms'
import { formatE164, isValidPhoneNumber, isEnabled, getConfig, configure, getDriver, init } from '@stacksjs/sms'
import { SmsBuilder } from '@stacksjs/sms'

// Drivers
import { TwilioDriver, createTwilioDriver } from '@stacksjs/sms'
import { VonageDriver, createVonageDriver } from '@stacksjs/sms'
```

## SMS Facade Object

The `SMS` default export aggregates all functions:

```typescript
SMS.init()                    // Load config from config/sms.ts
SMS.configure(config)         // Override config at runtime
SMS.isEnabled()               // Check if SMS is enabled (config.enabled)
SMS.getConfig()               // Get current config (returns copy)

SMS.send(message)             // Send single SMS
SMS.sendSms(message)          // Alias for send
SMS.sendBulk(messages)        // Send multiple SMS
SMS.sendTemplate(to, name, vars)  // Send using template

SMS.getStatus(messageId)      // Get delivery status
SMS.getBalance()              // Get account balance
SMS.verifyNumber(phone)       // Lookup/verify phone number

SMS.startVerification(req)    // Start OTP flow
SMS.checkVerification(req)    // Verify OTP code
SMS.cancelVerification(id)    // Cancel pending verification

SMS.formatE164(phone, cc?)    // Format to E.164
SMS.isValidPhoneNumber(phone) // Validate E.164 format

SMS.sms()                     // Create SmsBuilder instance
SMS.getDriver(provider?)      // Get driver instance
SMS.getVerificationDriver(p?) // Get verification driver
```

## SmsBuilder (Fluent API)

```typescript
import { sms } from '@stacksjs/sms'

// Basic SMS
await sms()
  .to('+1234567890')
  .body('Your code is 123456')
  .from('+0987654321')
  .send()

// With provider selection
await sms()
  .to('+1234567890')
  .body('Hello!')
  .via('vonage')
  .send()

// MMS with media and callback
await sms()
  .to(['+1111111111', '+2222222222'])
  .body('Check this out')
  .media(['https://example.com/image.jpg'])
  .callback('https://myapp.com/sms/status')
  .send()

// .text() is an alias for .body()
await sms()
  .to('+1234567890')
  .text('Hello!')
  .send()
```

The builder validates that `to` and `body` are set before sending. If missing, it returns a failed `SmsSendResult` without throwing. If `.via(provider)` is set, it creates a new driver for that provider; otherwise uses the default driver.

## Direct Send Functions

```typescript
import { send, sendBulk } from '@stacksjs/sms'

// SmsMessage shape
await send({
  to: '+1234567890',         // string or string[]
  body: 'Hello!',
  from: '+0987654321',       // optional, falls back to config
  mediaUrls: ['url'],        // optional, for MMS
  statusCallback: 'url',     // optional, webhook for status updates
})

// Bulk send
await sendBulk([msg1, msg2, msg3])  // returns SmsSendResult[]
```

`send()` and all sending functions call `ensureConfig()` first, which lazily loads `config/sms.ts` via dynamic import.

## Message Status & Info

```typescript
import { getStatus, verifyNumber, getBalance } from '@stacksjs/sms'

// Get delivery status for a message
const status = await getStatus('SM_message_id')
// Returns SmsStatusUpdate | null (null if driver doesn't support it)

// Verify/lookup a phone number
const info = await verifyNumber('+1234567890')
// Returns { valid: boolean, carrier?: string, type?: string }

// Get account balance
const balance = await getBalance()
// Returns { balance: number, currency: string } | null
```

## Phone Verification (OTP/2FA)

```typescript
import { startVerification, checkVerification, cancelVerification } from '@stacksjs/sms'

// Start -- sends OTP code via SMS (or 'call', 'email' for Twilio; 'sms', 'whatsapp' for Vonage)
const result = await startVerification({
  to: '+1234567890',
  channel: 'sms',        // default: 'sms'
  codeLength: 6,          // optional
  locale: 'en',           // optional
  customMessage: 'Your code: {code}',  // optional, Twilio only
})
// Returns { success: boolean, verificationId?: string, status: 'pending' | 'denied', error?: string }

// Check -- verify the code the user entered
const check = await checkVerification({
  to: '+1234567890',           // Twilio requires 'to'
  verificationId: result.verificationId,  // Vonage requires this
  code: '123456',
})
// Returns { success: boolean, verificationId?: string, status: 'approved' | 'denied', error?: string }

// Cancel -- cancel a pending verification
const cancelled = await cancelVerification(result.verificationId!)
// Returns boolean
```

**Twilio** uses the Verify service (`verify.twilio.com/v2`) and requires `verifyServiceSid` passed to the TwilioDriver constructor.

**Vonage** uses the Verify V2 API (`api.nexmo.com/v2/verify`). Whatsapp channel maps to `whatsapp_interactive`.

## SMS Templates

```typescript
import { sendTemplate } from '@stacksjs/sms'

await sendTemplate('+1234567890', 'order-confirmation', {
  orderNumber: '#12345',
  total: '$29.99',
})
```

Templates are defined in `config/sms.ts` under `templates[]`. Each template has `name`, `body`, and optional `variables`. Variables use single-brace syntax `{variableName}` in the template body. The function finds the template by name, replaces variables, and sends via the default driver.

Returns a failed result (without throwing) if the template is not found.

## Phone Number Utilities

```typescript
import { formatE164, isValidPhoneNumber } from '@stacksjs/sms'

// Normalize to E.164 format
formatE164('+1 (234) 567-890')        // '+12345678900'
formatE164('2345678900', '1')          // '+12345678900'
formatE164('002345678900')             // '+2345678900' (00 prefix stripped)

// Validate E.164 format (regex: /^\+[1-9]\d{6,14}$/)
isValidPhoneNumber('+1234567890')      // true
isValidPhoneNumber('1234567890')       // true (formatted first, then validated)
```

`formatE164()` strips spaces, dashes, and parentheses. If the number starts with `+`, returns as-is. If it starts with `00`, replaces with `+`. Otherwise prepends `+` and the default country code (from `config.defaultCountryCode` or `'1'` for US).

## Twilio Driver

```typescript
import { TwilioDriver, createTwilioDriver } from '@stacksjs/sms'

const driver = new TwilioDriver({
  accountSid: 'AC...',
  authToken: '...',
  from: '+1234567890',
  messagingServiceSid: 'MG...',    // optional, used instead of 'from'
  statusCallback: 'https://...',   // optional, default callback URL
}, 'VA_verify_service_sid')         // optional second arg for verification

// Or use factory
const driver = createTwilioDriver(config, verifyServiceSid?)
```

Uses REST API at `https://api.twilio.com/2010-04-01`. Auth via HTTP Basic (`accountSid:authToken`).

Features:
- `send(message)` -- sends via Messages API, supports MMS (mediaUrls), statusCallback, messagingServiceSid
- `sendBulk(messages)` -- sends in parallel via `Promise.all`
- `getStatus(messageId)` -- retrieves message status
- `verify(phoneNumber)` -- uses Twilio Lookup API v2 with line_type_intelligence
- `getBalance()` -- retrieves account balance
- `startVerification(req)` -- Twilio Verify service
- `checkVerification(req)` -- check OTP code
- `cancelVerification(id)` -- cancel pending verification

Status mapping: queued, sending, sent, delivered, undelivered -> `SmsStatus`; canceled -> 'failed'.

## Vonage Driver

```typescript
import { VonageDriver, createVonageDriver } from '@stacksjs/sms'

const driver = new VonageDriver({
  apiKey: '...',
  apiSecret: '...',
  from: 'MyApp',
  applicationId: '...',    // optional, for JWT auth
  privateKey: '...',        // optional, for JWT auth
}, true)                    // optional: useMessagesApi (default: false)

// Or use factory
const driver = createVonageDriver(config, useMessagesApi?)
```

Two send modes:
- **SMS API** (legacy, default) -- `https://rest.nexmo.com/sms/json`, uses API key/secret in body
- **Messages API** (newer) -- `https://api.nexmo.com/v1/messages`, uses Basic auth or JWT

Features:
- `send(message)` -- sends via chosen API mode
- `sendBulk(messages)` -- sends in parallel
- `getStatus(messageId)` -- Messages API only
- `verify(phoneNumber)` -- Vonage Number Insight API (basic)
- `getBalance()` -- account balance in EUR
- `startVerification(req)` -- Vonage Verify V2 API
- `checkVerification(req)` -- requires `verificationId`
- `cancelVerification(id)` -- DELETE request to cancel

The `nexmo` export is a legacy alias for `vonage`.

## Other Drivers (Commented Out / Placeholder)

The `drivers/` directory contains placeholder files for: `gupshup`, `nexmo` (alias for vonage), `plivo`, `sms77`, `sns`, `telnyx`, `termii`. These are all commented out and not functional. Only Twilio and Vonage are active.

## config/sms.ts

```typescript
{
  enabled: false,                    // Must explicitly enable
  provider: 'twilio',               // 'twilio' | 'vonage'
  from: env.SMS_FROM_NUMBER || '',
  defaultCountryCode: '1',
  messageType: 'TRANSACTIONAL',     // TRANSACTIONAL or PROMOTIONAL

  drivers: {
    twilio: {
      accountSid: env.TWILIO_ACCOUNT_SID || '',
      authToken: env.TWILIO_AUTH_TOKEN || '',
      from: env.TWILIO_FROM_NUMBER || '',
      messagingServiceSid: env.TWILIO_MESSAGING_SERVICE_SID || '',
    },
    vonage: {
      apiKey: env.VONAGE_API_KEY || '',
      apiSecret: env.VONAGE_API_SECRET || '',
      from: env.VONAGE_FROM_NUMBER || '',
    },
    pinpoint: {
      region: env.AWS_REGION || 'us-east-1',
      accessKeyId: env.AWS_ACCESS_KEY_ID || '',
      secretAccessKey: env.AWS_SECRET_ACCESS_KEY || '',
      senderId: env.SMS_SENDER_ID || '',
      originationNumber: env.SMS_ORIGINATION_NUMBER || '',
    },
  },

  maxSpendPerMonth: 100,
  optOut: {
    enabled: true,
    keywords: ['STOP', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT'],
  },
  templates: [],                     // Array of { name, body, variables }
  twoWay: {
    enabled: false,
  },
} satisfies SmsConfig
```

## Type Interfaces (from @stacksjs/types)

```typescript
interface SmsMessage {
  to: string | string[]
  body: string
  from?: string
  mediaUrls?: string[]
  statusCallback?: string
}

interface SmsSendResult {
  success: boolean
  messageId?: string
  status?: SmsStatus
  to: string
  provider: string
  error?: string
  segments?: number
  price?: number
  currency?: string
}

type SmsStatus = 'queued' | 'sending' | 'sent' | 'delivered' | 'undelivered' | 'failed' | 'unknown'

interface SmsStatusUpdate {
  messageId: string
  to: string
  status: SmsStatus
  timestamp: Date
  errorCode?: string
  errorMessage?: string
}

type SmsProvider = 'twilio' | 'vonage' | 'pinpoint'

interface VerificationRequest {
  to: string
  channel?: string        // 'sms' | 'call' | 'email' | 'whatsapp'
  codeLength?: number
  locale?: string
  customMessage?: string  // Twilio only
}

interface VerificationCheckRequest {
  to: string
  code: string
  verificationId?: string
}

interface VerificationResult {
  success: boolean
  verificationId?: string
  status: 'pending' | 'approved' | 'denied'
  error?: string
}
```

## Gotchas
- SMS is **disabled by default** (`enabled: false`) -- must set to `true` in `config/sms.ts`
- Provider API keys go in `.env`, not config files
- Config is loaded lazily via dynamic import on first `send()`/operation -- `init()` pre-loads it
- Only Twilio and Vonage drivers are fully implemented; other drivers (gupshup, plivo, sns, telnyx, etc.) are commented-out placeholders
- `pinpoint` appears in config but has no driver implementation in `getDriver()` -- using it throws `'Unsupported SMS provider: pinpoint'`
- `defaultCountryCode` is a numeric dialing prefix such as `'1'` for the US or `'44'` for the UK
- Template variables use single-brace syntax `{variableName}`, NOT double-brace
- `sendBulk()` sends all messages in parallel via `Promise.all` -- no rate limiting
- Twilio verification requires a `verifyServiceSid` passed to the driver constructor, which is not part of the standard config structure
- Vonage JWT authentication is a simplified placeholder -- real RS256 signing is not fully implemented
- `getDriver()` creates a new driver instance each time unless accessed through the cached `getDefaultDriver()`
- `sendTemplate()` returns a failed result (not an exception) if the template name is not found

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.