Webhooks request
When a webhook event fires, Logto sends a POST request to every endpoint subscribed to it. The full event catalog lives in Webhooks events; this page documents the shape of the request Logto delivers.
Request headers
| Key | Customizable | Notes |
|---|---|---|
| user-agent | ✅ | Logto (https://logto.io/) by default. |
| content-type | ✅ | application/json by default. |
| logto-signature-sha-256 | Signature of the request body. See securing your webhooks. |
Customizable headers can be overridden via the secure webhook configuration.
Request body overview
The body is a JSON object. Its exact shape depends on which family the event belongs to:
| Family | Events | When it fires |
|---|---|---|
| User flow | PostRegister, PostSignIn, PostResetPassword | A user completes a sign-up, sign-in, or password-reset flow handled by the Experience API. |
| Data mutation | User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* | The underlying data model is mutated by a Management API call or a user flow on the Experience API. |
| Exception | Identifier.Lockout | A security incident, for example an account locked after consecutive failed verification attempts. |
Every family shares a small set of common fields. Each family then layers on its own request-context fields plus an event-specific payload.
Common fields
Present in every delivery regardless of family:
| Field | Type | Optional | Notes |
|---|---|---|---|
| hookId | string | The webhook configuration identifier in Logto. | |
| event | string | The event that triggered this delivery. | |
| createdAt | string | The payload creation time in ISO 8601 format. | |
| userAgent | string | ✅ | The user-agent of the triggering request. |
Each family also includes the IP address of the triggering request, under the field name userIp for user-flow events and ip for data-mutation and exception events. The semantics are identical; the historical name difference is preserved for backward compatibility.
User flow event payloads
Events: PostRegister, PostSignIn, PostResetPassword.
Fired when a user completes a sign-up, sign-in, or password-reset flow handled by the Experience API. In addition to the common fields, the body carries:
| Field | Type | Optional | Notes |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | The user-flow event type. Maps to PostSignIn / PostRegister / PostResetPassword respectively. The field name retains historical "interaction" naming. | |
| sessionId | string | ✅ | The Session ID (not Interaction ID) for this event, if applicable. |
| userIp | string | ✅ | The IP address of the triggering request. |
| userId | string | ✅ | The User ID associated with this event, if applicable. |
| user | UserEntity | ✅ | The user entity associated with this event, if applicable. |
| applicationId | string | ✅ | The Application ID associated with this event, if applicable. |
| application | ApplicationEntity | ✅ | The application entity associated with this event, if applicable. |
Entity shapes
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;
};
See Users and Applications for the full field reference.
Data mutation event payloads
Events: every event under User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.*. See Webhooks events → Data mutation hook events for the complete catalog.
The body always carries:
- The common fields.
- An
ipfield, the IP address of the triggering request (optional, present when known). - An API context describing how the change was triggered. The context is one of two variants depending on the trigger source:
- Experience API context, when the change came from a user-facing flow.
- Management API context, when the change came from a direct Management API call.
- An event-specific payload: the affected entity in
dataand (for some events) additional top-level fields. See event-specific data payloads.
Experience API context fields
Present when the change was triggered by a user-facing flow on the Experience API, for example User.Created during sign-up or User.Data.Updated during profile updates.
| Field | Type | Optional | Notes |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | ✅ | The user-flow event type that produced the change. Field name retains historical "interaction" naming. |
| sessionId | string | ✅ | The Session ID (not Interaction ID) for this event, if applicable. |
| applicationId | string | ✅ | The Application ID, if applicable. |
| application | ApplicationEntity | ✅ | The application entity, if applicable. |
Management API context fields
Present when the change was triggered by a Management API call.
| Field | Type | Optional | Notes |
|---|---|---|---|
| path | string | ✅ | The path of the API call that triggered this hook. |
| method | string | ✅ | The HTTP method of the API call. |
| status | number | ✅ | The response status code of the API call. |
| params | object | ✅ | The koa path params of the API call. |
| matchedRoute | string | ✅ | The koa matched route. Logto uses this field to match enabled webhook event filters. |
Event-specific data payloads
Every data-mutation event includes a top-level data field carrying the affected entity, or null when the change can't be summarized as a single entity (delete and membership events). Some events also include event-specific top-level fields beyond data; Organization.Membership.Updated is one such case, documented below.
User events
| Event | Field | Type | Optional | Notes |
|---|---|---|---|---|
| User.Created | data | UserEntity | The created user entity. | |
| User.Data.Updated | data | UserEntity | The updated user entity. | |
| User.Deleted | data | null | / |
Role events
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 | The created role entity. | |
| Role.Data.Updated | data | Role | The updated role entity. | |
| Role.Deleted | data | null | / | |
| Role.Scopes.Updated | data | Scope[] | The updated scopes assigned to the role. | |
| Role.Scopes.Updated | roleId | string | ✅ | The role ID that scopes are assigned to. (Only available when the event was triggered by creating a role with pre-assigned scopes.) |
Permission (Scope) events
| Event | Field | Type | Optional | Notes |
|---|---|---|---|---|
| Scope.Created | data | Scope | The created scope entity. | |
| Scope.Data.Updated | data | Scope | The updated scope entity. | |
| Scope.Deleted | data | null | / |
Organization events
type Organization = {
id: string;
name: string;
description?: string;
customData: object;
createdAt: number;
};
| Event | Field | Type | Optional | Notes |
|---|---|---|---|---|
| Organization.Created | data | Organization | The created organization entity. | |
| Organization.Data.Updated | data | Organization | The updated organization entity. | |
| Organization.Deleted | data | null | / | |
| Organization.Membership.Updated | data | null | / | The change is described by optional top-level delta arrays. See Organization.Membership.Updated payload below. |
Organization.Membership.Updated payload
In addition to the common fields and the API-context fields that apply for the trigger source (Management API context for Management API routes, Experience API context for just-in-time provisioning), the Organization.Membership.Updated event carries an organizationId plus optional delta arrays at the top level of the payload (next to event, createdAt, etc., not inside data, which is always null for this event).
| Field | Type | Optional | Notes |
|---|---|---|---|
| organizationId | string | The organization whose membership changed. | |
| addedUserIds | string[] | ✅ | User IDs newly added by this trigger. Omitted when no users were added, or when the trigger does not affect user membership. |
| removedUserIds | string[] | ✅ | User IDs removed by this trigger. Omitted when no users were removed. |
| addedApplicationIds | string[] | ✅ | Application IDs newly added. Omitted when no applications were added, or when the trigger does not affect application membership. |
| removedApplicationIds | string[] | ✅ | Application IDs removed. Omitted when no applications were removed. |
The four delta arrays are optional and additive: they don't change the existing payload shape for consumers that don't expect them, and the legacy data: null field is still emitted unchanged.
Triggers and which delta fields they may emit
| 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 |
Empty deltas are omitted (absent ≠ empty change)
Empty delta arrays are omitted entirely from the payload. For example, a PUT /organizations/:id/users that replaces the membership set with the existing set produces no real change, and the payload reduces to just { organizationId } with all four delta fields absent. The same applies to a re-add of an existing member and a re-accept of an invitation by a user who is already a member.
Consumers must treat a missing field as "no change on that side," not as "an empty change."
Per-array cap (silent truncation)
Each delta array is capped at 5000 entries. When a single Management API call adds or removes more than 5000 users (or applications) in one operation, the corresponding delta array is silently truncated to its first 5000 entries. There is no in-payload marker that a cap fired.
If your application performs administrative bulk operations that can plausibly affect more than 5000 members in one call, treat an array of exactly 5000 entries as a signal to reconcile authoritative membership via the Management API:
GET /organizations/:id/users: full user membership.GET /organizations/:id/applications: full application membership.
This follows the same pattern as GitHub's push event, which caps commits at 20 entries and points consumers at the compare API for the full list.
Skipping no-op events
To skip no-op deliveries (events with no delta fields) on the consumer side, filter on delta-array presence:
if (
payload.addedUserIds?.length ||
payload.removedUserIds?.length ||
payload.addedApplicationIds?.length ||
payload.removedApplicationIds?.length
) {
// real membership change, handle it
}
?.length is falsy for both undefined and [], so the same predicate is robust whether the field is absent or (in some hypothetical future) emitted as an empty array.
Example payloads
Add a user (POST /organizations/:id/users):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedUserIds": ["u_001"]
}
Replace the user membership set (PUT /organizations/:id/users):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedUserIds": ["u_002"],
"removedUserIds": ["u_001"]
}
Remove a user (DELETE /organizations/:id/users/:userId):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_001"]
}
Add an application (POST /organizations/:id/applications):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedApplicationIds": ["app_xyz"]
}
Re-add an existing member, no-op PUT, or re-accept of an already-member invitation (no real change):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc"
}
Bulk operation that hits the 5000 cap (silently truncated):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_0001", "u_0002", "/* … exactly 5000 entries total */"]
}
Seeing an array of exactly 5000 entries should prompt a reconciling GET /organizations/:id/users (or /applications).
Organization role events
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 | The created organization role entity. | |
| OrganizationRole.Data.Updated | data | OrganizationRole | The updated organization role entity. | |
| OrganizationRole.Deleted | data | null | / | |
| OrganizationRole.Scopes.Updated | data | null | / | |
| OrganizationRole.Scopes.Updated | organizationRoleId | string | ✅ | The role ID that scopes are assigned to. (Only available when the event was triggered by creating a role with pre-assigned scopes.) |
Organization permission (scope) events
| Event | Field | Type | Optional | Notes |
|---|---|---|---|---|
| OrganizationScope.Created | data | OrganizationScope | The created organization scope entity. | |
| OrganizationScope.Data.Updated | data | OrganizationScope | The updated organization scope entity. | |
| OrganizationScope.Deleted | data | null | / |
Exception event payloads
Events: Identifier.Lockout.
Fired on security incidents, for example an account locked after consecutive failed verification attempts. These events always originate from a user-facing flow, so the body carries:
- The common fields.
- The
ipfield (same shape as data-mutation events). - The Experience API context fields.
- The exception-specific fields below.
enum SignInIdentifier {
Email = 'email',
Phone = 'phone',
Username = 'username',
}
| Field | Type | Optional | Notes |
|---|---|---|---|
| type | SignInIdentifier | The user's identifier type, e.g., email, phone or username. | |
| value | string | The user's identifier value that triggered the lockout. |