DEVELOPER DOCS / API V1

Webhook 接入与签名

以冻结的 v1 契约接入通信数据。示例均为合成结构,实际结果以当前数据集与账户权限为准。

创建订阅

所有 Webhook 管理接口需要账户 Cookie 会话并实施租户隔离。目标须为公网 HTTPS,禁止私网、本机、云元数据地址与重定向绕过。签名密钥只在创建时返回一次。

POST /console/webhooks
{
  "url": "https://your-domain.com/webhooks/ir21",
  "events": [
    "ir21.number_range.updated"
  ],
  "countries": [
    "CN"
  ],
  "operators": []
}
接口请求 / 返回 data
GET /console/webhooks{items:WebhookSubscription[]}
POST /console/webhooksbody {url,events,countries?,operators?} → {item,secret}
PATCH /console/webhooks/:idbody {url?,events?,countries?,operators?,enabled?} → {item}
DELETE /console/webhooks/:id{deleted:true}
POST /console/webhooks/:id/test{delivery_id,queued:true},测试事件标记 test:true。
GET /console/webhooks/:id/deliveries?cursor&limit&status&event_id{items:WebhookDelivery[],total,next_cursor}; limit=1–100
GET /console/webhooks/:id/deliveries/:deliveryId{item,attempts:[{id,attempted_at,response_status,error}],event}
POST /console/webhooks/:id/deliveries/retry-batchbody {delivery_ids:string[]} (1–20) → {queued,delivery_ids}
POST /console/webhooks/:id/rotate-secretbody {grace_seconds?:0–86400} → {item,secret,previous_secret_expires_at}
DELETE /console/webhooks/:id/previous-secret{revoked:true}
POST /console/webhooks/:id/deliveries/:deliveryId/retry{queued:true},保持相同 event ID。

事件类型与载荷

ir21.number_range.added|removed|updatedir21.operator.added|removed|updatedir21.network.added|removed|updatedir21.capability.changedir21.catalog.updated

JSON
{
  "id": "example-event",
  "type": "ir21.number_range.updated",
  "occurred_at": "2026-09-08T00:00:00Z",
  "effective_at": null,
  "dataset_version": "example-v2",
  "previous_version": "example-v1",
  "country_iso2": "CN",
  "operator_id": "example-operator",
  "data": {
    "summary": "合成事件,仅用于签名演示"
  },
  "test": true
}

签名验证

Header含义
X-IR21-Event-Id稳定事件 ID;接收端以此去重。
X-IR21-TimestampUnix 秒,用于检查接收时间窗口。
X-IR21-Signaturev1=<hex>;HMAC-SHA256(secret, `${timestamp}.${rawBody}`)。

先保留未经重新序列化的原始 body 字节,再验证签名。建议 5 分钟窗口,采用常量时间比较。验证通过后再解析 JSON、去重并处理。

NODE.JS / 接收端验证示例
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(rawBody, headers, secrets) {
  const timestamp = headers['x-ir21-timestamp'];
  if (typeof timestamp !== 'string' || !/^\d+$/.test(timestamp)) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(Date.now() / 1000 - seconds) > 300) return false;
  const signatures = [headers['x-ir21-signature'], headers['x-ir21-signature-previous']]
    .filter(value => typeof value === 'string' && /^v1=[a-f0-9]{64}$/i.test(value));
  return secrets.filter(Boolean).some(secret => {
    const expected = createHmac('sha256', secret)
      .update(timestamp + '.').update(rawBody).digest();
    return signatures.some(signature => {
      const received = Buffer.from(signature.slice(3), 'hex');
      return received.length === expected.length && timingSafeEqual(expected, received);
    });
  });
}

// rawBody 使用原始 Buffer;宽限期结束后移除本地旧密钥。
// 验签通过后核对 header event ID 与 body.id,并在事务中去重后处理。

投递、失败与恢复

轮换前先部署支持双签名的接收端。宽限期默认 1 小时,最长 24 小时,可选立即失效。新密钥对应 X-IR21-Signature;旧密钥在宽限期内对应 X-IR21-Signature-Previous。接收端可验证任一有效签名,到期后移除本地旧密钥;如果疑似泄露,应立即撤销旧密钥。一次只允许一组旧密钥宽限期。API Key 使用“创建新 Key→替换服务配置并验证调用→撤销旧 Key”的重叠轮换流程。

仓库 examples/webhook-receiver.mjs 提供完整接收端:使用 CELRYN_WEBHOOK_SECRET 和私有 CELRYN_WEBHOOK_INBOX。如设置旧密钥 CELRYN_WEBHOOK_PREVIOUS_SECRET,必须同时设置固定的 ISO 到期时间 CELRYN_WEBHOOK_PREVIOUS_SECRET_EXPIRES_AT,重启不会延长宽限期。它监听 127.0.0.1:4200 的 POST /webhooks/celryn,生产置于 HTTPS 反向代理后;在验签、事件 ID 去重与原子入箱后才返回成功。通知应触发同步补拉,不按可能乱序到达的通知直接覆写目录。

ir21.catalog.updated 表示全局重置,即使设置了地区或运营商筛选也必须处理。增量查询的 category 筛选同样不能屏蔽它;来源元数据变化可能要求重新获取完整快照。

WebhookDelivery 包含 id,event_id,event_type,status,attempts,response_status,last_error,created_at,next_attempt_at。状态为 pending / delivered / failed / cancelled。队列成功只表示已排队,不代表目的地收到。

平台执行有限超时和重试,并持久化结果。停用订阅会停止待发任务;手动重投仍保留原 event ID。接收端应快速应答成功状态,后续处理独立入队。若通知遗漏,使用 /v1/changes 按版本补拉,恢复时检查事件顺序和生效时间。

响应、鉴权与错误

数据接口支持X-API-Key 或同源 Cookie 会话。匿名访问为受限公共投影,完整字段权限由 API 服务执行。JSON 成功响应统一为{code:0,data,meta};文件导出直接返回文件。

字段说明
meta.request_id本次请求标识,用于排查错误,不包含密钥。
meta.dataset_version / source_mode当前数据版本与 demo / ir21 / reference / mixed 来源模式。
meta.source_published_at源发布时间;未知为 null。
meta.ingested_at平台导入时间,ISO 格式 UTC。

400 表示参数不符,401 表示需要有效凭证,403 表示权限不足,429 表示调用限制,503 表示服务或数据源暂不可用。请读取错误响应的 codemessagerequest_id,详见错误与重试

ERROR / 示意结构
{
  "code": "EXAMPLE_ERROR_CODE",
  "message": "具体错误原因由服务端返回",
  "request_id": "example-request-id"
}