跳到正文
iRoamly Shop

Webhook 通知

使用 Webhook 在您的应用中接收二维码交付和续费结果。

Webhook 通知

简介

使用 Webhook 在您的应用中接收二维码交付和续费结果。

什么是 Webhook?

iRoamly 会针对二维码交付和续费结果发送 Webhook 通知。每个 Webhook 均使用 HTTPS,并包含 JSON 负载。您可以使用 Webhook:

  • 在兑换码转换为二维码或直接购买二维码完成后接收二维码信息。
  • 通过 Webhook 接收续费结果:续费二维码就绪时发送 esim.renewal.success,上游续费订单失败时发送 esim.renewal.failed。

如果您的端点未返回 2xx 状态,iRoamly 最多共尝试推送 5 次:第一次立即推送,之后分别在 30 秒、2 分钟、3 分钟和 5 分钟后重试。

接收 Webhook 的步骤

按照以下步骤,即可开始在您的应用中接收实时事件:

  • 创建用于接收请求的本地端点
  • 在 iRoamly Shop 管理后台的“开发者 > Webhook 通知”中注册您的 Webhook 端点

1. 创建用于接收请求的本地端点

在您的本地应用中,创建一个可接受 POST 请求的新路由。

例如,您可以在 Next.js 中添加一个 API 路由:

typescript
import type { NextApiRequest, NextApiResponse } from 'next';

export default (req: NextApiRequest, res: NextApiResponse) => {
  if (req.method === 'POST') {
    const payload = req.body;
    console.log(payload);
    res.status(200);
  }
};

收到事件后,应返回 HTTP 200 OK,以告知 iRoamly 事件已成功送达。

2. 注册您的开发用 Webhook 端点

在 iRoamly Shop 管理后台中注册您可公开访问的 HTTPS URL。

您可以使用 ngrok 等工具创建通往本地服务器的隧道。

管理后台的 Webhook 通知页面,已打开“新建 Webhook”面板:Webhook URL 字段及“已启用 / 已停用”状态

3. 测试您的 Webhook 端点是否正常工作

将一个测试兑换码转换为二维码,或直接购买一个二维码,检查您的 Webhook 端点是否收到事件。

Webhook 通知

事件类型

支持的事件类型及其负载列表。

事件类型发送时机备注
esim.qrcode兑换码已转换为二维码,或直接购买二维码已完成。未变更。isrenew 始终为 0。
esim.renewal.success续费成功,续费兑换码的二维码已就绪。新增。续费不再发送 esim.qrcode。
esim.renewal.failed因无法下达上游订单,续费失败。新增。仅在上游订单失败时发送。

推送

所有事件的推送方式相同:向订阅该事件的每个端点发送带 JSON 请求体的 POST 请求。任何 2xx 响应均视为已送达;否则会重试,最多共尝试 5 次(第一次立即推送,之后分别在 30 秒、2 分钟、3 分钟和 5 分钟后重试)。每个请求都携带以下请求头:

请求头说明类型
X-Webhook-Event事件类型:esim.qrcode、esim.renewal.success 或 esim.renewal.failed。String
X-Webhook-Delivery推送 ID。String
X-Webhook-Timestamp请求签名的时间,Unix 秒级时间戳。String
X-Webhook-Signaturesha256= 后接原始请求体的十六进制 HMAC-SHA256 值。String
X-Webhook-Signature-V2v1= 后接 "{timestamp}.{body}" 的十六进制 HMAC-SHA256 值。密钥轮换期间包含多个以逗号分隔的 v1= 值。String

签名校验方法请参阅“验证 Webhook 请求”。

esim.qrcode

兑换或购买 eSIM 二维码后返回的 eSIM 二维码相关信息。

请求体示例

