常见问题解答
全球覆盖、不限文案、免费测试
电话/微信:182-0071-8221

印度短信API回调机制:如何实时获取短信发送状态?

2026-09-21 22:40:48

  向印度用户发送商业短信时,API返回“发送成功”并不意味着短信已经到达用户手机。在印度DLT(分布式账本技术)体系下,短信从提交到最终送达需要经过DLT清洗、运营商路由、终端投递等多个环节,每个环节都可能出现失败。**获取真实的发送状态,必须依赖短信API的回调机制。**

  本文将详细解析印度短信API的回调(Webhook)机制,对比Webhook与轮询两种状态获取方式的优劣,并提供Webhook接收端的代码实现和DLT特有的状态码解读。

  一、为什么需要回调机制?

  短信发送流程的复杂性决定了状态获取的必要性。以印度市场为例,一条商业短信的完整路径是:

  > 你的应用 → 短信API → **DLT清洗(Scrubbing)** → 电信运营商 → 接收方手机

  DLT清洗环节是印度特有的“过滤网”——运营商根据TRAI规定,逐字符比对短信内容与备案模板、验证Sender ID和Entity ID的合法性。清洗失败的消息会被永久拒绝,且不会重试。同时,促销类短信的DLR(Delivery Report)已被运营商停止发送,只能标记为“已提交至运营商”。这些DLT特有的状态,只有通过回调机制才能准确获取。

  二、Webhook vs 轮询:两种状态获取方式对比

  获取短信发送状态有两种主流方式:**Webhook回调**和**API轮询**。

对比维度 Webhook回调 API轮询
实时性 状态变化后秒级推送 取决于轮询频率,通常延迟数分钟
服务器压力 低,仅在状态变化时接收请求 高,需持续发起查询请求
实现复杂度 需搭建可公网访问的接收端点 仅需调用查询接口
可扩展性 适合大规模发送场景 高频轮询易触发速率限制
DLR可用性 交易类/服务类短信可获取 同样受运营商DLR政策影响

  **Webhook是生产环境的首选方案**。SMSGatewayCenter的官方文档明确指出,Webhook让状态“在几秒内到达”,而轮询主要用于Webhook丢失后的回填和审计。对于OTP和交易类短信,Webhook的实时性直接影响重试决策——如果第一条短信因DLT清洗失败,你需要立即知道并触发降级通道(如语音OTP)。

  三、Webhook回调的工作原理与配置

  3.1 工作原理

  Webhook回调的本质是**服务端主动推送**。短信服务商在状态变化时,向你在控制台配置的URL发起POST请求,请求体中包含消息ID和状态信息。

  **典型的状态变化触发场景**:

  - 消息已提交至运营商(submitted)

  - 消息已送达(delivered)

  - 消息失败(failed)

  - 消息被DLT清洗拒绝(DLT scrubbing failure)

  3.2 配置步骤(以SMSGatewayCenter为例)

  **第一步:创建Webhook端点**

  在短信服务商控制台或通过API注册Webhook URL。SMSGatewayCenter提供专门的Webhook创建接口:

  ```

  POST https://www.lanlansms.com/sms/

  ```

  配置时需指定:Webhook URL、每秒事务数(TPS)限制、认证方式。

  **第二步:确保Webhook端点可公网访问**

  Webhook URL必须能从服务商的服务器访问,支持POST方法,且不设置IP白名单拦截。建议使用HTTPS协议,避免中间人攻击。

  **第三步:验证Webhook连通性**

  使用RequestBin或webhook.site等工具生成临时URL,将其配置为Webhook端点,发送测试短信后检查是否收到POST请求。

  **第四步:在发送请求中传递reportURL(可选)**

  部分服务商支持在单次发送请求中动态指定DLR回调地址。SMSGatewayCenter的发送API支持`reportURL`参数:

  ```

  reportURL=https://www.lanlansms.com/sms/

  ```

  这种方式适合多租户场景,不同客户可以使用不同的回调端点。

  四、Webhook接收端代码实现

  以下以Python Flask为例,展示一个生产级的Webhook接收端。

  ```python

from flask import Flask, request, jsonify
import logging
import hmac
import hashlib

app = Flask(__name__)
logger = logging.getLogger(__name__)

# 配置:DLR状态映射
DLR_STATUS_MAP = {
    "1": "DELIVERED",
    "2": "FAILED",
    "3": "PENDING",
    "4": "EXPIRED",
    "5": "REJECTED",
    "6": "DLT_SCRUBBING_FAILED",
    "7": "SUBMITTED_TO_OPERATOR"
}

@app.route('/sms/dlr', methods=['POST'])
def handle_dlr():
    """接收短信DLR回调"""
    try:
        # 1. 验证请求来源(推荐)
        # 实际生产环境应验证服务商签名或IP白名单
        payload = request.get_json(force=True)
        
        message_id = payload.get('messageId') or payload.get('message_id')
        status_code = str(payload.get('status') or payload.get('statusCode'))
        mobile = payload.get('mobile')
        operator = payload.get('operator')
        timestamp = payload.get('timestamp')
        
        # 2. 解析DLR状态
        status = DLR_STATUS_MAP.get(status_code, "UNKNOWN")
        
        # 3. 记录日志(用于审计和排查)
        logger.info(
            f"DLR received: msgId={message_id}, mobile={mobile}, "
            f"status={status}, operator={operator}, ts={timestamp}"
        )
        
        # 4. 更新业务数据库中的发送状态
        # update_sms_status(message_id, status, operator)
        
        # 5. 根据状态触发后续动作
        if status == "DLT_SCRUBBING_FAILED":
            # DLT清洗失败——模板不匹配或Sender ID未注册
            # 触发告警,检查DLT模板配置
            logger.error(f"DLT scrubbing failed for msgId={message_id}")
            # alert_dlt_config_issue(message_id)
            
        elif status == "FAILED" and operator:
            # 运营商级失败——可能需要降级到语音通道
            # trigger_voice_fallback(mobile)
            pass
            
        elif status == "DELIVERED":
            # 成功送达——更新统计
            pass
        
        # 6. 必须返回200状态码,否则服务商会持续重试
        return jsonify({"status": "ok"}), 200
        
    except Exception as e:
        logger.exception(f"DLR handling error: {e}")
        # 即使处理失败,也返回200避免重复推送
        return jsonify({"status": "error"}), 200


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

  ```

  **关键实现要点**:

  - **返回200状态码**:无论处理是否成功,必须返回HTTP 200。否则服务商会认为回调失败并持续重试,造成重复处理

  - **幂等处理**:同一消息ID可能收到多次回调(如状态从submitted变为delivered),需用消息ID+状态做去重

  - **DLT特定状态处理**:`DLT_SCRUBBING_FAILED`表示模板与备案不匹配或Sender ID未注册,需立即触发告警而非重试

  - **日志留存**:所有回调请求需完整记录,以备RBI审计和运营商投诉处理

  五、DLT特有的状态码解读

  印度DLT体系下的短信状态码与普通市场有显著差异。以下为常见DLT相关状态:

