@msgly/twilio-sms
v1.11.0
Published
Twilio SMS adapter for Msgly
Maintainers
Readme
@msgly/twilio-sms
📖 Docs & channel reference: https://ayushjain070401.github.io/msgly/
Twilio SMS/MMS adapter for Msgly.
npm install @msgly/core @msgly/twilio-smsimport { createHub } from '@msgly/core';
import { createTwilioSmsAdapter } from '@msgly/twilio-sms';
const twilio = createTwilioSmsAdapter({
accountSid: process.env.TWILIO_ACCOUNT_SID!, // starts with "AC"
authToken: process.env.TWILIO_AUTH_TOKEN!,
phoneNumber: '+15550001111',
webhookUrl: 'https://example.com/webhook/twilio-sms', // required for signatures
});
const hub = createHub().register(twilio);
await hub.send({
channel: 'twilio-sms',
account: { channel: 'twilio-sms', channelAccountId: '+15550001111' },
contact: { channel: 'twilio-sms', channelUserId: '+15550002222' },
content: { type: 'text', text: 'hello from msgly' },
});webhookUrl must match exactly
Twilio signs the full webhook URL including query parameters, together with
the sorted POST body. webhookUrl therefore has to be byte-identical to the URL
Twilio actually calls — an http vs https mismatch, a stray trailing slash, or
a proxy rewriting the host will fail every signature check.
Leaving it unset makes verifySignature return true for everything, which is
only acceptable behind an IP allowlist.
MMS
Images are supported on US and Canada numbers. Twilio fetches the file itself, so pass a public URL:
content: {
type: 'image',
mediaRef: { kind: 'url', value: 'https://cdn.example.com/pic.png' },
caption: 'your receipt',
}Delivery receipts
Set statusCallbackUrl and Twilio posts status updates there. The adapter does
not consume that endpoint itself — wire it into your own handler if you need
delivery states beyond the initial API response.
Rate limits
Long codes send at 1 message/second, which is the campaign default here. Short codes and toll-free numbers are far higher — raise it when you have one:
await hub.sendBulk({ channel: 'twilio-sms', /* ... */, rateLimit: { perSecond: 100 } });Opt-outs
Twilio honours STOP/START at the carrier level, but your own list should track it too. Pair this adapter with core's suppression store:
import { applyConsentIntent } from '@msgly/core';
hub.on('message', (msg) => applyConsentIntent(msg, suppression));Checking the number
verifyCredentials() validates the phone number as well as the key and secret,
so a wrong number is caught where it was typed rather than on the first send.
The number check is also exposed on its own:
const check = await adapter.verifyPhoneNumber();
// { ok: false, status: 'not_owned', phoneNumber: '+15551234567', hint: '…' }It first checks the number is valid E.164 — naming the actual mistake, such as
a missing + or leftover dashes — then asks Twilio whether the number is on
this account, which also catches a valid number belonging to a different
Twilio account.
status is owned, not_owned, malformed, or inconclusive. The last one
matters: a restricted API key that cannot list numbers can't answer the
ownership question, so the check reports ok: true with inconclusive rather
than calling a working number invalid. Only not_owned and malformed fail.
License
MIT
