使用 Twilio Webhooks 和 Express 在 Node.js 中发送和接收短信
构建一个小型 Express 服务器,通过 Twilio 发送文本消息,利用 ngrok 提供的 webhook 接收回复,并用 TwiML 自动回复这些消息。
来源: **Build a Two-Way SMS with Node.js and Twilio (Full Code Included)**
大多数短信教程只在消息离开服务器后就结束了。更有用的是接收消息的部分:检测到用户回复后,读取内容并作出回应。这样的循环支撑着一次性密码生成流程、客服机器人以及只需一个字就能确认的日程提醒功能。完成本教程后,你将拥有一个能够向手机发送短信,并在手机回复时作出响应的Express服务器。
前置条件
- 一个Twilio账户。在撰写本文时,免费试用版包含约15美元的信用额度,足够用于实验。
- Postman或任何HTTP客户端。
- 已在本地安装的Node.js。
- ngrok,它能为Twilio提供一个可转发到你笔记本电脑的公共地址。
最简Express服务器
创建一个包含 server.js 文件的文件夹。该服务器绑定在本地主机的 3000 端口上,支持 JSON 和 URL 编码的请求体解析,并在 /api/v1/ 下提供一个健康检查接口。URL 编码解析器非常重要:Twilio 以表单编码的 POST 请求体形式发送消息,如果没有它,后续 req.body 将为空。
const express = require("express");
const bodyParser = require("body-parser");
const hostname = "127.0.0.1";
const port = 3000;
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const baseUrl = "/api/v1/";
app.get(`${baseUrl}`, (req, res) => {
res.status(200).json({ message: "Hello World" });
});
app.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
安装依赖项:
npm init -y
npm install express body-parser
运行 node server.js,然后打开 http://127.0.0.1:3000/api/v1/;如果收到“Hello World”响应,即说明基本功能正常。Express 4.16 及更高版本已内置 express.json() 和 express.urlencoded(),因此在新项目中使用 body-parser 是可选的。
使用 ngrok 公开本地主机地址
Twilio 运行在公共互联网上,而你的服务器位于私有机器上,因此 Twilio 无法直接访问它。ngrok 会建立一条隧道,并提供一个指向本地端口的公共 HTTPS URL。Heroku 曾是实现此功能的免费选项,但其免费层级已取消。
请参考ngrok 文档来安装并验证 ngrok,然后向 package.json 中添加脚本:
"scripts": {
"ngrok": "ngrok http http://127.0.0.1:3000/"
}
在一个终端中保持服务器运行,然后在另一个终端中启动隧道:
npm run ngrok
ngrok 会输出类似 https://abcd-1234.ngrok-free.app 的地址。请保持该终端处于打开状态。
注意:在免费套餐下,生成的 URL 通常会在每次 ngrok 重启时发生变化。如果正常的 webhook 突然停止响应,很可能是 Twilio 控制台中的旧 URL 导致的,因此请在每次重启后更新它。
配置 Twilio
在控制台仪表板中,将您的账户 SID 和认证令牌复制到 .env 文件中。该令牌属于机密信息:切勿将其提交到版本控制,同时需在 .gitignore 文件中列出 .env。
ACCOUNT_SID_TWILIO=your_account_sid
AUTH_TOKEN_TWILIO=your_auth_token
您可以在侧边导航中固定相关产品;此处只需关注 Messaging 和 Phone Numbers 两类。
获取电话号码
- 打开 Phone Numbers,然后选择 Buy a number。
- 挑选任意可用号码;美国号码通常最便宜。
- 费用会从试算余额中扣除,该号码也会出现在您的活跃号码列表中。
创建消息服务
- 在 Messaging 页面下创建一个新的 Messaging Service。
- 在 Sender Pool 步骤中,添加您的电话号码。
/api/v1/sms路径的ngrok地址。该路由已在下方配置完成。将消息服务SID及您的号码存储在.env文件中:
FROM_TWILIO=+1XXXXXXXXXX
MESSAGING_SERVICE_SID_TWILIO=your_messaging_service_sid
将Twilio SDK集成到Node中
首先安装SDK以及用于将.env内容加载到process.env中的dotenv:
npm install twilio dotenv
请将此代码放在server.js的最顶部。顺序很重要:客户端在创建时会读取凭证,如果dotenv在之后才被加载,那么这两个值都会为undefined,从而导致所有认证尝试失败。
require("dotenv").config();
const client = require("twilio")(
process.env.ACCOUNT_SID_TWILIO,
process.env.AUTH_TOKEN_TWILIO
);
发送外出短信
该路由会调用 client.messages.create(),并以 JSON 格式返回执行结果。try/catch 机制会将诸如号码格式错误之类的 Twilio 错误返回给调用方,而不会导致程序崩溃。
app.post(`${baseUrl}sendSms`, async (req, res) => {
try {
await client.messages.create({
body: "Hello",
from: process.env.FROM_TWILIO,
messagingServiceSid: process.env.MESSAGING_SERVICE_SID_TWILIO,
to: "+92XXXXXXXXXX", // whoever you're texting
});
res.status(200).json({ message: "Text sent successfully" });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
各字段的含义:
body:消息文本。from:你的 Twilio 号码。messagingServiceSid:将消息与对应的服务及发件人池关联起来。to:接收方的 E.164 格式号码(如+1...、+92...)。
在 Postman 中调用 POST /api/v1/sendSms,手机应会收到“Hello”消息。在实际代码中,应从请求中读取 to 和 body 并进行验证,而非直接使用硬编码值。
测试账户只能向在 Twilio 控制台验证过的号码发送短信。如果请求成功但未收到任何回复,请检查接收方或升级套餐。
通过 webhook 接收回复
当有人向您的 Twilio 号码发送短信时,Twilio 会将消息发送到 Messaging Service 中配置的 webhook。您的服务器会接收该消息,并以 TwiML 格式回复,TwiML 是 Twilio 用于描述下一步操作的 XML 格式。您需要将相关辅助文件与其他必需文件一起导入:
const MessagingResponse = require("twilio").twiml.MessagingResponse;
接着添加监听器。常见的错误是路径不匹配:路由地址必须与控制台中输入的路径一致,此处为 /api/v1/sms。处理程序会记录 req.body.Body 的内容,然后以 text/xml 格式返回空白的 TwiML 文档,这意味着不会发送回复。
app.post(`${baseUrl}sms`, (req, res) => {
// Fires every time someone replies to your number
const twiml = new MessagingResponse();
console.log("INCOMING ----->", req.body.Body);
res.writeHead(200, { "Content-Type": "text/xml" });
res.end(twiml.toString());
});
输入您的 Twilio 号码后,消息会显示在终端中,此时循环结束。消息体中还包含 From 字段,即发送方号码,当您需要按用户追踪对话时就会用到它。
由于该接口是公开的,任何找到该 URL 的人都可以发送虚假消息。Twilio 会在 X-Twilio-Signature 标头中使用您的认证令牌对每个 webhook 进行签名,Node SDK 也提供了验证工具。在本地测试之外部署之前,请务必启用这些功能。
自动回复
服务器会监听但不会回应。在 TwiML 响应中添加消息内容即可将其转变为关键词机器人。应在创建 twiml 后将其放入 /sms 处理器中;该处理器会在选择回复内容之前对文本进行截取和转小写处理:
const incoming = req.body.Body.trim().toLowerCase();
if (incoming === "hi") {
twiml.message("Hey there! How can I help?");
} else {
twiml.message("Hmm, I didn't catch that.");
}
Twilio会将返回的回复发送给发件人,因此无需额外的messages.create()调用。
完整的服务器代码
所有内容都在一个文件中,自动回复部分已被注释掉:
require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const MessagingResponse = require("twilio").twiml.MessagingResponse;
const client = require("twilio")(
process.env.ACCOUNT_SID_TWILIO,
process.env.AUTH_TOKEN_TWILIO
);
const hostname = "127.0.0.1";
const port = 3000;
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const baseUrl = "/api/v1/";
// Health check
app.get(`${baseUrl}`, (req, res) => {
res.status(200).json({ message: "Hello World" });
});
// Goal 1: Send an SMS
app.post(`${baseUrl}sendSms`, async (req, res) => {
try {
await client.messages.create({
body: "Hello",
from: process.env.FROM_TWILIO,
messagingServiceSid: process.env.MESSAGING_SERVICE_SID_TWILIO,
to: "+92XXXXXXXXXX", // whoever you're texting
});
res.status(200).json({ message: "Text sent successfully" });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Goal 2: Receive replies (this endpoint must match your Twilio webhook)
app.post(`${baseUrl}sms`, (req, res) => {
const twiml = new MessagingResponse();
console.log("INCOMING ----->", req.body.Body);
// Optional auto-reply:
// const incoming = req.body.Body.trim().toLowerCase();
// if (incoming === "hi") {
// twiml.message("Hey there! How can I help?");
// } else {
// twiml.message("Hmm, I didn't catch that.");
// }
res.writeHead(200, { "Content-Type": "text/xml" });
res.end(twiml.toString());
});
app.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
关键要点
- 发送短信只需一次SDK调用;接收短信则通过webhook实现,因此服务器必须可访问且能理解TwiML格式。
- Twilio会发送经过表单编码的数据,因此需要使用URL编码解析器。
- 控制台webhook的路径与Express路由必须一致,重新启动免费隧道会导致URL发生变化。
- 静默的测试失败通常是由于受验证号码限制导致的,而非代码错误。
- 在实际使用前需验证签名并移除硬编码的值。
从这里你可以保存对话记录、完善关键词机器人功能,或添加一次性密码登录功能。关于回调与重试的更多模式,请参阅六种可靠连接 Node.js 服务的集成模式。