Webhooks 请求
当一个 webhook 事件被触发时,Logto 会向每个已订阅该事件的端点发送一个 POST 请求。完整的事件目录见 Webhooks 事件;本页文档介绍 Logto 发送的请求结构。
请求头
| Key | 可自定义 | 说明 |
|---|---|---|
| user-agent | ✅ | 默认值为 Logto (https://logto.io/)。 |
| content-type | ✅ | 默认值为 application/json。 |
| logto-signature-sha-256 | 请求体的签名。详见保护你的 webhooks。 |
可自定义的请求头可以通过 安全 webhook 配置进行覆盖。
请求体概览
请求体是一个 JSON 对象。其具体结构取决于事件所属的类别:
| Family | Events | 触发时机 |
|---|---|---|
| 用户流程 | PostRegister, PostSignIn, PostResetPassword | 用户完成由 Experience API 处理的注册、登录或重置密码流程时触发。 |
| 数据变更 | User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* | 通过 Management API 调用或 Experience API 上的用户流程导致底层数据模型发生变更时触发。 |
| 异常 | Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded | 安全事件,例如连续验证失败后账户被锁定等。 |
每个类别都包含一组通用字段。每个类别还会叠加自身的请求上下文字段以及事件特定的 payload。
通用字段
无论属于哪个类别,每次投递都会包含以下字段:
| Field | Type | Optional | 说明 |
|---|---|---|---|
| hookId | string | Logto 中 webhook 配置的标识符。 | |
| deliveryId | string | 本次投递的唯一标识符,每个 webhook 和事件唯一,并在自动重试期间保持不变。详见去重投递。 | |
| event | string | 触发本次投递的事件。 | |
| createdAt | string | payload 创建时间,ISO 8601 格式。 | |
| userAgent | string | ✅ | 触发请求的 user-agent。 |
每个类别还会包含触发请求的 IP 地址:用户流程事件下字段名为 userIp,数据变更和异常事件下为 ip。语义一致,仅为历史兼容保留不同命名。
去重投递
一次投递可能会多次到达你的端点:Logto 会重试响应状态为 500 及以上的请求,因此即使你的端点已处理请求但仍返回 5xx(例如前置网关因处理慢返回 504),也会再次收到请求。每次相同投递的尝试都携带相同的 deliveryId,而新事件或同一事件投递到另一个 webhook 时会获得新的 deliveryId。请记录你已处理的每个 payload 的 deliveryId,并跳过已知 id 的 payload。
deliveryId 也是记录该投递的审计日志条目的 id,因此你的服务上报的值可以在 Console 的 webhook 最近请求中打开,或通过 GET /api/logs/:id 获取。
测试 payload 也会携带 deliveryId,因此同一个 handler 可以处理它,尽管测试请求不会被记录到日志中。
用户流程事件 payload
事件: PostRegister, PostSignIn, PostResetPassword。
当用户完成由 Experience API 处理的注册、登录或重置密码流程时触发。除了通用字段外,请求体还包含:
| Field | Type | Optional | 说明 |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | 用户流程事件类型。分别对应 PostSignIn / PostRegister / PostResetPassword。字段名保留历史“interaction”命名。 | |
| sessionId | string | ✅ | 本事件的 Session 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;
};
数据变更事件 payload
事件: 所有 User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* 下的事件。完整目录见 Webhooks 事件 → 数据变更 webhook 事件。
请求体始终包含:
- 通用字段。
- 一个
ip字段,表示触发请求的 IP 地址(可选,已知时存在)。 - 一个API 上下文,描述变更的触发方式。上下文根据触发来源有两种变体:
- Experience API 上下文:变更来自用户端流程时。
- Management API 上下文:变更来自直接的 Management API 调用时。
- 一个事件特定 payload:
data字段中的受影响实体,以及(部分事件)额外的顶层字段。详见事件特定数据 payload。
Experience API 上下文字段
当变更由 Experience API 上的用户端流程触发时存在,例如注册时的 User.Created 或资料更新时的 User.Data.Updated。
| Field | Type | Optional | 说明 |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | ✅ | 产生变更的用户流程事件类型。字段名保留历史“interaction”命名。 |
| sessionId | string | ✅ | 本事件的 Session ID(非 Interaction ID),如适用。 |
| applicationId | string | ✅ | 应用程序 ID,如适用。 |
| application | ApplicationEntity | ✅ | 应用程序实体,如适用。 |
Management API 上下文字段
当变更由 Management API 调用触发时存在。
| Field | Type | Optional | 说明 |
|---|---|---|---|
| path | string | ✅ | 触发本 webhook 的 API 调用路径。 |
| method | string | ✅ | API 调用的 HTTP 方法。 |
| status | number | ✅ | API 调用的响应状态码。 |
| params | object | ✅ | API 调用的 koa 路径参数。 |
| matchedRoute | string | ✅ | koa 匹配到的路由。Logto 用于匹配 webhook 事件过滤器。 |
事件特定数据 payload
每个数据变更事件都包含一个顶层 data 字段,携带受影响的实体;若变更无法归纳为单一实体(如删除和成员变更事件),则为 null。部分事件还会包含除 data 外的事件特定顶层字段;Organization.Membership.Updated 就是其中之一,见下文。
用户事件
| Event | Field | Type | Optional | 说明 |
|---|---|---|---|---|
| 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 | 说明 |
|---|---|---|---|---|
| Role.Created | data | Role | 新创建的角色实体。 | |
| Role.Data.Updated | data | Role | 更新后的角色实体。 | |
| Role.Deleted | data | null | / | |
| Role.Scopes.Updated | data | Scope[] | 分配给该角色的更新后权限 (Scopes)。 | |
| Role.Scopes.Updated | roleId | string | ✅ | 分配权限 (Scopes) 的角色 ID。(仅在通过预分配权限创建角色时可用。) |
权限 (Scope) 事件
| Event | Field | Type | Optional | 说明 |
|---|---|---|---|---|
| Scope.Created | data | Scope | 新创建的权限 (Scope) 实体。 | |
| Scope.Data.Updated | data | Scope | 更新后的权限 (Scope) 实体。 | |
| Scope.Deleted | data | null | / |
组织 (Organization) 事件
type Organization = {
id: string;
name: string;
description?: string;
customData: object;
createdAt: number;
};
| Event | Field | Type | Optional | 说明 |
|---|---|---|---|---|
| Organization.Created | data | Organization | 新创建的组织 (Organization) 实体。 | |
| Organization.Data.Updated | data | Organization | 更新后的组织 (Organization) 实体。 | |
| Organization.Deleted | data | null | / | |
| Organization.Membership.Updated | data | null | / | 变更通过可选的顶层 delta 数组描述。见下文 Organization.Membership.Updated payload。 |
Organization.Membership.Updated payload
除了通用字段和适用于触发来源的 API 上下文字段(Management API 路由为 Management API 上下文,即时供应为 Experience API 上下文),Organization.Membership.Updated 事件还会在 payload 顶层(与 event、createdAt 等并列,不在 data 内,该事件下 data 始终为 null)携带 organizationId 及可选的 delta 数组。
| Field | Type | Optional | 说明 |
|---|---|---|---|
| organizationId | string | 发生成员变更的组织 (Organization)。 | |
| addedUserIds | string[] | ✅ | 本次触发新增的用户 ID。未新增用户或不影响用户成员时省略。 |
| removedUserIds | string[] | ✅ | 本次触发移除的用户 ID。未移除用户时省略。 |
| addedApplicationIds | string[] | ✅ | 新增的应用程序 ID。未新增应用程序或不影响应用成员时省略。 |
| removedApplicationIds | string[] | ✅ | 移除的应用程序 ID。未移除应用程序时省略。 |
这四个 delta 数组可选且增量:对于不期望它们的消费方,不会改变现有 payload 结构,且历史的 data: null 字段仍然保持不变。
触发方式及可能出现的 delta 字段
| Trigger | 可能出现的 delta 字段 |
|---|---|
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 |
| 即时供应将用户加入新组织时 | addedUserIds |
空 delta 会被省略(缺失 ≠ 空变更)
空的 delta 数组会完全省略在 payload 中。例如,将成员集替换为原有集的 PUT /organizations/:id/users 不会产生实际变更,payload 仅为 { organizationId },四个 delta 字段均缺失。对于重复添加已存在成员、已是成员的用户重复接受邀请等情况同理。
消费方必须将缺失字段视为“该侧无变更”,而不是“空变更”。
单数组上限(静默截断)
每个 delta 数组最多5000 项。当一次 Management API 调用在单次操作中新增或移除超过 5000 个用户(或应用程序)时,相应的 delta 数组会被静默截断为前 5000 项。payload 内不会有任何标记说明已触发上限。
如果你的应用有可能在一次操作中批量管理超过 5000 个成员,请将数组长度正好为 5000 视为需要通过 Management API 对成员进行权威同步的信号:
GET /organizations/:id/users:获取完整用户成员列表。GET /organizations/:id/applications:获取完整应用成员列表。
这与 GitHub 的 push 事件对 commits 的 20 项上限及建议使用 compare API 获取完整列表的模式一致。
跳过无操作事件
要在消费端跳过无操作(无 delta 字段)投递,可按 delta 数组是否存在过滤:
if (
payload.addedUserIds?.length ||
payload.removedUserIds?.length ||
payload.addedApplicationIds?.length ||
payload.removedApplicationIds?.length
) {
// 有实际成员变更,处理之
}
?.length 对于 undefined 和 [] 都为 falsy,因此无论字段缺失还是(假设将来)发出空数组,该判断都可靠。
示例 payload
新增用户(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 | 说明 |
|---|---|---|---|---|
| OrganizationRole.Created | data | OrganizationRole | 新创建的组织角色实体。 | |
| OrganizationRole.Data.Updated | data | OrganizationRole | 更新后的组织角色实体。 | |
| OrganizationRole.Deleted | data | null | / | |
| OrganizationRole.Scopes.Updated | data | null | / | |
| OrganizationRole.Scopes.Updated | organizationRoleId | string | ✅ | 分配权限 (Scopes) 的角色 ID。(仅在通过预分配权限创建角色时可用。) |
组织权限 (scope) 事件
| Event | Field | Type | Optional | 说明 |
|---|---|---|---|---|
| OrganizationScope.Created | data | OrganizationScope | 新创建的组织权限 (scope) 实体。 | |
| OrganizationScope.Data.Updated | data | OrganizationScope | 更新后的组织权限 (scope) 实体。 | |
| OrganizationScope.Deleted | data | null | / |
异常事件 payload
事件: Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded。
在安全事件发生时触发,例如连续验证失败后账户被锁定,或因应用的并发设备数超限而撤销授权。
每个异常事件都包含通用字段和一个 ip 字段(结构同数据变更事件)。其余字段依事件而异。
Identifier.Lockout
来源于用户端流程,因此请求体还包含 Experience API 上下文字段,以及:
enum SignInIdentifier {
Email = 'email',
Phone = 'phone',
Username = 'username',
}
| Field | Type | Optional | 说明 |
|---|---|---|---|
| type | SignInIdentifier | 用户标识类型,如 email、phone 或用户名。 | |
| value | string | 触发锁定的用户标识值。 |
Message.RateLimited
来源于用户端流程,因此请求体还包含 Experience API 上下文字段,以及:
| Field | Type | Optional | 说明 |
|---|---|---|---|
| action | string | 被限流的操作,例如 VerificationCodeSend。 | |
| recipient | string | 触发发送限流的邮箱或手机号。 |
Grant.LimitExceeded
当一次成功授权使用户超出应用的最大并发认证设备数限制(maxAllowedGrants)并导致 Logto 撤销其最早的授权时触发。
该事件由 OIDC 授权端点发出,而非 Experience API,因此不包含 interactionEvent 或 sessionId。除通用字段和 ip 外,请求体还包含:
| Field | Type | Optional | 说明 |
|---|---|---|---|
| userId | string | 被撤销授权的用户。 | |
| applicationId | string | 超出 maxAllowedGrants 限制的应用程序。 | |
| application | ApplicationEntity | ✅ | 应用程序实体。若投递时无法解析应用则省略。 |
| maxAllowedGrants | number | 事件触发时应用配置的限制。 | |
| preRevocationActiveGrantCount | number | 撤销前用户在该应用下持有的活跃授权数(包括刚发放的这一个)。 | |
| revokedGrantIds | string[] | 实际被撤销的授权 ID,按最早顺序排列。 |
示例 payload:
{
"hookId": "hook_abc",
"deliveryId": "3nfp8xk1q2z7",
"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"]
}
投递说明:
- 被撤销的授权记录会在撤销时被销毁,因此
revokedGrantIds中的 ID 无法再通过授权列表接口或 Console 查询到——这些接口只返回活跃授权。请将 payload 作为记录,如需后续使用请自行持久化这些 ID。 - 仅当实际撤销了授权时才会触发事件。若授权未超限则不会产生事件。
- 每次授权超限都会触发一次事件,因此用户多设备重复登录超限会产生多次撤销事件。
- 投递为“即发即弃”:慢或失败的端点不会阻塞或影响用户授权。失败的投递会像其他 webhook 一样记录到审计日志中。