Webhook 事件
检测数据落库后,晓葆开放平台异步 POST plugin.report.ready 到宿主配置的 Webhook URL。 这是唯一的 Webhook 事件,订阅此事件即可获取全部检测类型的结果。
推送方式
| 项 | 说明 |
|---|---|
| 协议 | HTTPS POST,请求体 JSON |
| 触发时机 | 插件检测数据落库后异步触发(ECG / BCA 算法异步完成后触发) |
| 超时判定 | 宿主接口响应时间 > 10s,视为失败触发重试 |
| Webhook URL 配置 | 在晓葆开发者门户 → 应用设置中配置 |
业务处理耗时较长时,先返回 HTTP 200,再异步处理,避免触发重试。
请求 Header
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Webhook-Id: <UUID> # 本次投递唯一 ID,重试时保持不变(幂等键)
X-Webhook-Event: plugin.report.ready
X-Webhook-Signature: <HMAC-SHA256> # 验签用
X-Webhook-Timestamp: <Unix 秒>验签
signature = HMAC_SHA256(webhookSecret, X-Webhook-Timestamp + "." + rawBody)验签时使用原始请求体字节(rawBody),不要先 JSON.parse。
Node.js 示例
const crypto = require('crypto')
const express = require('express')
const app = express()
app.post('/webhook/xiaobao', express.raw({ type: '*/*' }), async (req, res) => {
const deliveryId = req.headers['x-webhook-id']
const timestamp = req.headers['x-webhook-timestamp']
const signature = req.headers['x-webhook-signature']
const rawBody = req.body.toString('utf8') // 原始字节转字符串,不 JSON.parse
// 1. 防重放:拒绝 5 分钟前的请求
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(400).end()
}
// 2. 验签
const expected = crypto
.createHmac('sha256', process.env.XIAOBAO_WEBHOOK_SECRET)
.update(timestamp + '.' + rawBody)
.digest('hex')
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).end()
}
// 3. 幂等:以 X-Webhook-Id 去重
if (await isDelivered(deliveryId)) return res.json({ code: 0 })
// 4. 处理事件
const payload = JSON.parse(rawBody)
if (payload.event === 'plugin.report.ready') {
const { measurement_no, third_user_id, report_type } = payload
await saveReport({ measurementNo: measurement_no, thirdUserId: third_user_id, reportType: report_type })
}
await markDelivered(deliveryId)
res.json({ code: 0 }) // 必须在 10s 内响应
})PHP 示例
function verifyWebhook(string $rawBody, array $headers, string $secret): bool {
$timestamp = $headers['X-Webhook-Timestamp'] ?? '';
$received = $headers['X-Webhook-Signature'] ?? '';
if (abs(time() - (int)$timestamp) > 300) {
return false; // 防重放
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $received);
}plugin.report.ready
检测报告就绪后触发,同步设备数据落库即触发;ECG / BCA 异步算法完成后触发。
Payload 字段
| 字段 | 类型 | 说明 |
|---|---|---|
event | string | 固定值 plugin.report.ready |
delivery_id | string | 幂等键,与 X-Webhook-Id 同值 |
app_id | int | 晓葆应用 ID |
measurement_no | string | 检测编号,与插件出参 measureId 对应 |
third_user_id | string|null | 宿主跳转时传入的第三方用户 ID |
xiaobao_user_id | int|null | 若宿主传了手机号完成绑定则有值,否则 null |
report_type | string | 见下方枚举 |
report_url | string|null | 当前版本为 null,后续版本补充 |
metadata | string|null | 宿主跳转时传入的自定义字符串,未传则为 null |
occurred_at | string | ISO 8601,如 2026-07-01T15:30:15+08:00 |
report_type 枚举
| report_type | 对应设备 | deviceKey |
|---|---|---|
blood_pressure | 多合一生化仪(血压) | HOME_MULTI_BIO_MONITOR |
blood_glucose | 多合一生化仪(血糖) | HOME_MULTI_BIO_MONITOR |
spo2 | 指环血氧仪 | HOME_SPO2_RING |
ecg | 心电记录仪 | HEARTLOG_ECG_RECORDER |
bca | 人体成分秤 | BODY_COMPOSITION_ANALYSIS |
Payload 示例
{
"event": "plugin.report.ready",
"delivery_id": "550e8400-e29b-41d4-a716-446655440000",
"app_id": 42,
"measurement_no": "PLG-20260701153012-ABCD12",
"third_user_id": "user_abc123",
"xiaobao_user_id": null,
"report_type": "blood_pressure",
"report_url": null,
"metadata": null,
"occurred_at": "2026-07-01T15:30:15+08:00"
}measurement_no 与插件出参 measureId 完全一致,可用于关联本地检测记录。third_user_id 即宿主跳转时传入的用户 ID,可直接识别用户。重试策略
| 次数 | 间隔 |
|---|---|
| 第 1 次重试 | 1 分钟后 |
| 第 2 次重试 | 5 分钟后 |
| 第 3 次重试 | 30 分钟后 |
| 第 4 次重试 | 2 小时后 |
| 第 5 次重试 | 6 小时后 |
5 次全部失败后写入死信队列,不再自动重试。请务必以 X-Webhook-Id 作为幂等键防止重复处理。