웹훅
확장 프로그램 웹훅은 구성된 웹훅 URL로 POST 요청으로 전송됩니다.
헤더:
Content-Type: application/jsonx-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
}
}
배송, 재시도 및 주문
- 웹훅은 대기열에 추가되어 백그라운드에서 비동식으로 전달됩니다.
- 배송은 Redis 기반 큐를 사용하며, 오류 복구를 위해 처리 중인 목록을 관리합니다.
- 처리 작업은 지속적으로 실행됩니다(기본 작업자에서 약 1초에 한 번).
- HTTP 시도당 시간 초과는 약 3초입니다.
재시도 정책:
- 다음과 같은 경우 배송이 재시도됩니다.
- 요청 시간이 초과되었거나 네트워크 오류가 발생했습니다.
- 엔드포인트는
429를 반환합니다. - 엔드포인트는
5xx상태를 반환합니다. - 다른
4xx상태에 대해서는 배송 재시도가 수행되지 않습니다. - 백오프 지연은
15s,60s,150s,600s입니다.
시스템 통합업체에 대한 시사점:
- 데이터 전송은 최소 한 번 발생해야 하며, 정확히 한 번 발생하는 것은 아닙니다. 엔드포인트는 멱등성을 가져야 합니다.
- 순서는 최대한 정확하게 유지하려고 노력하는 것입니다. 모든 이벤트에 걸쳐 엄격한 순서가 적용된다고 가정하지 마십시오.
- 빠른
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"
}
}
참고:
source는delete또는archive입니다.actorRole는wearer또는keyholder입니다.