Solicitud de Webhooks
Cuando se dispara un evento de webhook, Logto envía una solicitud POST a cada endpoint suscrito a dicho evento. El catálogo completo de eventos se encuentra en Eventos de Webhooks; en esta página se documenta la estructura de la solicitud que entrega Logto.
Encabezados de la solicitud
| Clave | Personalizable | Notas |
|---|---|---|
| user-agent | ✅ | Logto (https://logto.io/) por defecto. |
| content-type | ✅ | application/json por defecto. |
| logto-signature-sha-256 | Firma del cuerpo de la solicitud. Consulta asegura tus webhooks. |
Los encabezados personalizables pueden ser sobrescritos mediante la configuración de webhook seguro.
Resumen del cuerpo de la solicitud
El cuerpo es un objeto JSON. Su forma exacta depende de a qué familia pertenezca el evento:
| Familia | Eventos | Cuándo se dispara |
|---|---|---|
| Flujo de usuario | PostRegister, PostSignIn, PostResetPassword | Un usuario completa un flujo de registro, inicio de sesión o restablecimiento de contraseña gestionado por la Experience API. |
| Mutación de datos | User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* | El modelo de datos subyacente es modificado por una llamada a la Management API o un flujo de usuario en la Experience API. |
| Excepción | Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded | Un incidente de seguridad, por ejemplo, una cuenta bloqueada tras intentos fallidos consecutivos de verificación. |
Cada familia comparte un pequeño conjunto de campos comunes. Luego, cada familia añade sus propios campos de contexto de solicitud y un payload específico del evento.
Campos comunes
Presentes en cada entrega, independientemente de la familia:
| Campo | Tipo | Opcional | Notas |
|---|---|---|---|
| hookId | string | El identificador de configuración del webhook en Logto. | |
| event | string | El evento que disparó esta entrega. | |
| createdAt | string | La hora de creación del payload en formato ISO 8601. | |
| userAgent | string | ✅ | El user-agent de la solicitud que disparó el evento. |
Cada familia también incluye la dirección IP de la solicitud que disparó el evento, bajo el nombre de campo userIp para eventos de flujo de usuario y ip para eventos de mutación de datos y de excepción. La semántica es idéntica; la diferencia de nombre se mantiene por compatibilidad hacia atrás.
Payloads de eventos de flujo de usuario
Eventos: PostRegister, PostSignIn, PostResetPassword.
Se dispara cuando un usuario completa un flujo de registro, inicio de sesión o restablecimiento de contraseña gestionado por la Experience API. Además de los campos comunes, el cuerpo incluye:
| Campo | Tipo | Opcional | Notas |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | El tipo de evento de flujo de usuario. Corresponde a PostSignIn / PostRegister / PostResetPassword respectivamente. El nombre del campo mantiene la denominación histórica "interaction". | |
| sessionId | string | ✅ | El ID de sesión (no el ID de interacción) para este evento, si aplica. |
| userIp | string | ✅ | La dirección IP de la solicitud que disparó el evento. |
| userId | string | ✅ | El ID de usuario asociado a este evento, si aplica. |
| user | UserEntity | ✅ | La entidad de usuario asociada a este evento, si aplica. |
| applicationId | string | ✅ | El ID de la aplicación asociada a este evento, si aplica. |
| application | ApplicationEntity | ✅ | La entidad de la aplicación asociada a este evento, si aplica. |
Formatos de entidad
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;
};
Consulta Usuarios y Aplicaciones para la referencia completa de campos.
Payloads de eventos de mutación de datos
Eventos: todos los eventos bajo User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.*. Consulta Eventos de Webhooks → Eventos de mutación de datos para el catálogo completo.
El cuerpo siempre incluye:
- Los campos comunes.
- Un campo
ip, la dirección IP de la solicitud que disparó el evento (opcional, presente cuando se conoce). - Un contexto de API que describe cómo se disparó el cambio. El contexto es una de dos variantes dependiendo de la fuente del disparador:
- Contexto de Experience API, cuando el cambio proviene de un flujo orientado al usuario.
- Contexto de Management API, cuando el cambio proviene de una llamada directa a la Management API.
- Un payload específico del evento: la entidad afectada en
datay (para algunos eventos) campos adicionales de nivel superior. Consulta payloads de datos específicos del evento.
Campos de contexto de Experience API
Presentes cuando el cambio fue disparado por un flujo orientado al usuario en la Experience API, por ejemplo User.Created durante el registro o User.Data.Updated durante actualizaciones de perfil.
| Campo | Tipo | Opcional | Notas |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | ✅ | El tipo de evento de flujo de usuario que produjo el cambio. El nombre del campo mantiene la denominación histórica "interaction". |
| sessionId | string | ✅ | El ID de sesión (no el ID de interacción) para este evento, si aplica. |
| applicationId | string | ✅ | El ID de la aplicación, si aplica. |
| application | ApplicationEntity | ✅ | La entidad de la aplicación, si aplica. |
Campos de contexto de Management API
Presentes cuando el cambio fue disparado por una llamada a la Management API.
| Campo | Tipo | Opcional | Notas |
|---|---|---|---|
| path | string | ✅ | La ruta de la llamada API que disparó este webhook. |
| method | string | ✅ | El método HTTP de la llamada API. |
| status | number | ✅ | El código de estado de la respuesta de la llamada API. |
| params | object | ✅ | Los parámetros de ruta koa de la llamada API. |
| matchedRoute | string | ✅ | La ruta coincidente de koa. Logto usa este campo para coincidir con los filtros de eventos de webhook habilitados. |
Payloads de datos específicos del evento
Cada evento de mutación de datos incluye un campo de nivel superior data que contiene la entidad afectada, o null cuando el cambio no puede resumirse como una sola entidad (eventos de eliminación y membresía). Algunos eventos también incluyen campos de nivel superior específicos del evento además de data; Organization.Membership.Updated es uno de estos casos, documentado a continuación.
Eventos de usuario
| Evento | Campo | Tipo | Opcional | Notas |
|---|---|---|---|---|
| User.Created | data | UserEntity | La entidad de usuario creada. | |
| User.Data.Updated | data | UserEntity | La entidad de usuario actualizada. | |
| User.Deleted | data | null | / |
Eventos de rol
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;
};
| Evento | Campo | Tipo | Opcional | Notas |
|---|---|---|---|---|
| Role.Created | data | Role | La entidad de rol creada. | |
| Role.Data.Updated | data | Role | La entidad de rol actualizada. | |
| Role.Deleted | data | null | / | |
| Role.Scopes.Updated | data | Scope[] | Los alcances actualizados asignados al rol. | |
| Role.Scopes.Updated | roleId | string | ✅ | El ID del rol al que se asignan los alcances. (Solo disponible cuando el evento fue disparado al crear un rol con alcances preasignados.) |
Eventos de permiso (Scope)
| Evento | Campo | Tipo | Opcional | Notas |
|---|---|---|---|---|
| Scope.Created | data | Scope | La entidad de alcance creada. | |
| Scope.Data.Updated | data | Scope | La entidad de alcance actualizada. | |
| Scope.Deleted | data | null | / |
Eventos de organización
type Organization = {
id: string;
name: string;
description?: string;
customData: object;
createdAt: number;
};
| Evento | Campo | Tipo | Opcional | Notas |
|---|---|---|---|---|
| Organization.Created | data | Organization | La entidad de organización creada. | |
| Organization.Data.Updated | data | Organization | La entidad de organización actualizada. | |
| Organization.Deleted | data | null | / | |
| Organization.Membership.Updated | data | null | / | El cambio se describe mediante arreglos delta opcionales de nivel superior. Consulta payload de Organization.Membership.Updated abajo. |
Payload de Organization.Membership.Updated
Además de los campos comunes y los campos de contexto de API que aplican según la fuente del disparador (contexto de Management API para rutas de Management API, contexto de Experience API para aprovisionamiento just-in-time), el evento Organization.Membership.Updated lleva un organizationId más arreglos delta opcionales en el nivel superior del payload (junto a event, createdAt, etc., no dentro de data, que siempre es null para este evento).
| Campo | Tipo | Opcional | Notas |
|---|---|---|---|
| organizationId | string | La organización cuya membresía cambió. | |
| addedUserIds | string[] | ✅ | IDs de usuarios añadidos por este disparador. Se omite cuando no se añadieron usuarios, o cuando el disparador no afecta la membresía de usuarios. |
| removedUserIds | string[] | ✅ | IDs de usuarios eliminados por este disparador. Se omite cuando no se eliminaron usuarios. |
| addedApplicationIds | string[] | ✅ | IDs de aplicaciones añadidas. Se omite cuando no se añadieron aplicaciones, o cuando el disparador no afecta la membresía de aplicaciones. |
| removedApplicationIds | string[] | ✅ | IDs de aplicaciones eliminadas. Se omite cuando no se eliminaron aplicaciones. |
Los cuatro arreglos delta son opcionales y aditivos: no cambian la forma existente del payload para consumidores que no los esperan, y el campo heredado data: null sigue emitiéndose sin cambios.
Disparadores y qué campos delta pueden emitir
| Disparador | Campos delta posibles |
|---|---|
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 |
| Aprovisionamiento just-in-time al añadir el usuario a una nueva organización | addedUserIds |
Los deltas vacíos se omiten (ausente ≠ cambio vacío)
Los arreglos delta vacíos se omiten completamente del payload. Por ejemplo, un PUT /organizations/:id/users que reemplaza el conjunto de miembros con el mismo conjunto existente no produce ningún cambio real, y el payload se reduce a solo { organizationId } con los cuatro campos delta ausentes. Lo mismo aplica para volver a añadir un miembro existente y para la reaceptación de una invitación por parte de un usuario que ya es miembro.
Los consumidores deben tratar un campo ausente como "sin cambio en ese lado", no como "un cambio vacío".
Límite por arreglo (truncamiento silencioso)
Cada arreglo delta tiene un límite de 5000 entradas. Cuando una sola llamada a la Management API añade o elimina más de 5000 usuarios (o aplicaciones) en una operación, el arreglo delta correspondiente se trunca silenciosamente a sus primeras 5000 entradas. No hay ningún marcador en el payload que indique que se alcanzó el límite.
Si tu aplicación realiza operaciones administrativas masivas que puedan afectar a más de 5000 miembros en una sola llamada, trata un arreglo de exactamente 5000 entradas como una señal para reconciliar la membresía autoritativa mediante la Management API:
GET /organizations/:id/users: membresía completa de usuarios.GET /organizations/:id/applications: membresía completa de aplicaciones.
Esto sigue el mismo patrón que el evento push de GitHub, que limita commits a 20 entradas y dirige a los consumidores a la API de comparación para la lista completa.
Omitir eventos sin cambios (no-op)
Para omitir entregas sin cambios (eventos sin campos delta) del lado del consumidor, filtra por la presencia de arreglos delta:
if (
payload.addedUserIds?.length ||
payload.removedUserIds?.length ||
payload.addedApplicationIds?.length ||
payload.removedApplicationIds?.length
) {
// cambio real de membresía, manejarlo
}
?.length es falso tanto para undefined como para [], por lo que el mismo predicado es robusto tanto si el campo está ausente como si (en algún futuro hipotético) se emite como un arreglo vacío.
Ejemplos de payloads
Añadir un usuario (POST /organizations/:id/users):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedUserIds": ["u_001"]
}
Reemplazar el conjunto de miembros de usuario (PUT /organizations/:id/users):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedUserIds": ["u_002"],
"removedUserIds": ["u_001"]
}
Eliminar un usuario (DELETE /organizations/:id/users/:userId):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_001"]
}
Añadir una aplicación (POST /organizations/:id/applications):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedApplicationIds": ["app_xyz"]
}
Volver a añadir un miembro existente, PUT sin cambios, o reaceptar una invitación de un usuario que ya es miembro (sin cambio real):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc"
}
Operación masiva que alcanza el límite de 5000 (truncado silenciosamente):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_0001", "u_0002", "/* … exactamente 5000 entradas en total */"]
}
Ver un arreglo de exactamente 5000 entradas debe motivar una reconciliación con GET /organizations/:id/users (o /applications).
Eventos de rol de organización
type OrganizationRole = {
id: string;
name: string;
description?: string;
};
type OrganizationScope = {
id: string;
name: string;
description?: string;
};
| Evento | Campo | Tipo | Opcional | Notas |
|---|---|---|---|---|
| OrganizationRole.Created | data | OrganizationRole | La entidad de rol de organización creada. | |
| OrganizationRole.Data.Updated | data | OrganizationRole | La entidad de rol de organización actualizada. | |
| OrganizationRole.Deleted | data | null | / | |
| OrganizationRole.Scopes.Updated | data | null | / | |
| OrganizationRole.Scopes.Updated | organizationRoleId | string | ✅ | El ID del rol al que se asignan los alcances. (Solo disponible cuando el evento fue disparado al crear un rol con alcances preasignados.) |
Eventos de permiso de organización (scope)
| Evento | Campo | Tipo | Opcional | Notas |
|---|---|---|---|---|
| OrganizationScope.Created | data | OrganizationScope | La entidad de alcance de organización creada. | |
| OrganizationScope.Data.Updated | data | OrganizationScope | La entidad de alcance de organización actualizada. | |
| OrganizationScope.Deleted | data | null | / |
Payloads de eventos de excepción
Eventos: Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded.
Se dispara en incidentes de seguridad, por ejemplo, una cuenta bloqueada tras intentos fallidos consecutivos de verificación, o concesiones revocadas porque se superó el límite de dispositivos concurrentes autenticados de una aplicación.
Cada evento de excepción lleva los campos comunes y un campo ip (misma estructura que los eventos de mutación de datos). Los campos restantes dependen del evento.
Identifier.Lockout
Se origina en un flujo orientado al usuario, por lo que el cuerpo también incluye los campos de contexto de Experience API, además de:
enum SignInIdentifier {
Email = 'email',
Phone = 'phone',
Username = 'username',
}
| Campo | Tipo | Opcional | Notas |
|---|---|---|---|
| type | SignInIdentifier | El tipo de identificador del usuario, por ejemplo, email, teléfono o nombre de usuario. | |
| value | string | El valor del identificador del usuario que disparó el bloqueo. |
Message.RateLimited
Se origina en un flujo orientado al usuario, por lo que el cuerpo también incluye los campos de contexto de Experience API, además de:
| Campo | Tipo | Opcional | Notas |
|---|---|---|---|
| action | string | La acción limitada por tasa, por ejemplo, VerificationCodeSend. | |
| recipient | string | El correo electrónico o número de teléfono que alcanzó el límite de envío. |
Grant.LimitExceeded
Se dispara cuando una autorización exitosa hace que un usuario supere el límite máximo de dispositivos autenticados concurrentes de una aplicación (maxAllowedGrants) y Logto revoca sus concesiones más antiguas para esa aplicación.
Este evento se emite desde el endpoint de autorización OIDC en lugar de la Experience API, por lo que no lleva interactionEvent ni sessionId. Junto a los campos comunes e ip, el cuerpo incluye:
| Campo | Tipo | Opcional | Notas |
|---|---|---|---|
| userId | string | El usuario cuyas concesiones fueron revocadas. | |
| applicationId | string | La aplicación cuyo límite maxAllowedGrants fue superado. | |
| application | ApplicationEntity | ✅ | La entidad de la aplicación. Se omite si la aplicación no puede resolverse en el momento de la entrega. |
| maxAllowedGrants | number | El límite configurado en la aplicación cuando se disparó el evento. | |
| preRevocationActiveGrantCount | number | El número de concesiones activas que el usuario tenía para esta aplicación antes de la revocación, incluyendo la recién emitida. | |
| revokedGrantIds | string[] | Los IDs de las concesiones que realmente fueron revocadas, de la más antigua a la más reciente. |
Ejemplo de payload:
{
"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"]
}
Notas de entrega:
- Los registros de concesiones revocadas se destruyen como parte de la revocación, por lo que los IDs en
revokedGrantIdsya no se resuelven a través de los endpoints de listado de concesiones ni en la Consola — estos solo devuelven concesiones activas. Trata el payload como el registro y persiste los IDs en tu lado si los necesitas más adelante. - El evento solo se dispara cuando al menos una concesión fue realmente revocada. Una autorización que permanece dentro del límite no produce evento.
- Se dispara en cada autorización que excede el límite, por lo que un usuario que inicia sesión repetidamente desde más dispositivos de los permitidos produce un evento por cada expulsión.
- El despacho es fire-and-forget: un endpoint lento o fallido nunca bloquea ni falla la autorización del usuario. Las entregas fallidas se registran en los logs de auditoría como cualquier otro webhook.