wraps-sms

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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "wraps-sms" with this command: npx skills add wraps-team/skills/wraps-team-skills-wraps-sms

@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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

wraps-cli

No summary provided by upstream source.

Repository SourceNeeds Review
General

aws-ses-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
General

wraps-email

No summary provided by upstream source.

Repository SourceNeeds Review
General

ses-troubleshoot

No summary provided by upstream source.

Repository SourceNeeds Review
wraps-sms | V50.AI