json
{
	"code": 0,
	"data": {
		"activation_code": "CD112885573C49DDA42F64C905B925F4",
		"code": "UtTpoKCe769u2tt",
		"create_at": "2025-09-01 11:34:30",
		"isrenew": 0,
		"order_id": "ESIM202509021134343282",
		"plan_name": "Japan-eSIM-3GB-15 Days-Daily",
		"qrcode": "https://www.iroamly.com/qrcode/20250901/xSQfryvOWo0QUhsGcbpeTm9wWYfGu9BQ.png",
		"qrcode_content": "LPA:1$rsp1.cmlink.com$CD352885573C49DDA42F64C905B925F4",
		"sm_dp_address": "rsp1.cmlink.com"
	},
	"event_type": "esim.qrcode"
	}
Webhook 通知

esim.renewal.success

续费成功时发送:上游供应商返回续费订单的下载信息后,系统会为续费兑换码生成二维码,其 status 变为 2(已兑换)。

请求头

X-Webhook-Event:esim.renewal.success

与所有事件一样,该请求还携带 X-Webhook-Delivery、X-Webhook-Timestamp、X-Webhook-Signature 和 X-Webhook-Signature-V2(见“事件类型”)。

请求体示例

json
{
	"code": 0,
	"data": {
		"activation_code": "CD112885573C49DDA42F64C905B925F4",
		"code": "RnB7xTq2Lm9pQaZ",
		"create_at": "2026-10-28 09:12:41",
		"download_url": "LPA:1$rsp1.cmlink.com$CD352885573C49DDA42F64C905B925F4",
		"iccid": "8944501234567890123",
		"iscouldrenewal": 1,
		"isrenew": 1,
		"plan_name": "Japan-eSIM-3GB-15 Days-Daily",
		"qrcode": "https://www.iroamly.com/qrcode/20261028/xSQfryvOWo0QUhsGcbpeTm9wWYfGu9BQ.png",
		"qrcode_content": "LPA:1$rsp1.cmlink.com$CD352885573C49DDA42F64C905B925F4",
		"renew_order_id": "TG202610280912410001",
		"renewal_father_code": "UtTpoKCe769u2tt",
		"sm_dp_address": "rsp1.cmlink.com"
	},
	"event_type": "esim.renewal.success"
}

字段

字段说明类型
code始终为 0。Int
event_typeesim.renewal.successString
data.code此次续费生成的新兑换码。可用于“查询二维码”(qrcodeQuery)或再次续费。String
data.renewal_father_code被续费的原始(父)兑换码。String
data.renew_order_id上游续费订单 ID。String
data.plan_name续费套餐名称。String
data.qrcode二维码图片 URL。String
data.qrcode_contentLPA 字符串;可直接导入。String
data.sm_dp_addressSM-DP+ 地址。String
data.activation_code激活码。String
data.iccidICCID。可能为空。String
data.download_url上游供应商返回的下载字符串。String
data.create_at创建时间:服务器时间,格式为 YYYY-MM-DD HH:mm:ss。String
data.isrenew始终为 1。Int
data.iscouldrenewal新兑换码能否再次续费: 1 - 可续费 2 - 不可续费 3 - 已过期Int
Webhook 通知

esim.renewal.failed

续费失败时发送:上游供应商对续费订单返回了 0000 以外的代码,或向供应商发出的请求失败。

(注:上游供应商回调缺失或延迟不会触发此事件。请设置超时并回退为查询兑换码——见“处理续费事件”。)

请求头

X-Webhook-Event:esim.renewal.failed

与所有事件一样,该请求还携带 X-Webhook-Delivery、X-Webhook-Timestamp、X-Webhook-Signature 和 X-Webhook-Signature-V2(见“事件类型”)。

请求体示例

json
{
	"code": -1,
	"data": {
		"code": "RnB7xTq2Lm9pQaZ",
		"create_at": "2026-10-28 09:12:41",
		"error_msg": "TuGe_RenewOrderEx RespCode: 9999",
		"isrenew": 1,
		"plan_name": "Japan-eSIM-3GB-15 Days-Daily",
		"renewal_father_code": "UtTpoKCe769u2tt"
	},
	"event_type": "esim.renewal.failed"
}

