跳到主要内容

网络钩子

扩展 webhook 以 POST 请求的形式发送到您配置的 webhook URL。

标题:

  • Content-Type: application/json
  • x-webhook-event: <event-name>
  • x-webhook-token-hash: <sha256-hash>

顶层有效载荷形状:

{
"id": "uuid",
"event": "lock.time_changed",
"eventData": {},
"timestamp": "2026-02-16T01:23:45.678Z",
"app": {
"id": "app_id",
"slug": "my-app",
"displayName": "My App"
},
"sessionId": "extension_session_id",
"extension": {
"sessionId": "extension_session_id",
"enabled": true,
"config": {}
},
"wearer": {
"id": "wearer_user_id",
"username": "wearer_name"
},
"keyholder": {
"id": "keyholder_user_id",
"username": "keyholder_name"
},
"lock": {
"id": "lock_id",
"title": "Lock title",
"releaseAt": "2026-02-20T00:00:00.000Z",
"remainingSeconds": 12345,
"isLocked": true,
"isFrozen": false
}
}

配送、重试和订购

  • Webhook 会在后台排队并异步发送。
  • 交付采用 Redis 支持的队列,并带有用于崩溃恢复的在途处理列表。
  • 处理过程持续进行(在主工作节点上大约每秒一次)。
  • 每次HTTP请求的超时时间约为3秒。

重试策略:

  • 当出现以下情况时,将重试投递:
  • 请求超时或出现网络错误
  • 端点返回 429
  • 端点返回任何 5xx 状态
  • 对于其他 4xx 状态,不会重试投递。
  • 退避延迟为:15s60s150s600s

对系统集成商的启示:

  • 交付至少一次,而不是恰好一次。你的端点应该是幂等的。
  • 排序仅供参考。请勿假定所有事件都遵循严格的全局顺序。
  • 返回快速的 2xx 响应,并异步处理您自己的繁重工作。
  • 您可以使用有效载荷 id 作为去重密钥。

事件数据

lock.time_changed

{
"event": "lock.time_changed",
"eventData": {
"source": "keyholder",
"beforeReleaseAt": "2026-02-20T00:00:00.000Z",
"afterReleaseAt": "2026-02-20T01:00:00.000Z",
"requestedDeltaSeconds": 3600,
"appliedDeltaSeconds": 3600
}
}

笔记:

  • requestedDeltaSeconds 是请求的操作代码。
  • 实际持久化的是 appliedDeltaSeconds
  • 如果无法从发布时间戳中推导出 appliedDeltaSeconds,则可以是 null

lock.frozen / lock.unfrozen

{
"event": "lock.frozen",
"eventData": {
"source": "keyholder",
"durationSeconds": 3600,
"beforeFrozen": false,
"afterFrozen": true
}
}
{
"event": "lock.unfrozen",
"eventData": {
"source": "dice",
"durationSeconds": 900,
"beforeFrozen": true,
"afterFrozen": false
}
}

task.* 事件

task.assigned

{
"event": "task.assigned",
"eventData": {
"runId": "tr_1739665200000",
"taskId": "task_123",
"status": "active",
"source": "keyholder",
"overriddenRunId": null
}
}

task.completed

{
"event": "task.completed",
"eventData": {
"runId": "tr_1739665200000",
"taskId": "task_123",
"status": "completed",
"pointsAwarded": 10
}
}

task.failed

{
"event": "task.failed",
"eventData": {
"runId": "tr_1739665200000",
"taskId": "task_123",
"status": "failed",
"penaltyApplied": true,
"outcomeDescription": "1 hour added"
}
}

hygiene.* 事件

hygiene.started

{
"event": "hygiene.started",
"eventData": {
"startedAt": "2026-02-16T02:00:00.000Z",
"expiresAt": "2026-02-16T02:30:00.000Z",
"windowSeconds": 1800,
"penaltySeconds": 3600
}
}

hygiene.resumed

{
"event": "hygiene.resumed",
"eventData": {
"completedAt": "2026-02-16T02:20:00.000Z",
"lateMinutes": 0,
"penaltyApplied": false,
"punishmentSeconds": 0,
"penaltyMode": null,
"cardsAdjusted": 0,
"actualDeltaSeconds": null
}
}

hygiene.resumed_late

{
"event": "hygiene.resumed_late",
"eventData": {
"completedAt": "2026-02-16T02:45:00.000Z",
"lateMinutes": 15,
"penaltyApplied": true,
"punishmentSeconds": 3600,
"penaltyMode": "time",
"cardsAdjusted": 0,
"actualDeltaSeconds": 3600
}
}

verification.picture_submitted

{
"event": "verification.picture_submitted",
"eventData": {
"verificationId": "verification_id",
"date": "2026-02-16",
"submittedAt": "2026-02-16T03:10:00.000Z",
"photoUrl": "https://cdn.example.com/locks/..."
}
}

dice.rolled

{
"event": "dice.rolled",
"eventData": {
"userTotalScore": 14,
"botTotalScore": 8,
"difference": 6,
"requestedAdjustmentSeconds": -3600,
"appliedAdjustmentSeconds": -3600,
"cardGameEnabled": false
}
}

lock.archive_removal

{
"event": "lock.archive_removal",
"eventData": {
"source": "archive",
"actorRole": "wearer",
"lockId": "lock_id"
}
}

笔记:

  • sourcedeletearchive
  • actorRolewearerkeyholder