Relay SMS Platform
Core Concepts

SMS Basics

Understanding SMS fundamentals is crucial for building reliable messaging applications. This guide covers the core concepts, limitations, and best practices for SMS development with Relay.

What is SMS?

SMS (Short Message Service) is a text messaging service that allows sending messages of up to 160 characters between mobile devices. Despite being developed in the 1980s, SMS remains one of the most reliable and universally supported communication channels.

Key Characteristics

  • Universal Support: Works on virtually all mobile devices
  • High Open Rates: 98% of SMS messages are read within 3 minutes
  • No Internet Required: Uses cellular network infrastructure
  • Reliable Delivery: Built-in delivery confirmation system
  • Cost Effective: Typically $0.0075 per message in the US

SMS vs Other Messaging

SMS vs MMS

FeatureSMSMMS
ContentText onlyText, images, audio, video
Size Limit160/70 charactersUp to 5MB
Cost~$0.0075~$0.02-0.05
DeliveryNearly 100%85-95% (device dependent)
Character SetGSM 7-bit or UnicodeUnicode

SMS vs App-Based Messaging

AspectSMSWhatsApp/iMessage
Setup RequiredNoneApp installation
Internet DependencyNoYes
Rich MediaLimited (MMS)Full support
Read ReceiptsDelivery reportsRead receipts
Group MessagingLimitedFull featured
Business FeaturesBasicAdvanced

Phone Number Formats

E.164 Format

E.164 is the international standard for phone numbers. All Relay APIs require E.164 format.

Format: +[country code][area code][local number]

JavascriptCode
// ✅ Correct E.164 format (US/Canada supported) "+15551234567" // US number "+14165551234" // Canada number // ❌ Incorrect formats "555-123-4567" // Missing country code and + prefix "(555) 123-4567" // Formatting characters "15551234567" // Missing + prefix "+1 555 123 4567" // Spaces

Validation

Always validate phone numbers before sending:

JavascriptCode
function isValidE164(phoneNumber) { // E.164 regex: + followed by 1-15 digits return /^\+[1-9]\d{1,14}$/.test(phoneNumber); } // Relay-specific validation for US/Canada function isValidRelayNumber(phoneNumber) { // Must be US or Canada (+1 country code) return /^\+1\d{10}$/.test(phoneNumber); } // Usage if (!isValidRelayNumber(phoneNumber)) { throw new Error('Phone number must be US/Canada format (e.g., +15551234567 or +14165551234)'); }

Supported Regions

Relay currently supports United States and Canada:

CountryCodeExampleStatus
United States+1+15551234567Available
Canada+1+14165551234Available

International Expansion

While E.164 is the international standard for phone numbers, Relay currently validates and delivers only to US and Canada numbers. International expansion is planned based on customer demand.

Character Limits and Encoding

Single SMS Limits

SMS character limits depend on the character encoding used:

GSM 7-bit Encoding (160 characters)

Used for basic Latin characters, numbers, and common symbols:

JavascriptCode
// Characters that use GSM 7-bit encoding const gsmCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; // This message uses 45 characters in GSM 7-bit const message = "Hello! Your order #12345 is ready for pickup.";

Unicode Encoding (70 characters)

Required for emojis, accented characters, and non-Latin scripts:

JavascriptCode
// These characters require Unicode encoding const unicodeMessage = "¡Hola! Tu pedido está listo. 🚚"; // 32 characters in Unicode const emojiMessage = "Your order is ready! 📦✅🎉"; // 28 characters in Unicode const accentedMessage = "Café, résumé, naïve"; // 19 characters in Unicode

Message Segmentation

Messages longer than the single SMS limit are automatically split into segments:

GSM 7-bit Segmentation

  • Single SMS: 160 characters
  • Multi-part SMS: 153 characters per segment (7 characters reserved for headers)

Unicode Segmentation

  • Single SMS: 70 characters
  • Multi-part SMS: 67 characters per segment (3 characters reserved for headers)
JavascriptCode
// Example of message segmentation const longMessage = ` Welcome to our service! Your account has been created successfully and you now have access to all premium features. If you have any questions, please contact our support team at support@example.com or call us at 1-800-555-0123. Thank you for choosing us and we look forward to serving you! `.trim(); // This message (~280 characters) would be split into: // - Segment 1: 153 characters // - Segment 2: 127 characters // Total cost: 2 SMS messages

Character Counting

JavascriptCode
function calculateSMSSegments(message) { // Check if message contains Unicode characters const isUnicode = /[^\x00-\x7F]/.test(message) || /[{}\\[\]~|€]/.test(message); if (isUnicode) { // Unicode encoding if (message.length <= 70) { return { segments: 1, encoding: 'Unicode', charsPerSegment: 70 }; } else { const segments = Math.ceil(message.length / 67); return { segments, encoding: 'Unicode', charsPerSegment: 67 }; } } else { // GSM 7-bit encoding if (message.length <= 160) { return { segments: 1, encoding: 'GSM 7-bit', charsPerSegment: 160 }; } else { const segments = Math.ceil(message.length / 153); return { segments, encoding: 'GSM 7-bit', charsPerSegment: 153 }; } } } // Usage const result = calculateSMSSegments("Hello! Your order is ready 📦"); console.log(`${result.segments} segments, ${result.encoding} encoding`); // Output: 1 segments, Unicode encoding

Message Delivery Lifecycle

Understanding the SMS delivery process helps with troubleshooting and setting user expectations.

Delivery States

Code
graph LR A[Message Sent] --> B[Queued] B --> C[Sent to Carrier] C --> D[Delivered] C --> E[Failed] D --> F[Delivery Confirmed] E --> G[Retry or Permanent Failure]

Status Definitions

StatusDescriptionWhat It Means
queuedMessage accepted by RelayReady to send
sentSent to mobile carrierIn transit
deliveredDelivered to deviceSuccessfully received
failedDelivery failedPermanent failure

Delivery Times

Typical SMS delivery times for US/Canada:

  • Standard delivery: 1-30 seconds
  • Peak hours/holidays: Up to 10 minutes
  • Network congestion: Up to 30 minutes

Delivery times shown are for US and Canada destinations only.

Delivery Failure Reasons

Common failure reasons and solutions:

JavascriptCode
// Handle delivery failures in webhooks function handleMessageFailed(messageData) { const { failure_reason, error_code } = messageData; switch (error_code) { case '30003': // Unreachable destination handset console.log('Phone may be turned off or out of service'); // Don't retry immediately break; case '30006': // Landline or unreachable carrier console.log('Number may be a landline or invalid'); // Mark as permanently failed markAsInvalid(messageData.to); break; case '30007': // Carrier violation or spam filtering console.log('Message blocked by carrier spam filter'); // Review message content reviewForSpam(messageData.message); break; case '30008': // Unknown error console.log('Unknown delivery error, may retry'); // Implement retry logic scheduleRetry(messageData); break; } }

Regional Considerations

United States & Canada (Currently Supported)

Relay is optimized for US and Canada messaging with the following compliance requirements:

United States:

  • 10DLC Registration: Required for business messaging (handled automatically for Starter tier)
  • Carrier Filtering: Major carriers apply spam filtering to promotional content
  • Opt-out Required: Must provide STOP/UNSUBSCRIBE options
  • Time Restrictions: Respect 8 AM - 9 PM recipient local time
  • TCPA Compliance: Prior express consent required for marketing messages

Canada:

  • CASL Compliance: Canadian Anti-Spam Legislation applies
  • Opt-out Required: Clear unsubscribe mechanism required
  • Consent Requirements: Express or implied consent needed
  • Time Restrictions: Similar to US, respect local hours
JavascriptCode
// US/Canada-compliant message format const compliantMessage = ` Hi John! Your appointment is confirmed for tomorrow at 2 PM. Reply STOP to opt out. Questions? Call 555-123-4567. `.trim();

International Expansion

Relay is currently focused on US and Canada markets. International expansion is planned based on customer demand, with the same compliance and quality standards applied to each new region.

When sending to unsupported regions, the API will return an error indicating the destination is not yet supported.

Message Types and Use Cases

Transactional Messages

High-priority, individual messages triggered by user actions:

JavascriptCode
// Examples of transactional messages const transactionalExamples = { verification: "Your verification code is: 123456. Valid for 10 minutes.", orderConfirmation: "Order #12345 confirmed! Est. delivery: Jan 15. Track: link.co/abc123", appointment: "Reminder: Your appointment with Dr. Smith is tomorrow at 2 PM.", alert: "Security alert: New login detected from New York. If this wasn't you, secure your account.", receipt: "Payment of $29.99 processed successfully. Receipt: receipt.co/xyz789" };

Promotional Messages

Marketing and promotional content:

JavascriptCode
// Promotional message best practices const promotionalMessage = ` 🎉 FLASH SALE: 50% off everything! Use code FLASH50. Shop now: shop.example.com Reply STOP to opt out. `.trim(); // Requirements for promotional SMS: // 1. Clear opt-out instructions // 2. Brand identification // 3. Respect time zones // 4. Honor opt-out requests immediately

Two-Way Conversations

Interactive messaging with customer responses:

JavascriptCode
// Conversation flow example const conversationFlow = { initial: "Hi! I'm your virtual assistant. How can I help? Reply 1 for hours, 2 for locations.", responses: { '1': "We're open Mon-Fri 9 AM-6 PM, Sat 10 AM-4 PM. Anything else?", '2': "We have locations in downtown (123 Main St) and mall (456 Oak Ave). Need directions?", 'hours': "We're open Mon-Fri 9 AM-6 PM, Sat 10 AM-4 PM. Anything else?", 'stop': "You've been unsubscribed. Reply START to opt back in." } };

Opt-Out Management

SMS opt-out is legally required in most jurisdictions:

JavascriptCode
// Standard opt-out keywords (case-insensitive) const optOutKeywords = [ 'STOP', 'STOPALL', 'UNSUBSCRIBE', 'CANCEL', 'END', 'QUIT' ]; // Standard opt-in keywords const optInKeywords = [ 'START', 'YES', 'UNSTOP' ]; function handleOptOut(fromNumber, message) { const normalizedMessage = message.trim().toUpperCase(); if (optOutKeywords.includes(normalizedMessage)) { // Add to opt-out list immediately addToOptOutList(fromNumber); // Send confirmation (required) sendSMS(fromNumber, "You have been unsubscribed. Reply START to opt back in."); return true; // Message handled } if (optInKeywords.includes(normalizedMessage)) { // Remove from opt-out list removeFromOptOutList(fromNumber); // Send welcome back message sendSMS(fromNumber, "Welcome back! You're now subscribed to our updates."); return true; // Message handled } return false; // Not an opt-out/in message }

Compliance Best Practices

JavascriptCode
// Check opt-out status before sending async function sendCompliantSMS(to, message) { // 1. Check opt-out list if (await isOptedOut(to)) { throw new Error('Recipient has opted out'); } // 2. Check time restrictions (8 AM - 9 PM local time) if (!isValidSendTime(to)) { throw new Error('Outside allowed sending hours'); } // 3. Add opt-out instructions for promotional messages if (isPromotionalMessage(message)) { message += "\n\nReply STOP to opt out."; } // 4. Send message return await sendSMS(to, message); }

Cost Optimization

Message Optimization

JavascriptCode
// Optimize message length to avoid extra segments function optimizeMessage(message) { const segmentInfo = calculateSMSSegments(message); if (segmentInfo.segments > 1) { console.warn(`Message will use ${segmentInfo.segments} segments ($${segmentInfo.segments * 0.0075})`); // Suggest optimization const maxLength = segmentInfo.encoding === 'Unicode' ? 70 : 160; if (message.length > maxLength) { console.log(`Consider shortening to ${maxLength} characters to use 1 segment`); } } return segmentInfo; } // URL shortening to save characters function shortenUrls(message) { const urlRegex = /(https?:\/\/[^\s]+)/g; return message.replace(urlRegex, (url) => { // Use a URL shortener service return shortenUrl(url); // Returns something like "bit.ly/abc123" }); }

Batching and Timing

JavascriptCode
// Batch messages to avoid rate limits async function sendBatchSMS(recipients, message) { const batchSize = 10; // Respect rate limits const results = []; for (let i = 0; i < recipients.length; i += batchSize) { const batch = recipients.slice(i, i + batchSize); // Send batch in parallel const batchPromises = batch.map(async (recipient) => { try { return await sendCompliantSMS(recipient, message); } catch (error) { return { error: error.message, recipient }; } }); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); // Rate limiting delay between batches if (i + batchSize < recipients.length) { await new Promise(resolve => setTimeout(resolve, 1000)); } } return results; }

Testing Best Practices

Development Testing

JavascriptCode
// Environment-based configuration const config = { development: { apiUrl: 'http://localhost:4001/v1', // SMS-dev skipOptOutCheck: true, // For testing logAllMessages: true }, production: { apiUrl: 'https://api.relay.works/v1', skipOptOutCheck: false, logAllMessages: false } }; // Test message validation function testMessageValidation() { const testCases = [ { phone: '+15551234567', message: 'Test', shouldPass: true }, { phone: '555-123-4567', message: 'Test', shouldPass: false }, // Invalid format { phone: '+15551234567', message: 'A'.repeat(1700), shouldPass: false }, // Too long ]; testCases.forEach(testCase => { try { validateMessage(testCase.phone, testCase.message); console.log(`✅ Test passed: ${testCase.phone}`); } catch (error) { if (!testCase.shouldPass) { console.log(`✅ Test passed (expected failure): ${error.message}`); } else { console.log(`❌ Test failed: ${error.message}`); } } }); }

Load Testing

JavascriptCode
// Simulate high-volume messaging async function loadTestSMS(concurrentUsers = 5, messagesPerUser = 10) { const users = Array.from({ length: concurrentUsers }, (_, i) => `+155512345${i.toString().padStart(2, '0')}`); const startTime = Date.now(); const userPromises = users.map(async (user, index) => { const results = []; for (let i = 0; i < messagesPerUser; i++) { try { const result = await sendSMS(user, `Load test message ${i + 1} from user ${index + 1}`); results.push({ success: true, messageId: result.id }); } catch (error) { results.push({ success: false, error: error.message }); } // Small delay between messages from same user await new Promise(resolve => setTimeout(resolve, 100)); } return results; }); const allResults = await Promise.all(userPromises); const endTime = Date.now(); // Calculate statistics const totalMessages = allResults.flat().length; const successfulMessages = allResults.flat().filter(r => r.success).length; const duration = (endTime - startTime) / 1000; console.log(`Load Test Results:`); console.log(`Total messages: ${totalMessages}`); console.log(`Successful: ${successfulMessages} (${Math.round(successfulMessages/totalMessages*100)}%)`); console.log(`Duration: ${duration}s`); console.log(`Rate: ${Math.round(totalMessages/duration)} messages/second`); }

Common Pitfalls and Solutions

1. Phone Number Validation Issues

JavascriptCode
// ❌ Common mistakes const badFormats = [ "555-123-4567", // No country code "+1 555 123 4567", // Spaces "15551234567", // No + prefix "+1(555)123-4567" // Parentheses and dashes ]; // ✅ Correct format const goodFormat = "+15551234567"; // Helper function to clean phone numbers function cleanPhoneNumber(phone) { // Remove all non-digit characters except + let cleaned = phone.replace(/[^\d+]/g, ''); // Add + if missing and starts with country code if (!cleaned.startsWith('+') && cleaned.length >= 10) { // Assume US/Canada if 10 digits if (cleaned.length === 10) { cleaned = '+1' + cleaned; } else { cleaned = '+' + cleaned; } } return cleaned; }

2. Character Encoding Issues

JavascriptCode
// ❌ Unexpected Unicode characters const problematicMessage = "Your "order" is ready!"; // Smart quotes const emojiMessage = "Order ready 📦"; // Emoji forces Unicode // ✅ Check encoding before sending function checkEncoding(message) { const hasUnicode = /[^\x00-\x7F]/.test(message); const segmentInfo = calculateSMSSegments(message); if (hasUnicode && segmentInfo.segments === 1) { console.warn(`Message uses Unicode encoding (70 char limit instead of 160)`); } return segmentInfo; }

3. Rate Limiting

JavascriptCode
// ❌ Sending too fast async function badBatchSend(recipients, message) { // This will hit rate limits quickly return Promise.all(recipients.map(r => sendSMS(r, message))); } // ✅ Proper rate limiting async function goodBatchSend(recipients, message) { const results = []; const batchSize = 10; for (let i = 0; i < recipients.length; i += batchSize) { const batch = recipients.slice(i, i + batchSize); const batchResults = await Promise.all( batch.map(r => sendSMS(r, message).catch(err => ({ error: err.message, recipient: r }))) ); results.push(...batchResults); // Wait between batches if (i + batchSize < recipients.length) { await new Promise(resolve => setTimeout(resolve, 1000)); } } return results; }

Performance Considerations

Message Queuing

For high-volume applications, implement message queuing:

JavascriptCode
// Using a job queue for SMS sending const Queue = require('bull'); const smsQueue = new Queue('SMS sending'); // Add messages to queue async function queueSMS(to, message, options = {}) { await smsQueue.add('send-sms', { to, message, ...options }, { delay: options.delay || 0, attempts: 3, backoff: 'exponential' }); } // Process queued messages smsQueue.process('send-sms', async (job) => { const { to, message } = job.data; try { const result = await sendSMS(to, message); return result; } catch (error) { // Let the queue handle retries throw error; } });

Caching and Optimization

JavascriptCode
// Cache frequently used data const phoneValidationCache = new Map(); const optOutCache = new Map(); async function isOptedOutCached(phoneNumber) { if (optOutCache.has(phoneNumber)) { const cached = optOutCache.get(phoneNumber); if (Date.now() - cached.timestamp < 300000) { // 5 minute cache return cached.isOptedOut; } } const isOptedOut = await checkOptOutDatabase(phoneNumber); optOutCache.set(phoneNumber, { isOptedOut, timestamp: Date.now() }); return isOptedOut; }

Next Steps:

Last modified on