Guide · Amazon Pinpoint · SMS · Origination Numbers · Opt-Out · Two-Way SMS

Pinpoint SMS for MCP Servers — Origination Numbers, Opt-Out, and Two-Way Messaging

SMS is the most reliably-noticed notification channel for critical alerts — a "your MCP server has been down for 15 minutes" message delivered to a user's phone gets read in a way that email often doesn't. Amazon Pinpoint provides SMS transactional messaging via its SendMessagesCommand API, but SMS in the US requires a registered origination number, and carrier opt-out rules (the STOP keyword) are enforced automatically by the network. This guide covers origination number types, sending transactional SMS, opt-out management, two-way messaging for confirmation flows, and the character encoding rules that determine whether a 160-character limit applies.

TL;DR

To send SMS via Pinpoint: (1) request an origination phone number in the Pinpoint console (US requires 10DLC long code or toll-free number — no named sender IDs), (2) enable the SMS channel with UpdateSmsChannelCommand, (3) register recipient phone numbers as SMS endpoints in E.164 format, (4) send with SendMessagesCommand using MessageConfiguration.SMSMessage and MessageType: 'TRANSACTIONAL'. Keep messages under 160 GSM-7 characters to stay single-segment; avoid emoji and non-ASCII characters if length is a concern. Pinpoint automatically handles STOP replies — your endpoint's OptOut will be set to ALL; always check this before sending.

Origination number types

The origination number is the sender ID that appears on the recipient's phone. The US has specific carrier rules that make SMS setup more involved than in most other countries. Outside the US, many countries support alphabetic sender IDs (like "AliveMCP") that don't require phone number provisioning.

Number type Format US requirements Throughput Provisioning time
10DLC long code Standard 10-digit US number (e.g., +12025551234) Register brand + campaign with The Campaign Registry (TCR) via Pinpoint console; $4-$10/month per number + registration fees ~75-300 messages/sec after registration; unregistered sends are throttled to 1 msg/sec 1-7 business days for TCR approval
Toll-free number +1800, +1888, +1877, etc. Toll-free verification recommended (not required for low volume); unverified limited to 500 messages/day 3 messages/sec unverified; up to 100 messages/sec verified Immediate (number available instantly); verification takes 2-4 weeks
Short code 5-6 digit number (e.g., 12345) Carrier application process through Pinpoint; dedicated to your use case Highest — up to 100-500 messages/sec 8-12 weeks; significant upfront cost ($500-$1000/month)
Sender ID (non-US only) Alphanumeric string up to 11 chars (e.g., AliveMCP) Not supported in the US; works in AU, UK, EU, SG, and many others Varies by country; typically 10-100 messages/sec Immediate; some countries require pre-registration

For MCP server tools targeting US users, start with a toll-free number during development — it's immediately available, verification is optional for low volume, and there's no TCR registration process. Switch to a 10DLC long code if you need a local-looking number or higher throughput after launch.

Origination numbers are requested in the Pinpoint console under "SMS and voice > Phone numbers" or via the Pinpoint API's RequestPhoneNumberCommand. Once provisioned, the number must be explicitly associated with your Pinpoint application to be used as the outbound sender.

Enabling the SMS channel and sending a transactional message

import {
  PinpointClient,
  UpdateSmsChannelCommand,
  SendMessagesCommand,
} from '@aws-sdk/client-pinpoint';

const client = new PinpointClient({ region: 'us-east-1' });
const APP_ID = process.env.PINPOINT_APP_ID!;

// Enable SMS channel (run during setup, not on each tool call)
await client.send(new UpdateSmsChannelCommand({
  ApplicationId: APP_ID,
  SMSChannelRequest: {
    Enabled: true,
    // For countries that support sender IDs (not US):
    // SenderId: 'AliveMCP',
    // ShortCode is set by AWS after provisioning; do not set manually
  },
}));

