คำขอ Webhooks (Webhooks request)
เมื่อเกิดเหตุการณ์ webhook ขึ้น Logto จะส่งคำขอ POST ไปยังทุก endpoint ที่สมัครรับเหตุการณ์นั้นไว้ แคตตาล็อกเหตุการณ์ทั้งหมดดูได้ที่ เหตุการณ์ Webhooks; หน้านี้อธิบาย รูปแบบของคำขอ ที่ Logto ส่งให้
ส่วนหัวของคำขอ (Request headers)
| Key | ปรับแต่งได้ | หมายเหตุ |
|---|---|---|
| user-agent | ✅ | ค่าเริ่มต้นคือ Logto (https://logto.io/). |
| content-type | ✅ | ค่าเริ่มต้นคือ application/json. |
| logto-signature-sha-256 | ลายเซ็นของเนื้อหาใน request body ดู การรักษาความปลอดภัยของ webhook. |
ส่วนหัวที่ปรับแต่งได้สามารถ override ได้ผ่าน secure webhook
ภาพรวมของเนื้อหาในคำขอ (Request body overview)
เนื้อหาใน body เป็นอ็อบเจกต์ JSON รูปแบบที่แน่นอนขึ้นอยู่กับกลุ่มของเหตุการณ์ที่เกิดขึ้น:
| กลุ่ม (Family) | เหตุการณ์ (Events) | เมื่อใดที่เกิดเหตุการณ์ (When it fires) |
|---|---|---|
| User flow | PostRegister, PostSignIn, PostResetPassword | ผู้ใช้ดำเนินการลงทะเบียน, ลงชื่อเข้าใช้, หรือรีเซ็ตรหัสผ่านผ่าน Experience API |
| Data mutation | User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* | โมเดลข้อมูลถูกเปลี่ยนแปลงโดย Management API หรือ user flow บน Experience API |
| Exception | Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded | เหตุการณ์ด้านความปลอดภัย เช่น บัญชีถูกล็อกหลังจากพยายามยืนยันตัวตนผิดหลายครั้ง |
ทุกกลุ่มจะมี ฟิลด์ร่วม (common fields) ชุดเล็ก ๆ จากนั้นแต่ละกลุ่มจะเพิ่มฟิลด์ context ของคำขอและ payload เฉพาะเหตุการณ์ของตัวเอง
ฟิลด์ร่วม (Common fields)
มีอยู่ในทุกการส่ง ไม่ว่ากลุ่มใด:
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| hookId | string | รหัสการตั้งค่า webhook ใน Logto | |
| event | string | เหตุการณ์ที่ทริกเกอร์การส่งนี้ | |
| createdAt | string | เวลาสร้าง payload ในรูปแบบ ISO 8601 | |
| userAgent | string | ✅ | user-agent ของคำขอที่ทริกเกอร์เหตุการณ์ |
แต่ละกลุ่มยังมี IP address ของคำขอที่ทริกเกอร์เหตุการณ์ โดยใช้ชื่อฟิลด์ userIp สำหรับ user-flow events และ ip สำหรับ data-mutation และ exception events ความหมายเหมือนกัน ต่างกันแค่ชื่อเพื่อความเข้ากันได้ย้อนหลัง
Payload ของเหตุการณ์ user flow
เหตุการณ์: PostRegister, PostSignIn, PostResetPassword
เกิดขึ้นเมื่อผู้ใช้ดำเนินการลงทะเบียน, ลงชื่อเข้าใช้ หรือรีเซ็ตรหัสผ่านผ่าน Experience API นอกจาก ฟิลด์ร่วม (common fields) แล้ว body จะมี:
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | ประเภทเหตุการณ์ user-flow แมปกับ PostSignIn / PostRegister / PostResetPassword ชื่อฟิลด์ยังคงใช้ชื่อ "interaction" เดิม | |
| sessionId | string | ✅ | Session ID (ไม่ใช่ Interaction ID) ของเหตุการณ์นี้ ถ้ามี |
| userIp | string | ✅ | IP address ของคำขอที่ทริกเกอร์เหตุการณ์ |
| userId | string | ✅ | User ID ที่เกี่ยวข้องกับเหตุการณ์นี้ ถ้ามี |
| user | UserEntity | ✅ | user entity ที่เกี่ยวข้องกับเหตุการณ์นี้ ถ้ามี |
| applicationId | string | ✅ | Application ID ที่เกี่ยวข้องกับเหตุการณ์นี้ ถ้ามี |
| application | ApplicationEntity | ✅ | application entity ที่เกี่ยวข้องกับเหตุการณ์นี้ ถ้ามี |
รูปแบบ entity
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;
};
ดูรายละเอียดฟิลด์ทั้งหมดได้ที่ ผู้ใช้ (Users) และ แอปพลิเคชัน (Applications)
Payload ของเหตุการณ์ data mutation
เหตุการณ์: ทุกเหตุการณ์ภายใต้ User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* ดู เหตุการณ์ Webhooks → Data mutation webhook events สำหรับแคตตาล็อกเต็ม
body จะมีเสมอ:
- ฟิลด์ร่วม (common fields)
- ฟิลด์
ipคือ IP address ของคำขอที่ทริกเกอร์เหตุการณ์ (optional, มีเมื่อทราบ) - API context อธิบายว่าการเปลี่ยนแปลงเกิดจากอะไร context มี 2 แบบขึ้นกับแหล่งที่มา:
- Experience API context เมื่อเปลี่ยนแปลงมาจาก user-facing flow
- Management API context เมื่อเปลี่ยนแปลงมาจากการเรียก Management API โดยตรง
- payload เฉพาะเหตุการณ์: entity ที่ได้รับผลกระทบใน
dataและ (สำหรับบางเหตุการณ์) ฟิลด์ระดับบนเพิ่มเติม ดู event-specific data payloads
ฟิลด์ Experience API context
มีเมื่อการเปลี่ยนแปลงเกิดจาก user-facing flow บน Experience API เช่น User.Created ระหว่างลงทะเบียน หรือ User.Data.Updated ระหว่างอัปเดตโปรไฟล์
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| interactionEvent | 'SignIn' | 'Register' | 'ForgotPassword' | ✅ | ประเภทเหตุการณ์ user-flow ที่ทำให้เกิดการเปลี่ยนแปลง ชื่อฟิลด์ยังคงใช้ชื่อ "interaction" เดิม |
| sessionId | string | ✅ | Session ID (ไม่ใช่ Interaction ID) ของเหตุการณ์นี้ ถ้ามี |
| applicationId | string | ✅ | Application ID ถ้ามี |
| application | ApplicationEntity | ✅ | application entity ถ้ามี |
ฟิลด์ Management API context
มีเมื่อการเปลี่ยนแปลงเกิดจากการเรียก Management API
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| path | string | ✅ | path ของ API call ที่ทริกเกอร์ webhook นี้ |
| method | string | ✅ | HTTP method ของ API call |
| status | number | ✅ | response status code ของ API call |
| params | object | ✅ | koa path params ของ API call |
| matchedRoute | string | ✅ | koa matched route Logto ใช้ฟิลด์นี้จับคู่ webhook event filter ที่เปิดใช้งาน |
Payload ข้อมูลเฉพาะเหตุการณ์ (Event-specific data payloads)
ทุกเหตุการณ์ data-mutation จะมีฟิลด์ data ระดับบนสุดซึ่งบรรจุ entity ที่ได้รับผลกระทบ หรือ null เมื่อไม่สามารถสรุปเป็น entity เดียวได้ (เช่น เหตุการณ์ลบและ membership) บางเหตุการณ์จะมีฟิลด์ระดับบนเพิ่มเติมนอกเหนือจาก data เช่น Organization.Membership.Updated ดูรายละเอียดด้านล่าง
เหตุการณ์ผู้ใช้ (User events)
| Event | Field | Type | Optional | หมายเหตุ |
|---|---|---|---|---|
| User.Created | data | UserEntity | user entity ที่ถูกสร้าง | |
| User.Data.Updated | data | UserEntity | 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 | หมายเหตุ |
|---|---|---|---|---|
| Role.Created | data | Role | role entity ที่ถูกสร้าง | |
| Role.Data.Updated | data | Role | role entity ที่ถูกอัปเดต | |
| Role.Deleted | data | null | / | |
| Role.Scopes.Updated | data | Scope[] | scopes ที่อัปเดตแล้วซึ่งถูกกำหนดให้กับ role | |
| Role.Scopes.Updated | roleId | string | ✅ | role ID ที่ scopes ถูกกำหนดให้ (มีเฉพาะเมื่อเหตุการณ์เกิดจากการสร้าง role พร้อมกำหนด scopes ล่วงหน้า) |
เหตุการณ์สิทธิ์ (Scope) (Permission (Scope) events)
| Event | Field | Type | Optional | หมายเหตุ |
|---|---|---|---|---|
| Scope.Created | data | Scope | scope entity ที่ถูกสร้าง | |
| Scope.Data.Updated | data | Scope | scope entity ที่ถูกอัปเดต | |
| Scope.Deleted | data | null | / |
เหตุการณ์องค์กร (Organization events)
type Organization = {
id: string;
name: string;
description?: string;
customData: object;
createdAt: number;
};
| Event | Field | Type | Optional | หมายเหตุ |
|---|---|---|---|---|
| Organization.Created | data | Organization | organization entity ที่ถูกสร้าง | |
| Organization.Data.Updated | data | Organization | organization entity ที่ถูกอัปเดต | |
| Organization.Deleted | data | null | / | |
| Organization.Membership.Updated | data | null | / | การเปลี่ยนแปลงอธิบายโดย delta array ระดับบน (optional) ดู Organization.Membership.Updated payload ด้านล่าง |
Payload ของ Organization.Membership.Updated
นอกจาก ฟิลด์ร่วม (common fields) และฟิลด์ API-context ที่เกี่ยวข้องกับแหล่งที่มาของ trigger (Management API context สำหรับ Management API, Experience API context สำหรับ just-in-time provisioning) เหตุการณ์ Organization.Membership.Updated จะมี organizationId และ delta array (optional) ที่ระดับบนสุดของ payload (ถัดจาก event, createdAt ฯลฯ ไม่ใช่ ใน data ซึ่งจะเป็น null เสมอสำหรับเหตุการณ์นี้)
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| organizationId | string | องค์กรที่ membership เปลี่ยนแปลง | |
| addedUserIds | string[] | ✅ | User ID ที่ถูกเพิ่มใหม่โดย trigger นี้ ไม่ใส่เมื่อไม่มีผู้ใช้ถูกเพิ่ม หรือ trigger ไม่เกี่ยวกับ user membership |
| removedUserIds | string[] | ✅ | User ID ที่ถูกลบโดย trigger นี้ ไม่ใส่เมื่อไม่มีผู้ใช้ถูกลบ |
| addedApplicationIds | string[] | ✅ | Application ID ที่ถูกเพิ่มใหม่ ไม่ใส่เมื่อไม่มีแอปถูกเพิ่ม หรือ trigger ไม่เกี่ยวกับ application membership |
| removedApplicationIds | string[] | ✅ | Application ID ที่ถูกลบ ไม่ใส่เมื่อไม่มีแอปถูกลบ |
delta array ทั้งสี่นี้ เป็น optional และ additive: ไม่เปลี่ยนรูปแบบ payload เดิมสำหรับ consumer ที่ไม่รองรับ และฟิลด์ data: null แบบเดิมยังคงส่งออกเหมือนเดิม
Trigger และ delta field ที่อาจถูกส่ง
| Trigger | delta field ที่เป็นไปได้ |
|---|---|
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 เมื่อเพิ่มผู้ใช้สู่องค์กรใหม่ | addedUserIds |
delta ที่ว่างจะถูกละเว้น (Empty deltas are omitted)
delta array ที่ว่าง จะถูกละเว้นโดยสมบูรณ์ จาก payload เช่น PUT /organizations/:id/users ที่แทนที่สมาชิกด้วยชุดเดิมจะไม่มีการเปลี่ยนแปลงจริง และ payload จะเหลือแค่ { organizationId } โดยไม่มี delta field ทั้งสี่ เช่นเดียวกับการเพิ่มสมาชิกซ้ำหรือรับคำเชิญซ้ำโดยผู้ใช้ที่เป็นสมาชิกอยู่แล้ว
Consumer ต้องถือว่าฟิลด์ที่หายไปคือ "ไม่มีการเปลี่ยนแปลงฝั่งนั้น" ไม่ใช่ "เปลี่ยนแปลงเป็นค่าว่าง"
จำกัดจำนวนต่อ array (Per-array cap; silent truncation)
แต่ละ delta array จำกัดที่ 5000 รายการ หาก Management API call เดียวเพิ่มหรือลบผู้ใช้ (หรือแอป) มากกว่า 5000 รายการ array ที่เกี่ยวข้องจะถูกตัดเหลือ 5000 รายการแรก ไม่มี marker ใน payload ว่าถูกตัด
หากแอปของคุณดำเนินการ bulk ที่อาจกระทบสมาชิกมากกว่า 5000 คนในครั้งเดียว ให้ถือ array ที่มี 5000 รายการว่าเป็นสัญญาณให้ sync สมาชิก authoritative ผ่าน Management API:
GET /organizations/:id/users: สมาชิกผู้ใช้ทั้งหมดGET /organizations/:id/applications: สมาชิกแอปทั้งหมด
แนวทางนี้เหมือนกับ event push ของ GitHub ที่จำกัด commits ไว้ 20 รายการและแนะนำให้ consumer ไปดู compare API สำหรับรายการเต็ม
ข้าม event ที่ไม่มีการเปลี่ยนแปลง (Skipping no-op events)
เพื่อข้ามการส่งที่ไม่มีการเปลี่ยนแปลง (event ที่ไม่มี delta field) ฝั่ง consumer ให้ filter ด้วยการมีอยู่ของ delta array:
if (
payload.addedUserIds?.length ||
payload.removedUserIds?.length ||
payload.addedApplicationIds?.length ||
payload.removedApplicationIds?.length
) {
// มีการเปลี่ยนแปลง membership จริง ให้จัดการ
}
?.length จะเป็น falsy ทั้งกรณี undefined และ [] ดังนั้น predicate นี้ robust ไม่ว่าฟิลด์จะหายไปหรือ (ในอนาคต) ถูกส่งเป็น array ว่าง
ตัวอย่าง 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"]
}
เพิ่มสมาชิกเดิม, no-op PUT, หรือรับคำเชิญซ้ำ (ไม่มีการเปลี่ยนแปลงจริง):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc"
}
ดำเนินการ bulk ที่ชน cap 5000 (ถูกตัดแบบเงียบ ๆ):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_0001", "u_0002", "/* … exactly 5000 entries total */"]
}
หากเห็น array ที่มี 5000 รายการพอดี ควร sync ด้วย GET /organizations/:id/users (หรือ /applications)
เหตุการณ์บทบาทองค์กร (Organization role events)
type OrganizationRole = {
id: string;
name: string;
description?: string;
};
type OrganizationScope = {
id: string;
name: string;
description?: string;
};
| Event | Field | Type | Optional | หมายเหตุ |
|---|---|---|---|---|
| OrganizationRole.Created | data | OrganizationRole | organization role entity ที่ถูกสร้าง | |
| OrganizationRole.Data.Updated | data | OrganizationRole | organization role entity ที่ถูกอัปเดต | |
| OrganizationRole.Deleted | data | null | / | |
| OrganizationRole.Scopes.Updated | data | null | / | |
| OrganizationRole.Scopes.Updated | organizationRoleId | string | ✅ | role ID ที่ scopes ถูกกำหนดให้ (มีเฉพาะเมื่อเหตุการณ์เกิดจากการสร้าง role พร้อมกำหนด scopes ล่วงหน้า) |
เหตุการณ์สิทธิ์องค์กร (Organization permission (scope) events)
| Event | Field | Type | Optional | หมายเหตุ |
|---|---|---|---|---|
| OrganizationScope.Created | data | OrganizationScope | organization scope entity ที่ถูกสร้าง | |
| OrganizationScope.Data.Updated | data | OrganizationScope | organization scope entity ที่ถูกอัปเดต | |
| OrganizationScope.Deleted | data | null | / |
Payload ของเหตุการณ์ exception
เหตุการณ์: Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded
เกิดขึ้นเมื่อมีเหตุการณ์ด้านความปลอดภัย เช่น บัญชีถูกล็อกหลังจากพยายามยืนยันตัวตนผิดหลายครั้ง หรือ grant ถูกยกเลิกเพราะเกินขีดจำกัด concurrent-device ของแอป
ทุกเหตุการณ์ exception จะมี ฟิลด์ร่วม (common fields) และฟิลด์ ip (รูปแบบเดียวกับ data-mutation events) ฟิลด์ที่เหลือขึ้นกับแต่ละเหตุการณ์
Identifier.Lockout
มาจาก user-facing flow ดังนั้น body จะมี Experience API context fields ด้วย และเพิ่ม:
enum SignInIdentifier {
Email = 'email',
Phone = 'phone',
Username = 'username',
}
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| type | SignInIdentifier | ประเภท identifier ของผู้ใช้ เช่น email, phone หรือ username | |
| value | string | ค่า identifier ของผู้ใช้ที่ทริกเกอร์ lockout |
Message.RateLimited
มาจาก user-facing flow ดังนั้น body จะมี Experience API context fields ด้วย และเพิ่ม:
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| action | string | action ที่ถูกจำกัด rate เช่น VerificationCodeSend | |
| recipient | string | อีเมลหรือเบอร์โทรที่ชน send rate limit |
Grant.LimitExceeded
เกิดขึ้นเมื่อการอนุญาต (authorization) สำเร็จแล้วทำให้ผู้ใช้เกินขีดจำกัด max concurrent authenticated devices (maxAllowedGrants) ของแอป และ Logto จะเพิกถอน grant ที่เก่าที่สุดของแอปนั้น
เหตุการณ์นี้ถูกส่งจาก OIDC authorization endpoint ไม่ใช่ Experience API ดังนั้นจะ ไม่มี interactionEvent หรือ sessionId นอกจากฟิลด์ร่วมและ ip แล้ว body จะมี:
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| userId | string | ผู้ใช้ที่ grant ถูกเพิกถอน | |
| applicationId | string | แอปที่เกินขีดจำกัด maxAllowedGrants | |
| application | ApplicationEntity | ✅ | application entity ไม่ใส่ถ้าไม่สามารถ resolve แอปได้ขณะส่ง |
| maxAllowedGrants | number | ขีดจำกัดที่ตั้งไว้บนแอปขณะเกิดเหตุการณ์ | |
| preRevocationActiveGrantCount | number | จำนวน grant ที่ผู้ใช้มีอยู่ก่อนถูกเพิกถอน (รวมอันที่เพิ่งออกล่าสุด) | |
| revokedGrantIds | string[] | ID ของ grant ที่ถูกเพิกถอนจริง (เรียงจากเก่าสุด) |
ตัวอย่าง 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"]
}
หมายเหตุการส่ง:
- ข้อมูล grant ที่ถูกเพิกถอนจะถูกลบออกจากระบบ ดังนั้น ID ใน
revokedGrantIdsจะไม่สามารถค้นหาได้ผ่าน endpoint รายการ grant หรือ Console อีกต่อไป — endpoint เหล่านั้นแสดงเฉพาะ grant ที่ยัง active เท่านั้น ให้ถือ payload นี้เป็นบันทึก และเก็บ ID ฝั่งคุณเองหากต้องการใช้ในอนาคต - เหตุการณ์นี้จะเกิดขึ้นเฉพาะเมื่อมี grant ถูกเพิกถอนจริง เท่านั้น การอนุญาตที่ยังอยู่ในขีดจำกัดจะไม่เกิด event
- จะเกิด event ทุกครั้งที่มีการอนุญาตเกินขีดจำกัด ดังนั้นผู้ใช้ที่ลงชื่อเข้าใช้จากอุปกรณ์เกินจำนวนที่กำหนดจะเกิด event หนึ่งต่อการถูกเพิกถอนแต่ละครั้ง
- การส่งเป็นแบบ fire-and-forget: endpoint ที่ช้าหรือ error จะไม่บล็อกหรือทำให้การอนุญาตของผู้ใช้ล้มเหลว การส่งที่ล้มเหลวจะถูกบันทึกใน audit log เช่นเดียวกับ webhook อื่น ๆ