Webhook 요청
Webhook 이벤트가 발생하면, Logto는 해당 이벤트에 구독된 모든 엔드포인트에 POST 요청을 보냅니다. 전체 이벤트 카탈로그는 Webhook 이벤트에서 확인할 수 있습니다. 이 페이지에서는 Logto가 전달하는 요청의 형태를 문서화합니다.
요청 헤더
| Key | Customizable | Notes |
|---|---|---|
| user-agent | ✅ | 기본값은 Logto (https://logto.io/) 입니다. |
| content-type | ✅ | 기본값은 application/json 입니다. |
| logto-signature-sha-256 | 요청 본문의 서명입니다. Webhook 보안 설정 참고. |
커스터마이즈 가능한 헤더는 보안 webhook 설정을 통해 오버라이드할 수 있습니다.
요청 본문 개요
본문은 JSON 객체입니다. 정확한 형태는 이벤트가 속한 패밀리에 따라 다릅니다:
| Family | Events | When it fires |
|---|---|---|
| 사용자 플로우 | PostRegister, PostSignIn, PostResetPassword | 사용자가 Experience API에서 회원가입, 로그인, 비밀번호 재설정 플로우를 완료할 때 발생합니다. |
| 데이터 변경 | User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* | Management API 호출 또는 Experience API의 사용자 플로우로 데이터 모델이 변경될 때 발생합니다. |
| 예외 | Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded | 보안 사고 발생 시, 예를 들어 연속된 인증 실패로 계정이 잠긴 경우 등. |
모든 패밀리는 공통 필드 집합을 공유합니다. 각 패밀리는 여기에 자체적인 요청 컨텍스트 필드와 이벤트별 페이로드를 추가합니다.
공통 필드
패밀리와 관계없이 모든 전달에 포함됩니다:
| Field | Type | Optional | Notes |
|---|---|---|---|
| hookId | string | Logto의 webhook 구성 식별자입니다. | |
| event | string | 이 전달을 트리거한 이벤트입니다. | |
| createdAt | string | ISO 8601 형식의 페이로드 생성 시간입니다. | |
| userAgent | string | ✅ | 트리거 요청의 user-agent입니다. |
각 패밀리에는 트리거 요청의 IP 주소도 포함됩니다. 사용자 플로우 이벤트에서는 userIp, 데이터 변경 및 예외 이벤트에서는 ip 필드명으로 제공됩니다. 의미는 동일하며, 과거 호환성을 위해 이름만 다릅니다.
사용자 플로우 이벤트 페이로드
이벤트: PostRegister, PostSignIn, PostResetPassword.
사용자가 Experience API에서 회원가입, 로그인, 비밀번호 재설정 플로우를 완료할 때 발생합니다. 공통 필드 외에, 본문에는 다음이 포함됩니다:
| Field | Type | Optional | Notes |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | 사용자 플로우 이벤트 타입입니다. 각각 PostSignIn / PostRegister / PostResetPassword에 매핑됩니다. 필드명은 과거 "interaction" 명칭을 유지합니다. | |
| sessionId | string | ✅ | 해당 이벤트의 세션 ID (Interaction ID 아님), 해당되는 경우에만 포함됩니다. |
| userIp | string | ✅ | 트리거 요청의 IP 주소입니다. |
| userId | string | ✅ | 해당 이벤트와 연관된 사용자 ID, 해당되는 경우에만 포함됩니다. |
| user | UserEntity | ✅ | 해당 이벤트와 연관된 사용자 엔티티, 해당되는 경우에만 포함됩니다. |
| applicationId | string | ✅ | 해당 이벤트와 연관된 애플리케이션 ID, 해당되는 경우에만 포함됩니다. |
| application | ApplicationEntity | ✅ | 해당 이벤트와 연관된 애플리케이션 엔티티, 해당되는 경우에만 포함됩니다. |
엔티티 형태
type UserEntity = {
id: string;
username?: string;
primaryEmail?: string;
primaryPhone?: string;
name?: string;
avatar?: string;
customData?: object;
identities?: object;
lastSignInAt?: string;
createdAt?: string;
applicationId?: string;
isSuspended?: boolean;
};
enum ApplicationType {
Native = 'Native',
SPA = 'SPA',
Traditional = 'Traditional',
MachineToMachine = 'MachineToMachine',
Protected = 'Protected',
SAML = 'SAML',
}
type ApplicationEntity = {
id: string;
type: ApplicationType;
name: string;
description?: string;
};
전체 필드 참조는 사용자 및 애플리케이션 문서를 참고하세요.
데이터 변경 이벤트 페이로드
이벤트: User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* 하위의 모든 이벤트. 전체 카탈로그는 Webhook 이벤트 → 데이터 변경 webhook 이벤트에서 확인하세요.
본문에는 항상 다음이 포함됩니다:
- 공통 필드
- 트리거 요청의 IP 주소를 담는
ip필드 (선택적, 알려진 경우에만 포함) - 변경이 어떻게 트리거되었는지 설명하는 API 컨텍스트. 트리거 소스에 따라 두 가지 중 하나입니다:
- Experience API 컨텍스트: 사용자 플로우에서 변경된 경우
- Management API 컨텍스트: 직접 Management API 호출로 변경된 경우
- 이벤트별 페이로드:
data에 영향을 받은 엔티티, 그리고 (일부 이벤트의 경우) 추가 최상위 필드. 이벤트별 데이터 페이로드 참고.
Experience API 컨텍스트 필드
Experience API의 사용자 플로우에서 변경이 트리거된 경우에 포함됩니다. 예: 회원가입 중 User.Created, 프로필 업데이트 중 User.Data.Updated 등.
| Field | Type | Optional | Notes |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | ✅ | 변경을 발생시킨 사용자 플로우 이벤트 타입. 필드명은 과거 "interaction" 명칭을 유지합니다. |
| sessionId | string | ✅ | 해당 이벤트의 세션 ID (Interaction ID 아님), 해당되는 경우에만 포함됩니다. |
| applicationId | string | ✅ | 해당되는 경우 애플리케이션 ID. |
| application | ApplicationEntity | ✅ | 해당되는 경우 애플리케이션 엔티티. |
Management API 컨텍스트 필드
Management API 호출로 변경이 트리거된 경우에 포함됩니다.
| Field | Type | Optional | Notes |
|---|---|---|---|
| path | string | ✅ | 이 webhook을 트리거한 API 호출의 경로입니다. |
| method | string | ✅ | API 호출의 HTTP 메서드입니다. |
| status | number | ✅ | API 호출의 응답 상태 코드입니다. |
| params | object | ✅ | API 호출의 koa path params입니다. |
| matchedRoute | string | ✅ | koa의 매칭된 라우트입니다. Logto는 이 필드를 사용해 활성화된 webhook 이벤트 필터와 매칭합니다. |
이벤트별 데이터 페이로드
모든 데이터 변경 이벤트에는 영향을 받은 엔티티를 담는 최상위 data 필드가 포함되며, 단일 엔티티로 요약할 수 없는 경우(삭제 및 멤버십 이벤트 등)에는 null이 됩니다. 일부 이벤트는 data 외에 추가 최상위 필드를 포함할 수 있습니다. Organization.Membership.Updated가 그 예로, 아래에 문서화되어 있습니다.
사용자 이벤트
| Event | Field | Type | Optional | Notes |
|---|---|---|---|---|
| User.Created | data | UserEntity | 생성된 사용자 엔티티. | |
| User.Data.Updated | data | UserEntity | 업데이트된 사용자 엔티티. | |
| User.Deleted | data | null | / |
역할 이벤트
type Role = {
id: string;
name: string;
description: string;
type: 'User' | 'MachineToMachine';
isDefault: boolean;
};
type Scope = {
id: string;
name: string;
description: string;
resourceId: string;
createdAt: number;
};
| Event | Field | Type | Optional | Notes |
|---|---|---|---|---|
| Role.Created | data | Role | 생성된 역할 엔티티. | |
| Role.Data.Updated | data | Role | 업데이트된 역할 엔티티. | |
| Role.Deleted | data | null | / | |
| Role.Scopes.Updated | data | Scope[] | 역할에 할당된 업데이트된 스코프. | |
| Role.Scopes.Updated | roleId | string | ✅ | 스코프가 할당된 역할 ID. (사전 할당된 스코프로 역할을 생성할 때만 제공) |
권한 (스코프) 이벤트
| Event | Field | Type | Optional | Notes |
|---|---|---|---|---|
| Scope.Created | data | Scope | 생성된 스코프 엔티티. | |
| Scope.Data.Updated | data | Scope | 업데이트된 스코프 엔티티. | |
| Scope.Deleted | data | null | / |
조직 이벤트
type Organization = {
id: string;
name: string;
description?: string;
customData: object;
createdAt: number;
};
| Event | Field | Type | Optional | Notes |
|---|---|---|---|---|
| Organization.Created | data | Organization | 생성된 조직 엔티티. | |
| Organization.Data.Updated | data | Organization | 업데이트된 조직 엔티티. | |
| Organization.Deleted | data | null | / | |
| Organization.Membership.Updated | data | null | / | 변경 사항은 선택적 최상위 델타 배열로 설명됩니다. 아래 Organization.Membership.Updated 페이로드 참고. |
Organization.Membership.Updated 페이로드
공통 필드 및 트리거 소스에 해당하는 API 컨텍스트 필드(Management API 컨텍스트 또는 Experience API 컨텍스트) 외에, Organization.Membership.Updated 이벤트는 organizationId와 선택적 델타 배열을 페이로드의 최상위에 포함합니다 (event, createdAt 등과 나란히, 항상 data 내부가 아닌, 이 이벤트의 경우 data는 항상 null).
| Field | Type | Optional | Notes |
|---|---|---|---|
| organizationId | string | 멤버십이 변경된 조직의 ID. | |
| addedUserIds | string[] | ✅ | 이번 트리거로 새로 추가된 사용자 ID. 추가된 사용자가 없거나, 트리거가 사용자 멤버십에 영향을 주지 않으면 생략됩니다. |
| removedUserIds | string[] | ✅ | 이번 트리거로 제거된 사용자 ID. 제거된 사용자가 없으면 생략됩니다. |
| addedApplicationIds | string[] | ✅ | 새로 추가된 애플리케이션 ID. 추가된 애플리케이션이 없거나, 트리거가 애플리케이션 멤버십에 영향을 주지 않으면 생략됩니다. |
| removedApplicationIds | string[] | ✅ | 제거된 애플리케이션 ID. 제거된 애플리케이션이 없으면 생략됩니다. |
네 가지 델타 배열은 선택적이며 추가적입니다. 즉, 이 배열이 없는 경우에도 기존 페이로드 형태는 변하지 않으며, 레거시 data: null 필드는 그대로 유지됩니다.
트리거별 델타 필드 예시
| Trigger | Possible delta fields |
|---|---|
POST /organizations/:id/users | addedUserIds |
PUT /organizations/:id/users | addedUserIds, removedUserIds |
DELETE /organizations/:id/users/:userId | removedUserIds |
POST /organizations/:id/applications | addedApplicationIds |
PUT /organizations/:id/applications | addedApplicationIds, removedApplicationIds |
DELETE /organizations/:id/applications/:applicationId | removedApplicationIds |
PUT /organization-invitations/:id/status (Accepted) | addedUserIds |
| Just-in-time provisioning when adding the user to a new organization | addedUserIds |
빈 델타는 생략됨 (없음 ≠ 빈 변경)
빈 델타 배열은 페이로드에서 완전히 생략됩니다. 예를 들어, 기존 멤버십과 동일한 집합으로 교체하는 PUT /organizations/:id/users는 실제 변경이 없으므로 페이로드는 { organizationId }만 남고 네 가지 델타 필드는 모두 빠집니다. 이미 멤버인 사용자를 다시 추가하거나 이미 멤버인 사용자가 초대를 다시 수락하는 경우도 마찬가지입니다.
컨슈머는 필드가 없는 경우 "해당 측면에 변경 없음"으로 간주해야 하며, "빈 변경"으로 해석해서는 안 됩니다.
배열별 최대값 (조용한 잘림)
각 델타 배열은 최대 5000개 항목으로 제한됩니다. 한 번의 Management API 호출로 5000명(또는 애플리케이션) 이상을 추가/제거하면 해당 델타 배열은 처음 5000개 항목까지만 조용히 잘립니다. 페이로드 내에 잘림 여부를 알리는 마커는 없습니다.
관리자 대량 작업이 한 번에 5000명 이상의 멤버에 영향을 줄 수 있다면, 배열 길이가 정확히 5000개일 때 Management API로 멤버십을 재조정해야 합니다:
GET /organizations/:id/users: 전체 사용자 멤버십GET /organizations/:id/applications: 전체 애플리케이션 멤버십
이 패턴은 GitHub의 push 이벤트(커밋 20개 제한, 전체 목록은 compare API로 안내)와 동일합니다.
무의미(no-op) 이벤트 건너뛰기
컨슈머 측에서 무의미한(no-op) 전달(델타 필드가 없는 이벤트)을 건너뛰려면, 델타 배열 존재 여부로 필터링하세요:
if (
payload.addedUserIds?.length ||
payload.removedUserIds?.length ||
payload.addedApplicationIds?.length ||
payload.removedApplicationIds?.length
) {
// 실제 멤버십 변경, 처리 필요
}
?.length는 undefined와 [] 모두에 대해 falsy이므로, 필드가 없거나(현재) 혹은 미래에 빈 배열로 나올 때도 동일하게 동작합니다.
페이로드 예시
사용자 추가 (POST /organizations/:id/users):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedUserIds": ["u_001"]
}
사용자 멤버십 집합 교체 (PUT /organizations/:id/users):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedUserIds": ["u_002"],
"removedUserIds": ["u_001"]
}
사용자 제거 (DELETE /organizations/:id/users/:userId):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_001"]
}
애플리케이션 추가 (POST /organizations/:id/applications):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedApplicationIds": ["app_xyz"]
}
기존 멤버 재추가, 무의미한 PUT, 이미 멤버인 사용자의 초대 재수락(실제 변경 없음):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc"
}
5000개 제한에 걸린 대량 작업(조용히 잘림):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_0001", "u_0002", "/* … 총 5000개 항목 */"]
}
정확히 5000개 항목이 있는 배열을 보면 GET /organizations/:id/users (또는 /applications)로 재조정해야 함을 의미합니다.
조직 역할 이벤트
type OrganizationRole = {
id: string;
name: string;
description?: string;
};
type OrganizationScope = {
id: string;
name: string;
description?: string;
};
| Event | Field | Type | Optional | Notes |
|---|---|---|---|---|
| OrganizationRole.Created | data | OrganizationRole | 생성된 조직 역할 엔티티. | |
| OrganizationRole.Data.Updated | data | OrganizationRole | 업데이트된 조직 역할 엔티티. | |
| OrganizationRole.Deleted | data | null | / | |
| OrganizationRole.Scopes.Updated | data | null | / | |
| OrganizationRole.Scopes.Updated | organizationRoleId | string | ✅ | 스코프가 할당된 역할 ID. (사전 할당된 스코프로 역할을 생성할 때만 제공) |
조직 권한(스코프) 이벤트
| Event | Field | Type | Optional | Notes |
|---|---|---|---|---|
| OrganizationScope.Created | data | OrganizationScope | 생성된 조직 스코프 엔티티. | |
| OrganizationScope.Data.Updated | data | OrganizationScope | 업데이트된 조직 스코프 엔티티. | |
| OrganizationScope.Deleted | data | null | / |
예외 이벤트 페이로드
이벤트: Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded.
보안 사고 발생 시, 예를 들어 연속된 인증 실패로 계정이 잠기거나, 앱의 동시 인증 기기 제한을 초과해 grant가 회수될 때 발생합니다.
모든 예외 이벤트는 공통 필드와 ip 필드(데이터 변경 이벤트와 동일한 형태)를 포함합니다. 나머지 필드는 이벤트에 따라 다릅니다.
Identifier.Lockout
사용자 플로우에서 발생하므로, 본문에는 Experience API 컨텍스트 필드도 포함되며, 추가로:
enum SignInIdentifier {
Email = 'email',
Phone = 'phone',
Username = 'username',
}
| Field | Type | Optional | Notes |
|---|---|---|---|
| type | SignInIdentifier | 사용자의 식별자 타입(이메일, 전화번호, 사용자명 등). | |
| value | string | 잠금이 발생한 사용자의 식별자 값. |
Message.RateLimited
사용자 플로우에서 발생하므로, 본문에는 Experience API 컨텍스트 필드도 포함되며, 추가로:
| Field | Type | Optional | Notes |
|---|---|---|---|
| action | string | 제한된 액션, 예: VerificationCodeSend. | |
| recipient | string | 발송 속도 제한에 걸린 이메일 주소 또는 전화번호. |
Grant.LimitExceeded
성공적인 인가 (Authorization)로 인해 사용자가 앱의 최대 동시 인증 기기 수 제한(maxAllowedGrants)을 초과하면 Logto가 해당 앱의 가장 오래된 grant를 회수하며 발생합니다.
이 이벤트는 Experience API가 아닌 OIDC 인가 엔드포인트에서 발생하므로, interactionEvent나 sessionId는 포함되지 않습니다. 공통 필드와 ip 외에, 본문에는 다음이 포함됩니다:
| Field | Type | Optional | Notes |
|---|---|---|---|
| userId | string | grant가 회수된 사용자 ID. | |
| applicationId | string | 제한을 초과한 애플리케이션 ID. | |
| application | ApplicationEntity | ✅ | 애플리케이션 엔티티. 전달 시점에 애플리케이션을 확인할 수 없으면 생략됩니다. |
| maxAllowedGrants | number | 이벤트 발생 시 애플리케이션에 설정된 제한값. | |
| preRevocationActiveGrantCount | number | 회수 전 사용자가 해당 애플리케이션에 보유한 활성 grant 수(방금 발급된 것 포함). | |
| revokedGrantIds | string[] | 실제로 회수된 grant의 ID(오래된 순). |
페이로드 예시:
{
"hookId": "hook_abc",
"event": "Grant.LimitExceeded",
"createdAt": "2024-01-01T00:00:00.000Z",
"ip": "192.168.0.1",
"userAgent": "Mozilla/5.0",
"userId": "u_001",
"applicationId": "app_xyz",
"application": {
"id": "app_xyz",
"type": "SPA",
"name": "My app",
"description": "My app description"
},
"maxAllowedGrants": 2,
"preRevocationActiveGrantCount": 3,
"revokedGrantIds": ["grant_001"]
}
전달 참고 사항:
- 회수된 grant 레코드는 회수와 함께 삭제되므로,
revokedGrantIds의 ID는 grant 목록 엔드포인트나 콘솔에서 더 이상 조회되지 않습니다(이들은 활성 grant만 반환). 페이로드 자체를 기록으로 삼고, 나중에 필요하다면 별도로 저장하세요. - 실제로 하나 이상의 grant가 회수될 때만 이벤트가 발생합니다. 제한 내에서 인가가 이루어지면 이벤트가 발생하지 않습니다.
- 제한을 초과하는 인가가 있을 때마다 이벤트가 발생하므로, 사용자가 허용된 기기 수보다 더 많은 기기로 반복 로그인하면 퇴출(eviction)마다 이벤트가 발생합니다.
- 전달은 fire-and-forget 방식입니다. 느리거나 실패하는 엔드포인트가 있어도 사용자의 인가가 차단되거나 실패하지 않습니다. 실패한 전달도 다른 webhook과 마찬가지로 감사 로그에 기록됩니다.