// Send a transactional SMS alert
async function sendSmsAlert(
  endpointId: string,
  messageBody: string,
  originationNumber: string  // the toll-free or 10DLC number you provisioned, E.164 format
): Promise {
  // Validate message length before sending (see character encoding section)
  if (messageBody.length > 160) {
    console.warn(`SMS body is ${messageBody.length} chars — will be split into multiple segments`);
  }

  const { MessageResponse } = await client.send(new SendMessagesCommand({
    ApplicationId: APP_ID,
    MessageRequest: {
      Endpoints: { [endpointId]: {} },
      MessageConfiguration: {
        SMSMessage: {
          Body: messageBody,
          // TRANSACTIONAL: time-sensitive messages the user has requested (alerts, OTPs)
          // PROMOTIONAL: marketing messages — stricter carrier filtering, can't use short codes
          MessageType: 'TRANSACTIONAL',
          // OriginationNumber overrides the application's default SMS origination number
          // Required if you have multiple numbers associated with the application
          OriginationNumber: originationNumber,
          // SenderId is ignored in the US; used in countries where it's supported
          // SenderId: 'AliveMCP',
          // EntityId and TemplateId for TRAI-registered templates (India only)
        },
      },
    },
  }));

  const result = MessageResponse?.Result?.[endpointId];
  if (result?.DeliveryStatus !== 'SUCCESSFUL') {
    throw new Error(
      `SMS delivery failed: ${result?.DeliveryStatus} — ${result?.StatusMessage}`
    );
  }
}

TRANSACTIONAL vs PROMOTIONAL: Always use MessageType: 'TRANSACTIONAL' for alert tools — it gets higher priority at carrier level and is less likely to be filtered as spam. PROMOTIONAL is for marketing sends (newsletters, announcements) and subject to stricter carrier filtering. Using PROMOTIONAL for time-sensitive alerts is a common mistake that leads to delayed or undelivered messages.

Character encoding and message segmentation

SMS carriers break long messages into 160-character segments at the network level. Each segment is billed separately by Pinpoint. The character limit depends on the encoding:

Encoding Triggered by Single-part limit Multi-part segment limit
GSM-7 Standard Latin characters, digits, common punctuation — the default for English text 160 characters 153 characters per segment (7 chars used for header)
UCS-2 Any character outside the GSM-7 character set: emoji (🚨), accented chars beyond basic Latin, CJK characters, arrows (→), curly quotes 70 characters 67 characters per segment

A single emoji in an otherwise short message forces UCS-2 encoding and cuts the single-part limit from 160 to 70 characters. This is the most common reason a "short" alert message gets split into multiple chargeable segments.

// Check if a string requires UCS-2 encoding (and therefore a 70-char limit)
function requiresUcs2(text: string): boolean {
  // GSM-7 basic character set + extended characters
  const gsm7Basic = /^[\x00-\x7F£¥èéùìòÇØøÅåΔΦΓΛΩΠΨΣΘΞÆæßɤ¡ÄÖÜäöüà§¿€]*$/;
  return !gsm7Basic.test(text);
}

function countSmsSegments(body: string): number {
  if (requiresUcs2(body)) {
    return body.length <= 70 ? 1 : Math.ceil(body.length / 67);
  }
  return body.length <= 160 ? 1 : Math.ceil(body.length / 153);
}

// Use in your MCP tool to warn before sending multi-segment messages
function buildAlertBody(serverName: string, errorCode: string): string {
  // Keep under 160 GSM-7 chars; avoid emoji for single-segment guarantee
  const body = `AliveMCP: ${serverName} is DOWN (${errorCode}). Check https://alivemcp.com/status`;
  const segments = countSmsSegments(body);
  if (segments > 1) console.warn(`Alert SMS will be ${segments} segments (${body.length} chars)`);
  return body;
}

Opt-out management and STOP handling

US carriers require that all SMS senders honor STOP opt-outs. When a recipient replies "STOP" to a message from your origination number, the carrier routes that reply to Pinpoint, which automatically sets the corresponding endpoint's OptOut to ALL and adds the phone number to the application's opt-out list. Any subsequent SendMessagesCommand targeting that endpoint will return DeliveryStatus: OPT_OUT — Pinpoint will not deliver the message, and this is correct behavior.

import { GetEndpointCommand, UpdateEndpointCommand } from '@aws-sdk/client-pinpoint';

// Check opt-out status before sending
async function isSmsEndpointOptedIn(endpointId: string): Promise {
  try {
    const { EndpointResponse } = await client.send(new GetEndpointCommand({
      ApplicationId: APP_ID,
      EndpointId: endpointId,
    }));
    return EndpointResponse?.OptOut === 'NONE';
  } catch (err: any) {
    if (err.name === 'NotFoundException') return false;
    throw err;
  }
}

