테마
Polling Events API
GET /api/v1/events?since=<cursor> 는 RP 가 자기 cursor 이후 발생한 모든 이벤트를 가져오는 catch-up 경로입니다. 웹훅이 first-class 라면 polling 은 누락 보정 이 1차 목적입니다.
Confidential client 전용
Polling events API 는 confidential client 전용 입니다 — HTTP Basic 으로 client_id:client_secret 인증을 요구합니다. Public / PKCE 만 쓰는 client (모바일 앱, SPA) 는 직접 호출할 수 없습니다.
Public-only RP 가 누락 보정이 필요하다면:
- 별도 confidential BFF (server-side) 를 운영해 거기서 polling 을 돌리거나,
- Webhook subscriptions 로 push 기반 수신만 사용하세요.
자세한 인증 정책은 Public Clients 와 Public vs Confidential 참고.
요청
GET /api/v1/events?since=evt_01HDxxxxxxxxxxxxxxxxxxxxxx&limit=100
Authorization: Basic base64(client_id:client_secret)client_id 는 logi_... 형태의 confidential application id, client_secret 은 콘솔에서 발급받은 값입니다. PAK (Personal API Key) 가 아니므로 헷갈리지 마세요.
파라미터
| 파라미터 | 필수 | 의미 |
|---|---|---|
since | 권장 | RP 가 마지막으로 처리한 event_id. 다음 page 는 이 id 직후부터 시작. 처음 호출 시 생략 → 최근 N 분 분의 이벤트만 반환 (history dump 방지). |
limit | 선택 | page 당 최대 row 수. 기본 100, 최대 1000. |
event_type | 선택 | user.merged, user.grants_revoked 등으로 filter. 여러 개 콤마 구분. 생략 시 전부. |
응답
json
{
"events": [
{
"event_id": "evt_01HE3...",
"event_type": "user.merged",
"occurred_at": "2026-05-11T12:34:56Z",
"data": { /* same shape as webhook payload */ }
},
...
],
"next_cursor": "evt_01HE9...",
"has_more": false
}events는occurred_at오름차순으로 정렬되며 동일 millisecond 내에서는event_id로 tiebreak.next_cursor는 다음 호출의since값.has_more=false면 현재로서는 보낼 게 없다는 의미.- 응답 페이로드의
data는 byte-for-byte 동일하지 않습니다 — polling 은 HMAC 서명을 함께 반환하지 않고 신뢰 경로가 다릅니다 (TLS + bearer auth). HMAC 검증이 필요한 이벤트는 webhook 으로 받는 것을 권장.
Cursor 의미
cursor 는 opaque 합니다. RP 는 받은 next_cursor 를 그대로 저장하고 다음 호출에 그대로 전달하면 됩니다. logi 가 내부 형식을 바꿔도 RP 는 영향을 받지 않습니다.
cursor 가 가리키는 이벤트는 포함되지 않습니다 (exclusive). 즉:
since=evt_X → returns events strictly AFTER evt_X따라서 RP 는 가장 마지막에 처리한 이벤트의 id 를 cursor 로 그대로 저장하면 안전합니다.
Rate Limits
| Client 유형 | RPS | Burst |
|---|---|---|
| 일반 RP | 5/s | 10 |
polling_intensive=true flag | 20/s | 40 |
Burst 초과 시 429 Too Many Requests + Retry-After 헤더. polling reconciler 는 보통 1분 단위면 충분하므로 일반 RP 한도로 여유 있게 처리 가능. polling-intensive flag 는 다중 region RP 또는 fan-out 이 필요한 RP 용이며 콘솔에서 요청.
Drift Recovery
RP 자기 DB 가 stale 하다고 의심될 때 (예: bug fix 후 재처리 필요) cursor 를 의도적으로 과거 시점으로 돌립니다:
ruby
# RP-side example — replay last 24h
since = LogiEvent.where("created_at > ?", 24.hours.ago).order(:created_at).first&.logi_event_id
events = LogiClient.events(since: since, limit: 1000)
events.each { |e| ApplyEventJob.perform_later(e) }이때 ApplyEventJob 는 반드시 idempotent 해야 합니다 — 같은 event_id 를 두 번 처리해도 효과가 동일해야 합니다. logi 의 webhook 도 retry 가 가능하므로 어차피 idempotent 가 강제됩니다.
Retention
logi 는 events 를 최소 90일 보관합니다. 그 이전 데이터까지 거슬러 올라가는 cursor 는 410 Gone 으로 응답하며 RP 는 full reconciliation (모든 user 의 userinfo 재조회) 으로 fallback 해야 합니다. 정상 운영 중인 RP 가 90일을 넘기는 경우는 incident 로 간주.
호출 예시 (Ruby)
ruby
class LogiPollingReconciler
def perform
cursor = LogiState.last_event_id
loop do
resp = http_get("/api/v1/events?since=#{cursor}&limit=200")
resp[:events].each { |e| ApplyEventJob.perform_later(e) }
cursor = resp[:next_cursor]
LogiState.update!(last_event_id: cursor) if cursor
break unless resp[:has_more]
end
end
endproduction 에서는 LogiState.update! 와 ApplyEventJob.perform_later 가 같은 transaction 안에 들어가야 cursor advance 와 작업 enqueue 가 atomic 하게 묶입니다. SolidQueue 같은 DB-backed job adapter 가 이 패턴에 적합합니다.