Taiwan SMS Adapters
This skill provides comprehensive guidance for using @rytass/sms-adapter-* packages to integrate Taiwan SMS service providers.
Overview
All adapters implement the SMSService interface from @rytass/sms , providing a unified API across different providers:
Package Provider Description
@rytass/sms-adapter-every8d
Every8D (每日簡訊 / 互動資通) Taiwan SMS gateway by Interactive Communications
Base Interface (@rytass/sms)
All adapters share these core concepts:
SMSService - Main interface for SMS operations:
interface SMSService< Request extends SMSRequest, SendResponse extends SMSSendResponse, MultiTarget extends MultiTargetRequest,
{ send(request: Request[]): Promise<SendResponse[]>; send(request: Request): Promise<SendResponse>; send(request: MultiTarget): Promise<SendResponse[]>; }
Request Types:
-
SMSRequest
-
Single SMS message to one recipient
-
MultiTargetRequest
-
Same message to multiple recipients
Response Status:
-
SUCCESS
-
Message sent successfully
-
FAILED
-
Message delivery failed
Taiwan Phone Number Helpers (from @rytass/sms ):
// 台灣手機號碼正規表達式 const TAIWAN_PHONE_NUMBER_RE = /^(0|+?886-?)9\d{2}-?\d{3}-?\d{3}$/;
// 正規化台灣手機號碼(移除非數字字元,將 886 前綴轉為 0) function normalizedTaiwanMobilePhoneNumber(mobile: string): string; // '0987-654-321' → '0987654321' // '+886987654321' → '0987654321' // '886987654321' → '0987654321'
Installation
Install base package
npm install @rytass/sms
Choose the adapter for your provider
npm install @rytass/sms-adapter-every8d
Quick Start
Every8D (每日簡訊 / 互動資通)
import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d';
// Initialize SMS service const smsService = new SMSServiceEvery8D({ username: process.env.EVERY8D_USERNAME!, password: process.env.EVERY8D_PASSWORD!, baseUrl: 'https://api.e8d.tw', // Default API endpoint onlyTaiwanMobileNumber: true, // Restrict to Taiwan numbers only });
// Send single SMS const result = await smsService.send({ mobile: '0987654321', text: 'Hello! This is a test message.', });
console.log('Message ID:', result.messageId); console.log('Status:', result.status); console.log('Mobile:', result.mobile);
Common Usage Patterns
Single SMS Delivery
Send a message to a single recipient:
// Basic single SMS const result = await smsService.send({ mobile: '0987654321', text: 'Your verification code is 123456', });
// Check delivery status if (result.status === SMSRequestResult.SUCCESS) { console.log('SMS sent successfully!'); console.log('Message ID:', result.messageId); console.log('Recipient:', result.mobile); } else { console.error('SMS delivery failed'); console.error('Error Code:', result.errorCode); console.error('Error Message:', result.errorMessage); }
Batch Messaging (Same Message to Multiple Recipients)
Send the same message to multiple recipients efficiently:
// Batch send with same message const results = await smsService.send({ mobileList: [ '0987654321', '0912345678', '0923456789', '0934567890' ], text: 'Important notification: Your order has been shipped!', });
// Process results
console.log(Total sent: ${results.length});
const successful = results.filter(r => r.status === SMSRequestResult.SUCCESS); const failed = results.filter(r => r.status === SMSRequestResult.FAILED);
console.log(Successful: ${successful.length});
console.log(Failed: ${failed.length});
// Log failed deliveries
failed.forEach(result => {
console.error(Failed to send to ${result.mobile}:, result.errorMessage);
});
Multi-Target Messaging (Different Messages)
Send different messages to different recipients:
// Send different messages to each recipient const results = await smsService.send([ { mobile: '0987654321', text: 'Dear John, your appointment is confirmed for tomorrow at 10 AM.', }, { mobile: '0912345678', text: 'Hi Mary, your package will arrive today between 2-4 PM.', }, { mobile: '0923456789', text: 'Hello Mike, thank you for your purchase! Your receipt is attached.', }, ]);
// Check each result
results.forEach((result, index) => {
console.log(Message ${index + 1} to ${result.mobile}: ${result.status});
if (result.status === SMSRequestResult.SUCCESS) {
console.log( Message ID: ${result.messageId});
} else {
console.error( Error: ${result.errorMessage});
}
});
Taiwan Mobile Number Handling
Automatic Number Normalization
The adapter automatically normalizes Taiwan mobile numbers:
// All these formats are normalized to 0987654321 const validFormats = [ '0987654321', // Standard format '0987-654-321', // With dashes '+886987654321', // International format '886987654321', // International without + '+886-987654321', // International with dash after country code ];
// All will be normalized and send successfully for (const number of validFormats) { const result = await smsService.send({ mobile: number, text: 'Test message', });
// Result.mobile will always be '0987654321' console.log('Normalized number:', result.mobile); }
Number Validation
Validate Taiwan mobile numbers with strict mode:
// Enable Taiwan-only validation const strictService = new SMSServiceEvery8D({ username: process.env.EVERY8D_USERNAME!, password: process.env.EVERY8D_PASSWORD!, onlyTaiwanMobileNumber: true, // Enable strict validation });
try { // This will work - valid Taiwan mobile number await strictService.send({ mobile: '0987654321', // Taiwan number (09xxxxxxxx) text: 'Valid Taiwan number', });
// This will throw error - international number
await strictService.send({
mobile: '+1234567890', // Non-Taiwan number
text: 'Invalid for Taiwan-only mode',
});
} catch (error) {
console.error('Number validation error:', error.message);
// Error: +1234567890 is not taiwan mobile phone (onlyTaiwanMobileNumber option is true)
}
// Taiwan number format requirements: // - Must start with 09 (or +8869, 8869) // - Total 10 digits after country code // - Examples: 0912345678, 0923456789, 0987654321
International Numbers
Send to international numbers when validation is disabled:
// Allow international numbers const globalService = new SMSServiceEvery8D({ username: process.env.EVERY8D_USERNAME!, password: process.env.EVERY8D_PASSWORD!, onlyTaiwanMobileNumber: false, // Default: allow all numbers });
// Send to various countries const results = await globalService.send([ { mobile: '0987654321', text: 'Taiwan message' }, // Taiwan { mobile: '+85298765432', text: 'Hong Kong message' }, // Hong Kong { mobile: '+60123456789', text: 'Malaysia message' }, // Malaysia { mobile: '+6591234567', text: 'Singapore message' }, // Singapore ]);
// Note: Check with Every8D for international coverage and rates
Integration Examples
E-commerce Order Notifications
import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d'; import { SMSRequestResult } from '@rytass/sms';
class OrderNotificationService { private smsService: SMSServiceEvery8D;
constructor() { this.smsService = new SMSServiceEvery8D({ username: process.env.EVERY8D_USERNAME!, password: process.env.EVERY8D_PASSWORD!, onlyTaiwanMobileNumber: true, }); }
/**
- Send order confirmation SMS
- @param order - Order object with customer details
- @returns SMS delivery result
*/
async sendOrderConfirmation(order: {
id: string;
customerPhone: string;
total: number;
deliveryDate: string;
trackingUrl: string;
}): Promise<boolean> {
const message =
訂單已確認! 訂單編號:${order.id} 金額:NT$${order.total} 預計送達:${order.deliveryDate} 追蹤連結:${order.trackingUrl}.trim();
const result = await this.smsService.send({
mobile: order.customerPhone,
text: message,
});
return result.status === SMSRequestResult.SUCCESS;
}
/**
- Send shipping notification
- @param order - Order object with tracking number
- @returns SMS delivery result
*/
async sendShippingNotification(order: {
id: string;
customerPhone: string;
trackingNumber: string;
}): Promise<boolean> {
const message =
您的訂單 #${order.id} 已出貨!追蹤號碼:${order.trackingNumber};
const result = await this.smsService.send({
mobile: order.customerPhone,
text: message,
});
if (result.status === SMSRequestResult.FAILED) {
console.error('Failed to send shipping notification:', {
orderId: order.id,
errorCode: result.errorCode,
errorMessage: result.errorMessage,
});
}
return result.status === SMSRequestResult.SUCCESS;
}
/**
- Send delivery notification to multiple orders
- @param orders - Array of orders ready for delivery
- @returns Delivery statistics
*/
async sendBulkDeliveryNotifications(orders: Array<{
id: string;
customerPhone: string;
}>): Promise<{
total: number;
successful: number;
failed: number;
}> {
const results = await this.smsService.send(
orders.map(order => ({
mobile: order.customerPhone,
text:
您的訂單 #${order.id} 將於今日送達,請留意收件。, })) );
const successful = results.filter(r => r.status === SMSRequestResult.SUCCESS).length;
const failed = results.filter(r => r.status === SMSRequestResult.FAILED).length;
return {
total: results.length,
successful,
failed,
};
} }
Authentication & Verification
import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d'; import { SMSRequestResult } from '@rytass/sms';
class AuthSMSService { private smsService: SMSServiceEvery8D;
constructor() { this.smsService = new SMSServiceEvery8D({ username: process.env.EVERY8D_USERNAME!, password: process.env.EVERY8D_PASSWORD!, onlyTaiwanMobileNumber: true, }); }
/**
- Generate and send verification code
- @param phoneNumber - Recipient phone number
- @returns Verification code (store in cache/database) */ async sendVerificationCode(phoneNumber: string): Promise<string> { // Generate 6-digit code const code = Math.random().toString().slice(-6);
const message = `您的驗證碼是 ${code},10 分鐘內有效。請勿將此驗證碼告知他人。`;
const result = await this.smsService.send({
mobile: phoneNumber,
text: message,
});
if (result.status === SMSRequestResult.SUCCESS) {
// Store code in Redis/cache with 10-minute expiration
await this.storeVerificationCode(phoneNumber, code, 600);
console.log('Verification code sent:', result.messageId);
return code;
} else {
throw new Error(`SMS delivery failed: ${result.errorMessage}`);
}
}
/**
- Send 2FA code for login
- @param phoneNumber - User's phone number
- @param serviceName - Name of the service for branding
- @returns void */ async send2FACode( phoneNumber: string, serviceName: string ): Promise<void> { const code = Math.random().toString().slice(-6);
const message = `${serviceName} 登入驗證碼:${code},5 分鐘內有效。`;
const result = await this.smsService.send({
mobile: phoneNumber,
text: message,
});
if (result.status === SMSRequestResult.SUCCESS) {
await this.store2FACode(phoneNumber, code, 300); // 5 minutes
} else {
throw new Error(`Failed to send 2FA code: ${result.errorMessage}`);
}
}
/**
- Send password reset link
- @param phoneNumber - User's phone number
- @param resetLink - Password reset URL
- @returns SMS delivery result
*/
async sendPasswordResetLink(
phoneNumber: string,
resetLink: string
): Promise<boolean> {
const message =
密碼重設連結:${resetLink}。此連結將在 30 分鐘後失效。;
const result = await this.smsService.send({
mobile: phoneNumber,
text: message,
});
return result.status === SMSRequestResult.SUCCESS;
}
private async storeVerificationCode(
phone: string,
code: string,
expirationSeconds: number
): Promise<void> {
// Implementation depends on your storage solution
// Redis, database, or in-memory cache
// Example with Redis:
// await redis.setex(verification:${phone}, expirationSeconds, code);
}
private async store2FACode(
phone: string,
code: string,
expirationSeconds: number
): Promise<void> {
// Store with shorter expiration for 2FA
// await redis.setex(2fa:${phone}, expirationSeconds, code);
}
}
Marketing Campaign Service
import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d'; import { SMSRequestResult } from '@rytass/sms';
class MarketingCampaignService { private smsService: SMSServiceEvery8D;
constructor() { this.smsService = new SMSServiceEvery8D({ username: process.env.EVERY8D_USERNAME!, password: process.env.EVERY8D_PASSWORD!, onlyTaiwanMobileNumber: true, }); }
/**
- Send promotional campaign to customers
- @param customers - Customer list with phone numbers
- @param campaignMessage - Campaign message text
- @returns Campaign statistics */ async sendPromotionalCampaign( customers: Array<{ phoneNumber: string; name: string }>, campaignMessage: string ): Promise<{ total: number; successful: number; failed: number; successRate: number; }> { // Split into batches to avoid rate limiting const batchSize = 100; const batches: Array<typeof customers> = [];
for (let i = 0; i < customers.length; i += batchSize) {
batches.push(customers.slice(i, i + batchSize));
}
const allResults = [];
for (const batch of batches) {
const phoneNumbers = batch.map(customer => customer.phoneNumber);
try {
const batchResult = await this.smsService.send({
mobileList: phoneNumbers,
text: campaignMessage,
});
allResults.push(...batchResult);
// Add delay between batches to respect rate limits
await this.delay(1000); // 1 second delay
} catch (error) {
console.error('Batch failed:', error);
// Add failed entries for this batch
phoneNumbers.forEach(phone => {
allResults.push({
mobile: phone,
status: SMSRequestResult.FAILED,
errorMessage: 'Batch processing error',
});
});
}
}
return this.analyzeCampaignResults(allResults);
}
/**
- Send personalized messages to customers
- @param customers - Customer list with personalized data
- @returns Campaign statistics */ async sendPersonalizedCampaign( customers: Array<{ phoneNumber: string; name: string; customMessage: string; }> ): Promise<{ total: number; successful: number; failed: number; successRate: number; }> { const results = await this.smsService.send( customers.map(customer => ({ mobile: customer.phoneNumber, text: customer.customMessage, })) );
return this.analyzeCampaignResults(results);
}
/**
- Delay helper for rate limiting
- @param ms - Milliseconds to delay
- @returns Promise that resolves after delay */ private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); }
/**
- Analyze campaign results
- @param results - Array of SMS send results
- @returns Campaign statistics */ private analyzeCampaignResults( results: Array<{ status: SMSRequestResult; mobile: string }> ): { total: number; successful: number; failed: number; successRate: number; } { const successful = results.filter( r => r.status === SMSRequestResult.SUCCESS ).length; const failed = results.filter( r => r.status === SMSRequestResult.FAILED ).length;
return {
total: results.length,
successful,
failed,
successRate: (successful / results.length) * 100,
};
} }
Appointment Reminders
import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d'; import { SMSRequestResult } from '@rytass/sms';
class AppointmentReminderService { private smsService: SMSServiceEvery8D;
constructor() { this.smsService = new SMSServiceEvery8D({ username: process.env.EVERY8D_USERNAME!, password: process.env.EVERY8D_PASSWORD!, onlyTaiwanMobileNumber: true, }); }
/**
- Send appointment reminder
- @param appointment - Appointment details
- @returns SMS delivery result
*/
async sendAppointmentReminder(appointment: {
customerPhone: string;
customerName: string;
date: string;
time: string;
location: string;
doctorName?: string;
}): Promise<boolean> {
const message = appointment.doctorName
?
親愛的 ${appointment.customerName},提醒您與 ${appointment.doctorName} 醫師的預約:${appointment.date} ${appointment.time},地點:${appointment.location}。請提前 10 分鐘報到。:親愛的 ${appointment.customerName},提醒您的預約:${appointment.date} ${appointment.time},地點:${appointment.location}。請提前 10 分鐘報到。;
const result = await this.smsService.send({
mobile: appointment.customerPhone,
text: message,
});
return result.status === SMSRequestResult.SUCCESS;
}
/**
- Send batch appointment reminders
- @param appointments - Array of appointments
- @returns Statistics of sent reminders
*/
async sendBatchReminders(
appointments: Array<{
customerPhone: string;
date: string;
time: string;
}>
): Promise<{
total: number;
sent: number;
failed: number;
}> {
const results = await this.smsService.send(
appointments.map(apt => ({
mobile: apt.customerPhone,
text:
預約提醒:${apt.date} ${apt.time}。請準時出席,如需取消請提前通知。, })) );
const sent = results.filter(
r => r.status === SMSRequestResult.SUCCESS
).length;
const failed = results.filter(
r => r.status === SMSRequestResult.FAILED
).length;
return {
total: results.length,
sent,
failed,
};
} }
NestJS Integration
Basic Setup
// sms.module.ts import { Module, Global } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d';
export const SMS_SERVICE = Symbol('SMS_SERVICE');
@Global() @Module({ imports: [ConfigModule], providers: [ { provide: SMS_SERVICE, useFactory: (config: ConfigService) => { return new SMSServiceEvery8D({ username: config.get('EVERY8D_USERNAME')!, password: config.get('EVERY8D_PASSWORD')!, onlyTaiwanMobileNumber: config.get('EVERY8D_TAIWAN_ONLY') === 'true', }); }, inject: [ConfigService], }, ], exports: [SMS_SERVICE], }) export class SMSModule {}
Using in Services
// notification.service.ts import { Injectable, Inject, Logger } from '@nestjs/common'; import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d'; import { SMSRequestResult } from '@rytass/sms'; import { SMS_SERVICE } from './sms.module';
@Injectable() export class NotificationService { private readonly logger = new Logger(NotificationService.name);
constructor( @Inject(SMS_SERVICE) private readonly smsService: SMSServiceEvery8D, ) {}
/**
-
Send verification code via SMS
-
@param phoneNumber - Recipient phone number
-
@param code - Verification code
-
@returns Whether SMS was sent successfully */ async sendVerificationCode( phoneNumber: string, code: string, ): Promise<boolean> { try { const result = await this.smsService.send({ mobile: phoneNumber, text:
您的驗證碼是 ${code},10 分鐘內有效。, });if (result.status === SMSRequestResult.SUCCESS) { this.logger.log(
Verification code sent to ${phoneNumber}); return true; } else { this.logger.error(Failed to send verification code to ${phoneNumber}: ${result.errorMessage}, ); return false; } } catch (error) { this.logger.error('SMS sending error:', error); return false; } }
/**
-
Send bulk notification
-
@param phoneNumbers - Array of recipient phone numbers
-
@param message - Message to send
-
@returns Statistics of sent messages */ async sendBulkNotification( phoneNumbers: string[], message: string, ): Promise<{ total: number; successful: number; failed: number; }> { try { const results = await this.smsService.send({ mobileList: phoneNumbers, text: message, });
const successful = results.filter( r => r.status === SMSRequestResult.SUCCESS, ).length; const failed = results.filter( r => r.status === SMSRequestResult.FAILED, ).length;
this.logger.log(
Bulk notification sent: ${successful} successful, ${failed} failed, );return { total: results.length, successful, failed, }; } catch (error) { this.logger.error('Bulk SMS sending error:', error); throw error; } } }
Usage in Controllers
// notification.controller.ts import { Controller, Post, Body } from '@nestjs/common'; import { NotificationService } from './notification.service';
@Controller('notifications') export class NotificationController { constructor(private readonly notificationService: NotificationService) {}
@Post('send-verification') async sendVerification( @Body() body: { phoneNumber: string; code: string }, ): Promise<{ success: boolean }> { const success = await this.notificationService.sendVerificationCode( body.phoneNumber, body.code, );
return { success };
}
@Post('send-bulk') async sendBulk( @Body() body: { phoneNumbers: string[]; message: string }, ): Promise<{ total: number; successful: number; failed: number; }> { return await this.notificationService.sendBulkNotification( body.phoneNumbers, body.message, ); } }
Configuration
Environment Variables
.env
EVERY8D_USERNAME=your_every8d_username EVERY8D_PASSWORD=your_every8d_password EVERY8D_BASE_URL=https://api.e8d.tw EVERY8D_TAIWAN_ONLY=true
TypeScript Configuration
// config.ts export interface SMSConfig { username: string; password: string; baseUrl?: string; onlyTaiwanMobileNumber?: boolean; }
export const smsConfig: SMSConfig = { username: process.env.EVERY8D_USERNAME!, password: process.env.EVERY8D_PASSWORD!, baseUrl: process.env.EVERY8D_BASE_URL || 'https://api.e8d.tw', onlyTaiwanMobileNumber: process.env.EVERY8D_TAIWAN_ONLY === 'true', };
Error Handling
Handling Delivery Failures
import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d'; import { SMSRequestResult } from '@rytass/sms'; import { Every8DError } from '@rytass/sms-adapter-every8d';
async function sendWithRetry( smsService: SMSServiceEvery8D, mobile: string, text: string, maxRetries: number = 3 ): Promise<boolean> { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = await smsService.send({ mobile, text });
if (result.status === SMSRequestResult.SUCCESS) {
console.log(`SMS sent successfully on attempt ${attempt}`);
return true;
}
// Check error code
if (result.errorCode === Every8DError.FORMAT_ERROR) {
console.error('Invalid format - no retry needed');
return false;
}
console.warn(`Attempt ${attempt} failed:`, result.errorMessage);
if (attempt < maxRetries) {
// Wait before retry (exponential backoff)
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 1000)
);
}
} catch (error) {
console.error(`Attempt ${attempt} error:`, error);
if (attempt === maxRetries) {
throw error;
}
}
}
return false; }
Batch Error Handling
async function sendBatchWithErrorHandling( smsService: SMSServiceEvery8D, phoneNumbers: string[], message: string ): Promise<{ successful: string[]; failed: Array<{ phone: string; error: string }>; }> { const results = await smsService.send({ mobileList: phoneNumbers, text: message, });
const successful: string[] = []; const failed: Array<{ phone: string; error: string }> = [];
results.forEach(result => { if (result.status === SMSRequestResult.SUCCESS) { successful.push(result.mobile); } else { failed.push({ phone: result.mobile, error: result.errorMessage || 'Unknown error', }); } });
// Log failures if (failed.length > 0) { console.error('Failed deliveries:', failed); }
return { successful, failed }; }
Best Practices
Message Content
class MessageTemplates { /**
- Verification code template
-
- Keep message under 70 characters for single SMS
-
- Include expiration time
-
- Add security warning
*/
static verification(code: string): string {
return
您的驗證碼是 ${code},10 分鐘內有效。請勿將此驗證碼告知他人。; }
- Add security warning
*/
static verification(code: string): string {
return
/**
- Order confirmation template
-
- Include order number for reference
-
- Keep essential info only
*/
static orderConfirmation(orderNumber: string, amount: number): string {
return
訂單 ${orderNumber} 已確認。金額:NT$${amount}。感謝您的購買!; }
- Keep essential info only
*/
static orderConfirmation(orderNumber: string, amount: number): string {
return
/**
- Appointment reminder template
-
- Include date, time, and location
-
- Add action instructions
*/
static appointmentReminder(date: string, time: string): string {
return
預約提醒:${date} ${time}。請提前 10 分鐘報到。如需取消請來電。; }
- Add action instructions
*/
static appointmentReminder(date: string, time: string): string {
return
/**
- Marketing campaign template
-
- Include opt-out instructions for compliance
-
- Keep under 70 characters if possible
*/
static promotion(discount: string): string {
return
限時優惠!${discount} 折扣活動進行中。查看詳情:[連結]。回 N 取消訂閱。; } }
- Keep under 70 characters if possible
*/
static promotion(discount: string): string {
return
Security
// 1. Store credentials securely // ❌ Don't hardcode credentials const badService = new SMSServiceEvery8D({ username: 'myusername', password: 'mypassword', });
// ✅ Use environment variables const goodService = new SMSServiceEvery8D({ username: process.env.EVERY8D_USERNAME!, password: process.env.EVERY8D_PASSWORD!, });
// 2. Validate phone numbers before sending function isValidTaiwanMobile(phone: string): boolean { const cleaned = phone.replace(/[^0-9]/g, '').replace(/^886/, '0'); return /^09\d{8}$/.test(cleaned); }
// 3. Implement rate limiting class RateLimitedSMSService { private lastSendTime = 0; private minInterval = 1000; // 1 second between sends
async send(smsService: SMSServiceEvery8D, data: any) { const now = Date.now(); const timeSinceLastSend = now - this.lastSendTime;
if (timeSinceLastSend < this.minInterval) {
await new Promise(resolve =>
setTimeout(resolve, this.minInterval - timeSinceLastSend)
);
}
const result = await smsService.send(data);
this.lastSendTime = Date.now();
return result;
} }
// 4. Log all SMS activities for audit async function sendWithAuditLog( smsService: SMSServiceEvery8D, mobile: string, text: string, userId?: string ): Promise<void> { const auditEntry = { timestamp: new Date(), userId, recipient: mobile, messageLength: text.length, action: 'SMS_SEND', };
try { const result = await smsService.send({ mobile, text });
await logAudit({
...auditEntry,
status: result.status,
messageId: result.messageId,
});
} catch (error) { await logAudit({ ...auditEntry, status: 'ERROR', error: error.message, }); throw error; } }
async function logAudit(entry: any): Promise<void> { // Save to database or logging service }
Performance
// 1. Batch messages efficiently // ❌ Don't send one by one async function inefficientSend(phones: string[], message: string) { for (const phone of phones) { await smsService.send({ mobile: phone, text: message }); } }
// ✅ Use batch send async function efficientSend(phones: string[], message: string) { await smsService.send({ mobileList: phones, text: message }); }
// 2. Handle large batches with chunking async function sendLargeBatch( smsService: SMSServiceEvery8D, phones: string[], message: string, chunkSize: number = 100 ): Promise<void> { for (let i = 0; i < phones.length; i += chunkSize) { const chunk = phones.slice(i, i + chunkSize);
await smsService.send({
mobileList: chunk,
text: message,
});
// Add delay between chunks
if (i + chunkSize < phones.length) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
} }
Compliance
// 1. Respect user preferences and opt-out requests interface UserSMSPreference { phoneNumber: string; optedOut: boolean; marketingConsent: boolean; transactionalOnly: boolean; }
async function sendRespectingPreferences( smsService: SMSServiceEvery8D, user: UserSMSPreference, message: string, messageType: 'marketing' | 'transactional' ): Promise<boolean> { // Check if user opted out completely if (user.optedOut) { console.log('User opted out - skipping SMS'); return false; }
// Check marketing consent if (messageType === 'marketing' && !user.marketingConsent) { console.log('No marketing consent - skipping SMS'); return false; }
// Send the message const result = await smsService.send({ mobile: user.phoneNumber, text: message, });
return result.status === SMSRequestResult.SUCCESS; }
// 2. Include opt-out instructions in marketing messages
function createMarketingMessage(content: string): string {
return ${content}\n\n回覆 N 取消訂閱簡訊通知。;
}
// 3. Maintain do-not-contact list class DoNotContactList { private blockedNumbers = new Set<string>();
async add(phoneNumber: string): Promise<void> { this.blockedNumbers.add(phoneNumber); // Persist to database }
async remove(phoneNumber: string): Promise<void> { this.blockedNumbers.delete(phoneNumber); // Update database }
isBlocked(phoneNumber: string): boolean { return this.blockedNumbers.has(phoneNumber); }
async filterAllowed(phoneNumbers: string[]): Promise<string[]> { return phoneNumbers.filter(phone => !this.isBlocked(phone)); } }
Internal Implementation Details
Empty Target Handling
傳入空陣列或空 mobileList 會拋出錯誤:
// 以下情況會拋出 'No target provided.' 錯誤 await smsService.send([]); // 空陣列 await smsService.send({ mobileList: [], text: 'Test' }); // 空 mobileList
Batch Message Optimization
相同訊息內容的多個請求會自動合併為單一 API 呼叫,提高發送效率:
// 這三個請求會合併成一個 API 呼叫 const results = await smsService.send([ { mobile: '0912345678', text: 'Hello' }, { mobile: '0923456789', text: 'Hello' }, // 相同訊息 { mobile: '0934567890', text: 'Hello' }, // 相同訊息 ]); // 內部會將 DEST 參數合併為 '0912345678,0923456789,0934567890'
Partial Delivery Failure Handling
當部分發送失敗時,API 回傳 unsend 數量,系統會從列表末端開始標記失敗:
// 假設發送 5 個號碼,API 回傳 unsend=2 // 則前 3 個標記為 SUCCESS,後 2 個標記為 FAILED const results = await smsService.send({ mobileList: ['0911...', '0922...', '0933...', '0944...', '0955...'], text: 'Test', }); // results[0..2].status === SMSRequestResult.SUCCESS // results[3..4].status === SMSRequestResult.FAILED
Troubleshooting
Common Issues
- Authentication Failure
Symptoms:
-
All messages fail to send
-
Error message about credentials
Solutions:
// Check credentials console.log('Username:', process.env.EVERY8D_USERNAME); console.log('Password length:', process.env.EVERY8D_PASSWORD?.length);
// Ensure no extra spaces in .env EVERY8D_USERNAME=your_username # ❌ Trailing space EVERY8D_USERNAME=your_username # ✅ No space
// Test with minimal code const testService = new SMSServiceEvery8D({ username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD', });
const result = await testService.send({ mobile: '0987654321', text: 'Test', });
console.log('Result:', result);
- Invalid Phone Number Format
Symptoms:
-
FORMAT_ERROR (-306) error code
-
Message fails for specific numbers
Solutions:
// Enable Taiwan-only validation const service = new SMSServiceEvery8D({ username: process.env.EVERY8D_USERNAME!, password: process.env.EVERY8D_PASSWORD!, onlyTaiwanMobileNumber: true, });
// Validate before sending function validateTaiwanMobile(phone: string): boolean { const normalized = phone.replace(/[^0-9]/g, '').replace(/^886/, '0'); return /^09\d{8}$/.test(normalized); }
if (!validateTaiwanMobile(phoneNumber)) { console.error('Invalid Taiwan mobile number:', phoneNumber); }
- Message Not Delivered
Symptoms:
-
Status shows SUCCESS but message not received
-
Delays in delivery
Solutions:
// 1. Check message content // - Avoid special characters that might cause issues // - Keep message under 70 characters for single SMS
// 2. Verify phone number is active // - Test with your own phone first
// 3. Check account balance // - Contact Every8D to verify account status
// 4. Log message ID for tracking const result = await smsService.send({ mobile, text }); console.log('Message ID for tracking:', result.messageId);
API Reference
SMSServiceEvery8D
Constructor Options:
Property Type Required Default Description
username
string
Yes
Every8D account username
password
string
Yes
Every8D account password
baseUrl
string
No 'https://api.e8d.tw'
API endpoint URL
onlyTaiwanMobileNumber
boolean
No false
Restrict to Taiwan mobile numbers only
Methods:
// Send single SMS send(request: Every8DSMSRequest): Promise<Every8DSMSSendResponse>
// Send multiple SMS with different messages send(requests: Every8DSMSRequest[]): Promise<Every8DSMSSendResponse[]>
// Send same message to multiple recipients send(request: Every8DSMSMultiTargetRequest): Promise<Every8DSMSSendResponse[]>
Request Types
interface Every8DSMSRequest { mobile: string; // Phone number (will be normalized) text: string; // Message content }
interface Every8DSMSMultiTargetRequest { mobileList: string[]; // Array of phone numbers text: string; // Message content (same for all) }
Response Types
interface Every8DSMSSendResponse { messageId?: string; // Message ID for tracking status: SMSRequestResult; // SUCCESS or FAILED mobile: string; // Normalized phone number errorMessage?: string; // Error message if failed errorCode?: Every8DError; // Error code if failed }
enum SMSRequestResult { SUCCESS = 'SUCCESS', FAILED = 'FAILED', }
enum Every8DError { FORMAT_ERROR = -306, UNKNOWN = -99, }
Advanced Topics
For creating new SMS adapters or extending functionality, see the SMS Development Guide.