状态码 含义 是否可重试 处理建议
DELIVERED 已送达用户手机 正常完成
SUBMITTED_TO_OPERATOR 已提交至运营商(促销类短信的最终状态) 运营商不再返回DLR,视为完成
DLT_SCRUBBING_FAILED DLT清洗失败 不可重试 检查模板、Sender ID、Entity ID是否匹配
FAILED_DND 因DND拦截失败 促销类不可重试 促销短信不能发送至DND号码,改用Service Implicit模板
INVALID_TEMPLATE 模板ID无效或未注册 不可重试 确认模板已通过DLT审核
INVALID_ENTITY Entity ID无效 不可重试 确认Entity ID在DLT平台有效
INVALID_SENDER Sender ID未注册或格式错误 不可重试 确认6位Sender ID已关联到正确模板

  **DLT清洗失败的处理逻辑**:当收到`DLT_SCRUBBING_FAILED`状态时,**不要重试同一条消息**——重试同样会被拦截。正确的做法是:检查发送时的`msg`内容是否与备案模板逐字符一致,确认`dltTemplateId`和`dltEntityId`是否正确传递,修正后重新发送。

  六、Webhook失效的排查清单

问题现象 可能原因 排查方法
从未收到回调 Webhook URL未配置或配置错误 在控制台确认Webhook URL已保存
回调偶尔丢失 Webhook端点超时或返回非200 检查服务端响应时间,确保<5秒返回200
重复收到相同回调 未做幂等处理,服务商重试 用消息ID+状态做去重键
回调中DLT状态缺失 促销类短信运营商不返回DLR 促销类短信只能依赖submitted状态
Webhook被防火墙拦截 服务商IP未在白名单中 获取服务商出口IP并加入白名单

  七、总结

  印度短信API回调机制的核心要点可以概括为 **“Webhook为主、轮询为辅、DLT状态优先处理”** :

  1. **Webhook是首选方案**:实时性高、服务器压力低,适合OTP和交易类短信的即时状态获取

  2. **正确配置Webhook端点**:确保公网可访问、返回200、支持POST

  3. **DLT状态码必须识别**:`DLT_SCRUBBING_FAILED`不可重试,需立即排查模板和Sender ID配置

  4. **幂等处理是底线**:同一消息ID可能收到多次状态更新,去重逻辑不可省略

  5. **轮询作为回填机制**:Webhook丢失时用`getDlr`接口回填状态,确保状态完整性

  对于向印度发送短信的开发团队而言,**Webhook接收端的健壮性直接决定了消息状态的可追踪性**。建议从第一天就建立完整的DLR处理管道——包括幂等去重、DLT状态识别、告警触发和审计日志——而不是等到生产环境出问题后才补上。

本文链接:https://www.lanlansms.com/faq/764.html

联系我们--即刻申请免费测试账号

点击拨号:182-0071-8221