wraps-sms

wraps-sms

TypeScript SDK for AWS End User Messaging (Pinpoint SMS) with opt-out management, batch sending, and E.164 validation.

1Sterne
0Forks
Aktualisiert 1/20/2026
SKILL.md
readonlyread-only
name
wraps-sms
description

TypeScript SDK for AWS End User Messaging (Pinpoint SMS) with opt-out management, batch sending, and E.164 validation.

@wraps.dev/sms SDK

TypeScript SDK for AWS End User Messaging (Pinpoint SMS) with opt-out management, batch sending, and E.164 validation. Calls your AWS account directly — no proxy, no markup.

Installation

npm install @wraps.dev/sms
# or
pnpm add @wraps.dev/sms

Quick Start

import { WrapsSMS } from '@wraps.dev/sms';

const sms = new WrapsSMS();

const result = await sms.send({
  to: '+14155551234',
  message: 'Your verification code is 123456',
});

console.log('Sent:', result.messageId);

Client Configuration

Default (Auto-detect credentials)

// Uses AWS credential chain (env vars, IAM role, ~/.aws/credentials)
const sms = new WrapsSMS();

With Region

const sms = new WrapsSMS({
  region: 'us-west-2', // defaults to us-east-1
});

With Explicit Credentials

const sms = new WrapsSMS({
  region: 'us-east-1',
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

With IAM Role (OIDC / Cross-account)

// For Vercel, EKS, GitHub Actions with OIDC federation
const sms = new WrapsSMS({
  region: 'us-east-1',
  roleArn: 'arn:aws:iam::123456789012:role/WrapsSMSRole',
  roleSessionName: 'my-app-session', // optional
});

With Credential Provider (Advanced)

import { fromWebToken } from '@aws-sdk/credential-providers';

const credentials = fromWebToken({
  roleArn: process.env.AWS_ROLE_ARN!,
  webIdentityToken: async () => process.env.VERCEL_OIDC_TOKEN!,
});

const sms = new WrapsSMS({
  region: 'us-east-1',
  credentials,
});

Sending SMS

Basic Send

const result = await sms.send({
  to: '+14155551234', // E.164 format required
  message: 'Hello from Wraps!',
});

console.log('Message ID:', result.messageId);
console.log('Segments:', result.segments);
console.log('Status:', result.status);

Transactional vs Promotional

// OTP, alerts, notifications (higher priority, opt-out not required)
await sms.send({
  to: '+14155551234',
  message: 'Your code is 123456',
  messageType: 'TRANSACTIONAL', // default
});

// Marketing messages (requires opt-in, subject to quiet hours)
await sms.send({
  to: '+14155551234',
  message: 'Sale! 20% off today only!',
  messageType: 'PROMOTIONAL',
});

With Custom Sender

// Use a specific origination number from your account
await sms.send({
  to: '+14155551234',
  message: 'Hello!',
  from: '+18005551234', // Your registered number
});

With Tracking Context

await sms.send({
  to: '+14155551234',
  message: 'Your order has shipped!',
  context: {
    orderId: 'order_123',
    userId: 'user_456',
    type: 'shipping_notification',
  },
});

With Price Limit

// Fail if message would cost more than specified amount
await sms.send({
  to: '+14155551234',
  message: 'Hello!',
  maxPrice: '0.05', // USD per segment
});

With TTL (Time to Live)

// Message expires if not delivered within TTL
await sms.send({
  to: '+14155551234',
  message: 'Your OTP is 123456',
  ttl: 300, // 5 minutes in seconds
});

Dry Run (Validate without sending)

const result = await sms.send({
  to: '+14155551234',
  message: 'Test message',
  dryRun: true,
});

console.log('Would use', result.segments, 'segment(s)');
// No message is actually sent

Batch Sending

const result = await sms.sendBatch({
  messages: [
    { to: '+14155551234', message: 'Hello Alice!' },
    { to: '+14155555678', message: 'Hello Bob!' },
    { to: '+14155559012', message: 'Hello Carol!' },
  ],
  messageType: 'TRANSACTIONAL',
});

console.log(`Total: ${result.total}`);
console.log(`Queued: ${result.queued}`);
console.log(`Failed: ${result.failed}`);

// Check individual results
result.results.forEach((r) => {
  if (r.status === 'QUEUED') {
    console.log(`${r.to}: ${r.messageId}`);
  } else {
    console.log(`${r.to}: FAILED - ${r.error}`);
  }
});

Phone Number Management

List Your Numbers

const numbers = await sms.numbers.list();

numbers.forEach((n) => {
  console.log(`${n.phoneNumber} (${n.numberType})`);
  console.log(`  Message type: ${n.messageType}`);
  console.log(`  Two-way enabled: ${n.twoWayEnabled}`);
  console.log(`  Country: ${n.isoCountryCode}`);
});

Get Number Details

const number = await sms.numbers.get('phone-number-id-123');

if (number) {
  console.log(`Phone: ${number.phoneNumber}`);
  console.log(`Type: ${number.numberType}`);
}

Opt-Out Management

TCPA compliance requires honoring opt-out requests.

Check Opt-Out Status

const isOptedOut = await sms.optOuts.check('+14155551234');

if (isOptedOut) {
  console.log('User has opted out, do not send');
} else {
  await sms.send({
    to: '+14155551234',
    message: 'Hello!',
  });
}

List Opted-Out Numbers

const optOuts = await sms.optOuts.list();

optOuts.forEach((entry) => {
  console.log(`${entry.phoneNumber} opted out at ${entry.optedOutAt}`);
});

Manually Add to Opt-Out List

// User requested to stop receiving messages
await sms.optOuts.add('+14155551234');

Remove from Opt-Out List

// User re-subscribed (must have explicit consent)
await sms.optOuts.remove('+14155551234');

Error Handling

import { WrapsSMS, SMSError, ValidationError, OptedOutError } from '@wraps.dev/sms';

try {
  await sms.send({
    to: '+14155551234',
    message: 'Hello!',
  });
} catch (error) {
  if (error instanceof ValidationError) {
    // Invalid parameters (e.g., invalid phone number format)
    console.error('Validation error:', error.message);
  } else if (error instanceof OptedOutError) {
    // Recipient has opted out
    console.error('User opted out:', error.phoneNumber);
  } else if (error instanceof SMSError) {
    // AWS SMS service error
    console.error('SMS error:', error.message);
    console.error('Error code:', error.code);
    console.error('Request ID:', error.requestId);
    console.error('Is throttled:', error.isThrottled);
  } else {
    throw error;
  }
}

Utility Functions

Validate Phone Number

import { validatePhoneNumber } from '@wraps.dev/sms';

const isValid = validatePhoneNumber('+14155551234'); // true
const isInvalid = validatePhoneNumber('415-555-1234'); // false (not E.164)

Sanitize Phone Number

import { sanitizePhoneNumber } from '@wraps.dev/sms';

const clean = sanitizePhoneNumber('(415) 555-1234', 'US');
// Returns: '+14155551234'

Calculate Segments

import { calculateSegments } from '@wraps.dev/sms';

// GSM-7 encoding: 160 chars = 1 segment
const segments1 = calculateSegments('Hello world!'); // 1

// Unicode: 70 chars = 1 segment
const segments2 = calculateSegments('Hello! emoji here'); // may be more due to encoding

// Long message
const longMsg = 'A'.repeat(200);
const segments3 = calculateSegments(longMsg); // 2 (for GSM-7)

Cleanup

// When done (e.g., in serverless cleanup or app shutdown)
sms.destroy();

Type Exports

import type {
  WrapsSMSConfig,
  SendOptions,
  SendResult,
  BatchOptions,
  BatchResult,
  BatchMessage,
  BatchMessageResult,
  PhoneNumber,
  OptOutEntry,
  MessageType,
  MessageStatus,
} from '@wraps.dev/sms';

Common Patterns

OTP Service

import { WrapsSMS } from '@wraps.dev/sms';

class OTPService {
  private sms: WrapsSMS;

  constructor() {
    this.sms = new WrapsSMS({
      region: process.env.AWS_REGION,
    });
  }

  async sendOTP(phoneNumber: string, code: string) {
    return this.sms.send({
      to: phoneNumber,
      message: `Your verification code is ${code}. Valid for 10 minutes.`,
      messageType: 'TRANSACTIONAL',
      ttl: 600, // 10 minutes
      context: {
        type: 'otp',
      },
    });
  }
}

Notification Service with Opt-Out Check

import { WrapsSMS, OptedOutError } from '@wraps.dev/sms';

class NotificationService {
  private sms: WrapsSMS;

  constructor() {
    this.sms = new WrapsSMS();
  }

  async sendNotification(phoneNumber: string, message: string) {
    // Check opt-out status first
    const isOptedOut = await this.sms.optOuts.check(phoneNumber);

    if (isOptedOut) {
      console.log(`Skipping ${phoneNumber} - opted out`);
      return null;
    }

    try {
      return await this.sms.send({
        to: phoneNumber,
        message,
        messageType: 'TRANSACTIONAL',
      });
    } catch (error) {
      if (error instanceof OptedOutError) {
        // Race condition: user opted out between check and send
        console.log(`User ${phoneNumber} opted out`);
        return null;
      }
      throw error;
    }
  }
}

Vercel Edge/Serverless

import { WrapsSMS } from '@wraps.dev/sms';

// Initialize outside handler for connection reuse
const sms = new WrapsSMS({
  roleArn: process.env.AWS_ROLE_ARN,
});

export async function POST(request: Request) {
  const { to, message } = await request.json();

  const result = await sms.send({
    to,
    message,
    messageType: 'TRANSACTIONAL',
  });

  return Response.json({ messageId: result.messageId });
}

Phone Number Formats

Always use E.164 format:

  • US: +14155551234 (not 415-555-1234)
  • UK: +447911123456 (not 07911 123456)
  • Germany: +4915112345678

SMS Segments

Messages are billed per segment:

  • GSM-7 encoding (basic characters): 160 chars = 1 segment
  • Unicode (emojis, non-Latin chars): 70 chars = 1 segment
  • Long messages are split: 153 chars/segment (GSM-7) or 67 chars/segment (Unicode)

Requirements

  • Node.js 18+
  • AWS account with End User Messaging configured
  • Phone number (toll-free, 10DLC, or short code) provisioned in AWS

You Might Also Like

Related Skills

gog

gog

169Kdev-api

Google Workspace CLI for Gmail, Calendar, Drive, Contacts, Sheets, and Docs.

openclaw avataropenclaw
Holen
weather

weather

169Kdev-api

Get current weather and forecasts (no API key required).

openclaw avataropenclaw
Holen

Guide for implementing oRPC contract-first API patterns in Dify frontend. Triggers when creating new API contracts, adding service endpoints, integrating TanStack Query with typed contracts, or migrating legacy service calls to oRPC. Use for all API layer work in web/contract and web/service directories.

langgenius avatarlanggenius
Holen
blucli

blucli

92Kdev-api

BluOS CLI (blu) for discovery, playback, grouping, and volume.

moltbot avatarmoltbot
Holen
ordercli

ordercli

92Kdev-api

Foodora-only CLI for checking past orders and active order status (Deliveroo WIP).

moltbot avatarmoltbot
Holen
gifgrep

gifgrep

92Kdev-api

Search GIF providers with CLI/TUI, download results, and extract stills/sheets.

moltbot avatarmoltbot
Holen