// Re-opt-in a user after they reply START (only valid after user-initiated re-consent)
// IMPORTANT: only call this if the user has explicitly requested to re-subscribe
// Never programmatically re-opt-in without user action — this violates carrier rules
async function reOptInSmsEndpoint(endpointId: string): Promise {
  await client.send(new UpdateEndpointCommand({
    ApplicationId: APP_ID,
    EndpointId: endpointId,
    EndpointRequest: {
      ChannelType: 'SMS',
      OptOut: 'NONE',  // reset to opted-in
    },
  }));
}

// In your MCP tool
async function sendAlertWithOptOutCheck(
  endpointId: string,
  body: string,
  originationNumber: string
): Promise<{ sent: boolean; reason?: string }> {
  const optedIn = await isSmsEndpointOptedIn(endpointId);
  if (!optedIn) {
    return { sent: false, reason: 'opted_out' };
  }
  await sendSmsAlert(endpointId, body, originationNumber);
  return { sent: true };
}

HELP keyword: US carriers also require that recipients can reply HELP to get contact information about the sender. Pinpoint has a default HELP auto-reply ("Reply STOP to unsubscribe"). You can customize this in the Pinpoint console under "SMS and voice > Phone numbers" for each origination number.

Application-level opt-out list: Pinpoint maintains an opt-out list per application. If a user opts out of one campaign, they are opted out of all messages from that application to that phone number. To create separate opt-out scopes (e.g., "critical alerts" vs "weekly newsletter"), use separate Pinpoint applications or manage opt-out state in your own database and check it before calling SendMessagesCommand.

Two-way SMS for confirmation flows

Two-way SMS lets your MCP server receive replies from users — useful for confirmation flows ("Reply YES to acknowledge this alert"), OTP verification, or simple commands. Pinpoint routes inbound messages to an SNS topic that you configure on the origination number.

// Two-way SMS setup requires:
// 1. A two-way-capable origination number (long code or short code; not all toll-free)
// 2. An SNS topic for inbound message delivery
// 3. A Lambda (or other consumer) subscribed to that SNS topic

// The inbound message payload from Pinpoint via SNS:
interface PinpointSmsInbound {
  originationNumber: string;    // your Pinpoint origination number
  destinationNumber: string;    // the user's phone number (E.164)
  messageBody: string;          // the user's reply text, trimmed
  inboundMessageId: string;     // unique ID for this inbound message
  previousPublishedMessageId: string; // ID of the outbound message this replies to (may be null)
}

// Lambda handler for inbound SMS replies
export async function handleSmsReply(event: { Records: Array<{ Sns: { Message: string } }> }) {
  for (const record of event.Records) {
    const payload: PinpointSmsInbound = JSON.parse(record.Sns.Message);

    const reply = payload.messageBody.trim().toUpperCase();

    switch (reply) {
      case 'YES':
      case 'CONFIRM':
        // User acknowledged the alert
        await markAlertAcknowledged(payload.destinationNumber);
        // Optionally send a confirmation reply
        await sendSmsToNumber(payload.destinationNumber, 'Alert acknowledged. You will not receive further notifications for this incident.');
        break;

      case 'STOP':
        // Pinpoint handles opt-out automatically; this case may not reach your Lambda
        // but handle defensively in case the SNS delivery precedes Pinpoint's opt-out update
        console.log(`Opt-out received from ${payload.destinationNumber}`);
        break;

      default:
        // Unknown reply — send help text
        await sendSmsToNumber(payload.destinationNumber, 'Reply YES to acknowledge alerts, or STOP to unsubscribe.');
    }
  }
}

async function sendSmsToNumber(toNumber: string, body: string): Promise {
  // For outbound replies in a two-way flow, you can target by phone number directly
  // using the Addresses map (instead of the Endpoints map) in SendMessagesCommand
  await client.send(new SendMessagesCommand({
    ApplicationId: APP_ID,
    MessageRequest: {
      Addresses: {
        [toNumber]: { ChannelType: 'SMS' },
      },
      MessageConfiguration: {
        SMSMessage: {
          Body: body,
          MessageType: 'TRANSACTIONAL',
          OriginationNumber: process.env.PINPOINT_ORIGINATION_NUMBER!,
        },
      },
    },
  }));
}