字段

字段说明类型
code始终为 -1。Int
event_typeesim.renewal.failedString
data.code本次尝试生成的续费兑换码。该兑换码未开通,也没有二维码。String
data.renewal_father_code原计划续费的原始(父)兑换码。String
data.plan_name续费套餐名称。String
data.error_msg上游供应商返回的错误信息,用于排查问题。String
data.create_at创建时间:服务器时间,格式为 YYYY-MM-DD HH:mm:ss。String
data.isrenew始终为 1。Int
Webhook 通知

续费与 esim.qrcode

续费不再以 esim.qrcode 推送;该事件用于二维码转换和直接购买二维码。与 esim.qrcode 负载相比:

  • event_type 和 X-Webhook-Event 请求头为 esim.renewal.success 或 esim.renewal.failed,且 isrenew 为 1。
  • data.code 是续费生成的新兑换码;被续费的兑换码为 data.renewal_father_code。
  • esim.renewal.success 新增了 renew_order_id、renewal_father_code、iscouldrenewal、download_url 和 iccid,且不含 order_id。
  • 续费失败通过 esim.renewal.failed 报告,顶层 code 为 -1,上游错误信息位于 data.error_msg。
Webhook 通知

事件订阅

新端点默认订阅全部三种事件。现有端点会自动接收 esim.renewal.success 和 esim.renewal.failed——无需任何更改。

Webhook 通知

处理续费事件

  • 确保处理逻辑幂等。重试可能导致同一事件被多次推送,因此请按 X-Webhook-Delivery 或 event_type + data.code 去重,并忽略已处理过的事件。
  • 使用超时回退。如果在合理时间内既未收到 esim.renewal.success 也未收到 esim.renewal.failed,请使用查询 API(例如“查询二维码”)查询兑换码,而不要直接认定续费失败。
  • 不要根据 esim.renewal.failed 决定退款。该事件仅表示上游订单失败;是否以及何时退还点数另行决定,因此请勿根据此事件向您的客户退款。
Webhook 通知

验证 Webhook 请求

验证 Webhook 请求上的 iRoamly 签名。

如何验证 iRoamly 签名?

iRoamly 签名通过 Webhook 请求的 X-Webhook-Signature 请求头发送,格式为 sha256= 后接十六进制编码的签名。签名使用 HMAC-SHA256 算法生成,以 Webhook 密钥为密钥,以原始请求体(与收到时完全一致,未经任何 JSON 解析)为消息。

Webhook 请求头示例

json
{
'X-Webhook-Signature': 'sha256=dd7bdd2cf1f6bac6e171c6c508c157b7cd3cc1fd196394277fb59ba0bdd9b87b'
}

您可以在管理后台的“开发者 > Webhook 通知”中找到 Webhook 密钥。

管理后台的 Webhook 通知页面:Webhook 端点及其 URL、签名密钥(显示)以及推送记录

可使用以下代码片段生成签名:

typescript
import * as crypto from 'crypto';

  generateSignature(payload: string, secret: string): string {
    const computedSignature = crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');
    return 'sha256=' + computedSignature;
  }

在上面的代码片段中,payload 是原始请求体,secret 是 Webhook 密钥。只需将生成的签名(包括 sha256= 前缀)与 X-Webhook-Signature 请求头的值进行比较,即可完成验证。

带时间戳的签名(X-Webhook-Signature-V2)

每个请求还携带 X-Webhook-Signature-V2:v1= 后接使用同一 Webhook 密钥对 {timestamp}.{body}(即 X-Webhook-Timestamp 请求头的值、一个点号和原始请求体)计算的十六进制编码 HMAC-SHA256 值。

轮换 Webhook 密钥后,旧密钥在 24 小时内仍然有效。在此期间,X-Webhook-Signature 仍使用旧密钥签名,而 X-Webhook-Signature-V2 为每个密钥各携带一个值,以逗号分隔(v1=<old>,v1=<new>):只要任一值匹配,即可接受该请求。