Webhooks-Anfrage
Wenn ein Webhook-Ereignis ausgelöst wird, sendet Logto eine POST-Anfrage an jeden Endpunkt, der darauf abonniert ist. Den vollständigen Ereigniskatalog findest du unter Webhooks-Ereignisse; auf dieser Seite wird die Struktur der Anfrage dokumentiert, die Logto übermittelt.
Anfrage-Header
| Key | Anpassbar | Hinweise |
|---|---|---|
| user-agent | ✅ | Standardmäßig Logto (https://logto.io/). |
| content-type | ✅ | Standardmäßig application/json. |
| logto-signature-sha-256 | Signatur des Anfrage-Bodys. Siehe Webhooks absichern. |
Anpassbare Header können über die Secure Webhook-Konfiguration überschrieben werden.
Überblick über den Anfrage-Body
Der Body ist ein JSON-Objekt. Seine genaue Struktur hängt davon ab, zu welcher Familie das Ereignis gehört:
| Familie | Ereignisse | Wann es ausgelöst wird |
|---|---|---|
| User Flow | PostRegister, PostSignIn, PostResetPassword | Ein Benutzer schließt einen Anmelde-, Registrierungs- oder Passwort-Reset-Flow ab, der von der Experience API verarbeitet wird. |
| Datenmutation | User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* | Das zugrunde liegende Datenmodell wird durch einen Management API-Aufruf oder einen User Flow auf der Experience API verändert. |
| Ausnahme | Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded | Ein Sicherheitsvorfall, z. B. ein Konto wird nach aufeinanderfolgenden fehlgeschlagenen Verifizierungsversuchen gesperrt. |
Jede Familie teilt sich einen kleinen Satz von gemeinsamen Feldern. Jede Familie ergänzt dann eigene Kontextfelder sowie eine ereignisspezifische Nutzlast.
Gemeinsame Felder
In jeder Zustellung unabhängig von der Familie enthalten:
| Feld | Typ | Optional | Hinweise |
|---|---|---|---|
| hookId | string | Die Webhook-Konfigurationskennung in Logto. | |
| deliveryId | string | Die Kennung dieser Zustellung, eindeutig pro Webhook und Ereignis und bleibt über die automatischen Wiederholungen derselben Anfrage hinweg erhalten. Siehe Deduplizierung von Zustellungen. | |
| event | string | Das Ereignis, das diese Zustellung ausgelöst hat. | |
| createdAt | string | Die Erstellungszeit der Nutzlast im ISO 8601-Format. | |
| userAgent | string | ✅ | Der User-Agent der auslösenden Anfrage. |
Jede Familie enthält außerdem die IP-Adresse der auslösenden Anfrage, unter dem Feldnamen userIp für User-Flow-Ereignisse und ip für Datenmutations- und Ausnahmeereignisse. Die Semantik ist identisch; der historische Namensunterschied bleibt aus Gründen der Rückwärtskompatibilität erhalten.
Deduplizierung von Zustellungen
Eine Zustellung kann deinen Endpunkt mehrmals erreichen: Logto wiederholt eine Anfrage, deren Antwortstatus 500 oder höher ist. Eine Anfrage, die dein Endpunkt verarbeitet hat, aber dennoch mit einem 5xx geantwortet hat (z. B. ein Gateway gibt 504 vor einem langsamen Handler zurück), kommt erneut an. Jeder Versuch derselben Zustellung trägt die gleiche deliveryId, während ein neues Ereignis oder dasselbe Ereignis, das an einen anderen Webhook gesendet wird, eine neue erhält. Notiere die deliveryId jeder Nutzlast, die du verarbeitet hast, und überspringe eine Nutzlast, deren ID du bereits kennst.
Die deliveryId ist auch die ID des Audit-Log-Eintrags, der die Zustellung protokolliert. Ein von deinem Dienst gemeldeter Wert kann also aus den letzten Anfragen des Webhooks in der Konsole geöffnet oder mit GET /api/logs/:id abgerufen werden.
Auch die Test-Nutzlast trägt eine deliveryId, sodass derselbe Handler sie verarbeiten kann, obwohl Testanfragen nicht protokolliert werden.
User-Flow-Ereignis-Nutzlasten
Ereignisse: PostRegister, PostSignIn, PostResetPassword.
Wird ausgelöst, wenn ein Benutzer einen Anmelde-, Registrierungs- oder Passwort-Reset-Flow abschließt, der von der Experience API verarbeitet wird. Zusätzlich zu den gemeinsamen Feldern enthält der Body:
| Feld | Typ | Optional | Hinweise |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | Der User-Flow-Ereignistyp. Entspricht PostSignIn / PostRegister / PostResetPassword. Der Feldname behält die historische "interaction"-Benennung. | |
| sessionId | string | ✅ | Die Session-ID (nicht Interaction-ID) für dieses Ereignis, falls zutreffend. |
| userIp | string | ✅ | Die IP-Adresse der auslösenden Anfrage. |
| userId | string | ✅ | Die Benutzer-ID, die mit diesem Ereignis verknüpft ist, falls zutreffend. |
| user | UserEntity | ✅ | Die Benutzer-Entität, die mit diesem Ereignis verknüpft ist, falls zutreffend. |
| applicationId | string | ✅ | Die Anwendungs-ID, die mit diesem Ereignis verknüpft ist, falls zutreffend. |
| application | ApplicationEntity | ✅ | Die Anwendungs-Entität, die mit diesem Ereignis verknüpft ist, falls zutreffend. |
Entitätsstrukturen
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;
};
Siehe Benutzer und Anwendungen für die vollständige Feldreferenz.
Datenmutations-Ereignis-Nutzlasten
Ereignisse: jedes Ereignis unter User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.*. Siehe Webhooks-Ereignisse → Datenmutations-Webhook-Ereignisse für den vollständigen Katalog.
Der Body enthält immer:
- Die gemeinsamen Felder.
- Ein
ip-Feld, die IP-Adresse der auslösenden Anfrage (optional, vorhanden wenn bekannt). - Einen API-Kontext, der beschreibt, wie die Änderung ausgelöst wurde. Der Kontext ist eine von zwei Varianten, abhängig von der Auslöserquelle:
- Experience API-Kontext, wenn die Änderung aus einem benutzerorientierten Flow stammt.
- Management API-Kontext, wenn die Änderung durch einen direkten Management API-Aufruf ausgelöst wurde.
- Eine ereignisspezifische Nutzlast: die betroffene Entität in
dataund (bei einigen Ereignissen) zusätzliche Felder auf oberster Ebene. Siehe ereignisspezifische Daten-Nutzlasten.
Experience API-Kontextfelder
Vorhanden, wenn die Änderung durch einen benutzerorientierten Flow auf der Experience API ausgelöst wurde, z. B. User.Created während der Registrierung oder User.Data.Updated bei Profilaktualisierungen.
| Feld | Typ | Optional | Hinweise |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | ✅ | Der User-Flow-Ereignistyp, der die Änderung verursacht hat. Feldname behält die historische "interaction"-Benennung. |
| sessionId | string | ✅ | Die Session-ID (nicht Interaction-ID) für dieses Ereignis, falls zutreffend. |
| applicationId | string | ✅ | Die Anwendungs-ID, falls zutreffend. |
| application | ApplicationEntity | ✅ | Die Anwendungs-Entität, falls zutreffend. |
Management API-Kontextfelder
Vorhanden, wenn die Änderung durch einen Management API-Aufruf ausgelöst wurde.
| Feld | Typ | Optional | Hinweise |
|---|---|---|---|
| path | string | ✅ | Der Pfad des API-Aufrufs, der diesen Webhook ausgelöst hat. |
| method | string | ✅ | Die HTTP-Methode des API-Aufrufs. |
| status | number | ✅ | Der Antwortstatuscode des API-Aufrufs. |
| params | object | ✅ | Die koa path params des API-Aufrufs. |
| matchedRoute | string | ✅ | Die koa matched route. Logto verwendet dieses Feld, um aktivierte Webhook-Ereignisfilter abzugleichen. |
Ereignisspezifische Daten-Nutzlasten
Jedes Datenmutationsereignis enthält ein oberstes data-Feld mit der betroffenen Entität oder null, wenn die Änderung nicht als einzelne Entität zusammengefasst werden kann (Lösch- und Mitgliedschaftsereignisse). Einige Ereignisse enthalten außerdem ereignisspezifische Felder auf oberster Ebene; Organization.Membership.Updated ist ein solcher Fall, siehe unten.
Benutzerereignisse
| Ereignis | Feld | Typ | Optional | Hinweise |
|---|---|---|---|---|
| User.Created | data | UserEntity | Die erstellte Benutzer-Entität. | |
| User.Data.Updated | data | UserEntity | Die aktualisierte Benutzer-Entität. | |
| User.Deleted | data | null | / |
Rollenereignisse
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;
};
| Ereignis | Feld | Typ | Optional | Hinweise |
|---|---|---|---|---|
| Role.Created | data | Role | Die erstellte Rollen-Entität. | |
| Role.Data.Updated | data | Role | Die aktualisierte Rollen-Entität. | |
| Role.Deleted | data | null | / | |
| Role.Scopes.Updated | data | Scope[] | Die aktualisierten Berechtigungen, die der Rolle zugewiesen sind. | |
| Role.Scopes.Updated | roleId | string | ✅ | Die Rollen-ID, der Berechtigungen zugewiesen wurden. (Nur verfügbar, wenn das Ereignis durch das Erstellen einer Rolle mit vorab zugewiesenen Berechtigungen ausgelöst wurde.) |
Berechtigungs- (Scope-) Ereignisse
| Ereignis | Feld | Typ | Optional | Hinweise |
|---|---|---|---|---|
| Scope.Created | data | Scope | Die erstellte Berechtigungs-Entität. | |
| Scope.Data.Updated | data | Scope | Die aktualisierte Berechtigungs-Entität. | |
| Scope.Deleted | data | null | / |
Organisationserereignisse
type Organization = {
id: string;
name: string;
description?: string;
customData: object;
createdAt: number;
};
| Ereignis | Feld | Typ | Optional | Hinweise |
|---|---|---|---|---|
| Organization.Created | data | Organization | Die erstellte Organisations-Entität. | |
| Organization.Data.Updated | data | Organization | Die aktualisierte Organisations-Entität. | |
| Organization.Deleted | data | null | / | |
| Organization.Membership.Updated | data | null | / | Die Änderung wird durch optionale Delta-Arrays auf oberster Ebene beschrieben. Siehe Organization.Membership.Updated-Nutzlast unten. |
Organization.Membership.Updated-Nutzlast
Zusätzlich zu den gemeinsamen Feldern und den API-Kontextfeldern, die für die Auslöserquelle gelten (Management API-Kontext für Management API-Routen, Experience API-Kontext für Just-in-Time-Bereitstellung), enthält das Ereignis Organization.Membership.Updated eine organizationId sowie optionale Delta-Arrays auf oberster Ebene der Nutzlast (neben event, createdAt usw., nicht innerhalb von data, das für dieses Ereignis immer null ist).
| Feld | Typ | Optional | Hinweise |
|---|---|---|---|
| organizationId | string | Die Organisation, deren Mitgliedschaft sich geändert hat. | |
| addedUserIds | string[] | ✅ | Benutzer-IDs, die durch diesen Auslöser neu hinzugefügt wurden. Wird weggelassen, wenn keine Benutzer hinzugefügt wurden oder der Auslöser die Benutzerzugehörigkeit nicht betrifft. |
| removedUserIds | string[] | ✅ | Benutzer-IDs, die entfernt wurden. Wird weggelassen, wenn keine Benutzer entfernt wurden. |
| addedApplicationIds | string[] | ✅ | Anwendungs-IDs, die neu hinzugefügt wurden. Wird weggelassen, wenn keine Anwendungen hinzugefügt wurden oder der Auslöser die Anwendungszugehörigkeit nicht betrifft. |
| removedApplicationIds | string[] | ✅ | Anwendungs-IDs, die entfernt wurden. Wird weggelassen, wenn keine Anwendungen entfernt wurden. |
Die vier Delta-Arrays sind optional und additiv: Sie ändern die bestehende Nutzlaststruktur für Konsumenten, die sie nicht erwarten, nicht, und das Legacy-Feld data: null wird weiterhin unverändert ausgegeben.
Auslöser und welche Delta-Felder sie ausgeben können
| Auslöser | Mögliche Delta-Felder |
|---|---|
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-Bereitstellung beim Hinzufügen des Benutzers zu einer neuen Organisation | addedUserIds |
Leere Deltas werden weggelassen (abwesend ≠ keine Änderung)
Leere Delta-Arrays werden vollständig aus der Nutzlast entfernt. Beispielsweise erzeugt ein PUT /organizations/:id/users, das die Mitgliedschaftsmenge durch die bestehende Menge ersetzt, keine echte Änderung, und die Nutzlast reduziert sich auf nur { organizationId } mit allen vier Delta-Feldern abwesend. Gleiches gilt für das erneute Hinzufügen eines bestehenden Mitglieds und das erneute Akzeptieren einer Einladung durch einen Benutzer, der bereits Mitglied ist.
Konsumenten müssen ein fehlendes Feld als "keine Änderung auf dieser Seite" behandeln, nicht als "leere Änderung".
Pro-Array-Limit (stilles Abschneiden)
Jedes Delta-Array ist auf 5000 Einträge begrenzt. Wenn ein einzelner Management API-Aufruf mehr als 5000 Benutzer (oder Anwendungen) in einer Operation hinzufügt oder entfernt, wird das entsprechende Delta-Array stillschweigend auf die ersten 5000 Einträge abgeschnitten. Es gibt keinen Marker in der Nutzlast, dass ein Limit ausgelöst wurde.
Wenn deine Anwendung administrative Massenoperationen durchführt, die plausibel mehr als 5000 Mitglieder in einem Aufruf betreffen können, behandle ein Array mit genau 5000 Einträgen als Signal, die autoritative Mitgliedschaft über die Management API abzugleichen:
GET /organizations/:id/users: vollständige Benutzer-Mitgliedschaft.GET /organizations/:id/applications: vollständige Anwendungs-Mitgliedschaft.
Dies folgt demselben Muster wie das push-Ereignis von GitHub, das commits auf 20 Einträge begrenzt und Konsumenten auf die Compare-API für die vollständige Liste verweist.
No-op-Ereignisse überspringen
Um No-op-Zustellungen (Ereignisse ohne Delta-Felder) auf der Konsumentenseite zu überspringen, filtere auf das Vorhandensein von Delta-Arrays:
if (
payload.addedUserIds?.length ||
payload.removedUserIds?.length ||
payload.addedApplicationIds?.length ||
payload.removedApplicationIds?.length
) {
// echte Mitgliedschaftsänderung, verarbeite sie
}
?.length ist für sowohl undefined als auch [] falsch, sodass das gleiche Prädikat robust ist, egal ob das Feld fehlt oder (in einer hypothetischen Zukunft) als leeres Array ausgegeben wird.
Beispiel-Nutzlasten
Benutzer hinzufügen (POST /organizations/:id/users):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedUserIds": ["u_001"]
}
Benutzer-Mitgliedschaftsmenge ersetzen (PUT /organizations/:id/users):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedUserIds": ["u_002"],
"removedUserIds": ["u_001"]
}
Benutzer entfernen (DELETE /organizations/:id/users/:userId):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_001"]
}
Anwendung hinzufügen (POST /organizations/:id/applications):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedApplicationIds": ["app_xyz"]
}
Erneutes Hinzufügen eines bestehenden Mitglieds, No-op-PUT oder erneutes Akzeptieren einer Einladung eines bereits Mitglieds (keine echte Änderung):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc"
}
Massenoperation, die das 5000er-Limit erreicht (still abgeschnitten):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_0001", "u_0002", "/* … genau 5000 Einträge insgesamt */"]
}
Das Erkennen eines Arrays mit genau 5000 Einträgen sollte einen abgleichenden GET /organizations/:id/users (oder /applications) auslösen.
Organisationsrollen-Ereignisse
type OrganizationRole = {
id: string;
name: string;
description?: string;
};
type OrganizationScope = {
id: string;
name: string;
description?: string;
};
| Ereignis | Feld | Typ | Optional | Hinweise |
|---|---|---|---|---|
| OrganizationRole.Created | data | OrganizationRole | Die erstellte Organisationsrollen-Entität. | |
| OrganizationRole.Data.Updated | data | OrganizationRole | Die aktualisierte Organisationsrollen-Entität. | |
| OrganizationRole.Deleted | data | null | / | |
| OrganizationRole.Scopes.Updated | data | null | / | |
| OrganizationRole.Scopes.Updated | organizationRoleId | string | ✅ | Die Rollen-ID, der Berechtigungen zugewiesen wurden. (Nur verfügbar, wenn das Ereignis durch das Erstellen einer Rolle mit vorab zugewiesenen Berechtigungen ausgelöst wurde.) |
Organisationsberechtigungs- (Scope-) Ereignisse
| Ereignis | Feld | Typ | Optional | Hinweise |
|---|---|---|---|---|
| OrganizationScope.Created | data | OrganizationScope | Die erstellte Organisationsberechtigungs-Entität. | |
| OrganizationScope.Data.Updated | data | OrganizationScope | Die aktualisierte Organisationsberechtigungs-Entität. | |
| OrganizationScope.Deleted | data | null | / |
Ausnahme-Ereignis-Nutzlasten
Ereignisse: Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded.
Wird bei Sicherheitsvorfällen ausgelöst, z. B. wenn ein Konto nach aufeinanderfolgenden fehlgeschlagenen Verifizierungsversuchen gesperrt wird oder Grants entfernt werden, weil das gleichzeitige Geräte-Limit einer App überschritten wurde.
Jedes Ausnahmeereignis enthält die gemeinsamen Felder und ein ip-Feld (gleiche Struktur wie bei Datenmutationsereignissen). Die übrigen Felder hängen vom Ereignis ab.
Identifier.Lockout
Stammt aus einem benutzerorientierten Flow, daher enthält der Body auch die Experience API-Kontextfelder sowie:
enum SignInIdentifier {
Email = 'email',
Phone = 'phone',
Username = 'username',
}
| Feld | Typ | Optional | Hinweise |
|---|---|---|---|
| type | SignInIdentifier | Der Identifikatortyp des Benutzers, z. B. E-Mail, Telefon oder Benutzername. | |
| value | string | Der Identifikatorwert des Benutzers, der die Sperrung ausgelöst hat. |
Message.RateLimited
Stammt aus einem benutzerorientierten Flow, daher enthält der Body auch die Experience API-Kontextfelder sowie:
| Feld | Typ | Optional | Hinweise |
|---|---|---|---|
| action | string | Die rate-limitierte Aktion, z. B. VerificationCodeSend. | |
| recipient | string | Die E-Mail-Adresse oder Telefonnummer, die das Sendelimit erreicht hat. |
Grant.LimitExceeded
Wird ausgelöst, wenn eine erfolgreiche Autorisierung einen Benutzer über das maximal zulässige gleichzeitige Geräte-Limit (maxAllowedGrants) einer App hinausbringt und Logto die ältesten Grants für diese App widerruft.
Dieses Ereignis wird vom OIDC-Autorisierungsendpunkt und nicht von der Experience API gesendet, daher enthält es kein interactionEvent oder sessionId. Neben den gemeinsamen Feldern und ip enthält der Body:
| Feld | Typ | Optional | Hinweise |
|---|---|---|---|
| userId | string | Der Benutzer, dessen Grants widerrufen wurden. | |
| applicationId | string | Die Anwendung, deren maxAllowedGrants-Limit überschritten wurde. | |
| application | ApplicationEntity | ✅ | Die Anwendungs-Entität. Wird weggelassen, wenn die Anwendung zum Zeitpunkt der Zustellung nicht aufgelöst werden kann. |
| maxAllowedGrants | number | Das zum Zeitpunkt des Ereignisses konfigurierte Limit der Anwendung. | |
| preRevocationActiveGrantCount | number | Die Anzahl aktiver Grants, die der Benutzer für diese Anwendung vor dem Widerruf hielt, einschließlich des gerade ausgestellten. | |
| revokedGrantIds | string[] | Die IDs der tatsächlich widerrufenen Grants, älteste zuerst. |
Beispiel-Nutzlast:
{
"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"]
}
Hinweise zur Zustellung:
- Die widerrufenen Grant-Datensätze werden im Rahmen des Widerrufs gelöscht, sodass die IDs in
revokedGrantIdsnicht mehr über die Grant-Listen-Endpunkte oder die Konsole aufgelöst werden können — diese geben nur aktive Grants zurück. Behandle die Nutzlast als Datensatz und speichere die IDs auf deiner Seite, wenn du sie später benötigst. - Das Ereignis wird nur ausgelöst, wenn mindestens ein Grant tatsächlich widerrufen wurde. Eine Autorisierung, die innerhalb des Limits bleibt, erzeugt kein Ereignis.
- Es wird bei jeder Autorisierung ausgelöst, die das Limit überschreitet, sodass ein Benutzer, der sich wiederholt von mehr Geräten als erlaubt anmeldet, pro Entfernung ein Ereignis erhält.
- Die Zustellung erfolgt nach dem Fire-and-Forget-Prinzip: Ein langsamer oder fehlerhafter Endpunkt blockiert oder verhindert niemals die Autorisierung des Benutzers. Fehlgeschlagene Zustellungen werden wie jeder andere Webhook im Audit-Log protokolliert.