Note the Addresses map in the last code block: unlike the Endpoints map (which requires a pre-registered endpoint ID), the Addresses map allows sending directly to a phone number or email address without prior endpoint registration. This is useful for reply flows where you have the user's phone number from the inbound message payload but do not have their endpoint ID handy. The tradeoff is that Pinpoint cannot track engagement events (open/click) for address-targeted sends.

OTP verification with SendOTPMessage

Pinpoint has a dedicated OTP API that handles code generation, delivery, and validation in one flow. This avoids the need to store OTP codes in your database.

import {
  SendOTPMessageCommand,
  VerifyOTPMessageCommand,
} from '@aws-sdk/client-pinpoint';

const OTP_BRAND_NAME = 'AliveMCP';
const OTP_EXPIRY_MINUTES = 10;

// Step 1: generate and send OTP to user's phone
async function sendOtp(endpointId: string, referenceId: string): Promise {
  await client.send(new SendOTPMessageCommand({
    ApplicationId: APP_ID,
    SendOTPMessageRequestParameters: {
      Channel: 'SMS',
      DestinationIdentity: endpointId,  // endpoint ID of the SMS endpoint
      BrandName: OTP_BRAND_NAME,
      CodeLength: 6,
      ValidityPeriod: OTP_EXPIRY_MINUTES,
      AllowedAttempts: 3,
      // referenceId ties this OTP to a specific user action; pass same ID to VerifyOTP
      EntityId: referenceId,
      TemplateId: referenceId,  // used as a unique identifier for this OTP session
      Language: 'en-US',
    },
  }));
  // Pinpoint sends a message like: "Your AliveMCP code is 847291. Valid for 10 minutes."
}

// Step 2: verify the code the user entered
async function verifyOtp(
  endpointId: string,
  referenceId: string,
  userEnteredCode: string
): Promise {
  const { VerificationResponse } = await client.send(new VerifyOTPMessageCommand({
    ApplicationId: APP_ID,
    VerifyOTPMessageRequestParameters: {
      DestinationIdentity: endpointId,
      Otp: userEnteredCode,
      ReferenceId: referenceId,
    },
  }));
  return VerificationResponse?.Valid === true;
}

Common errors and their causes

Error / symptom Cause Fix
DeliveryStatus: PERMANENT_FAILURE, StatusMessage: "No origination number" SMS channel is enabled but no phone number has been provisioned and assigned to the application Request a phone number in the Pinpoint console under "SMS and voice > Phone numbers" and associate it with the application; specify it via OriginationNumber in SMSMessage
Messages sent but never received by US users despite SUCCESSFUL status Using an unregistered 10DLC long code — carriers throttle unregistered senders to ~1 msg/sec and may silently drop messages above that rate Register your brand and campaign via The Campaign Registry (TCR) in the Pinpoint console; or switch to a toll-free number while registration processes
InvalidParameterException: Phone number is not in E.164 format Phone number address contains spaces, dashes, parentheses, or is missing the leading + Normalize all phone numbers to E.164 on input: strip non-digits, prepend country code, add + prefix; validate with a library like libphonenumber-js before storing
Message sent as 2 segments despite body being under 160 characters Body contains a character outside the GSM-7 character set (curly quotes, em-dash, arrow characters, any emoji) which forces UCS-2 encoding with a 70-character single-part limit Use only standard ASCII for alert bodies; avoid emoji, curly quotes (use straight quotes), and typographic symbols; test with the segment counter utility above
Inbound two-way messages not arriving at SNS topic Two-way messaging was not enabled on the specific origination number, or the SNS topic ARN was not configured on that number In the Pinpoint console, select the phone number, enable "Two-way SMS", and enter the ARN of the SNS topic; also ensure the SNS topic allows Pinpoint to publish to it via its resource-based policy
BadRequestException: SMS channel is not enabled UpdateSmsChannelCommand was not called, or the SMS channel is disabled on the application Call GetSmsChannelCommand to inspect status; run UpdateSmsChannelCommand with Enabled: true