คำขอ Webhooks (Webhooks request)
เมื่อเกิดเหตุการณ์ webhook ขึ้น Logto จะส่งคำขอ POST ไปยังทุก endpoint ที่สมัครรับเหตุการณ์นั้นไว้ แคตตาล็อกเหตุการณ์ทั้งหมดดูได้ที่ เหตุการณ์ Webhooks; หน้านี้อธิบาย รูปแบบของคำขอ ที่ Logto ส่งให้
ส่วนหัวของคำขอ
| Key | ปรับแต่งได้ | หมายเหตุ |
|---|---|---|
| user-agent | ✅ | ค่าเริ่มต้นคือ Logto (https://logto.io/) |
| content-type | ✅ | ค่าเริ่มต้นคือ application/json |
| logto-signature-sha-256 | ลายเซ็นของเนื้อหา request ดู การรักษาความปลอดภัย webhook ของคุณ |
ส่วนหัวที่ปรับแต่งได้สามารถ override ได้ผ่าน secure webhook
ภาพรวมของเนื้อหาในคำขอ
เนื้อหาหลักเป็นวัตถุ JSON รูปแบบที่แน่นอนขึ้นอยู่กับกลุ่มของเหตุการณ์ที่เกิดขึ้น:
| กลุ่ม | เหตุการณ์ | เมื่อใดที่เกิดขึ้น |
|---|---|---|
| User flow | PostRegister, PostSignIn, PostResetPassword | ผู้ใช้ทำ flow สมัครสมาชิก, ลงชื่อเข้าใช้ หรือรีเซ็ตรหัสผ่านสำเร็จโดย Experience API |
| Data mutation | User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* | โมเดลข้อมูลถูกเปลี่ยนแปลงโดย Management API หรือ user flow บน Experience API |
| Exception | Identifier.Lockout, Message.RateLimited, Grant.LimitExceeded | เหตุการณ์ด้านความปลอดภัย เช่น บัญชีถูกล็อกหลังจากพยายามยืนยันตัวตนผิดหลายครั้ง |
ทุกกลุ่มจะมีชุด ฟิลด์ร่วม เล็ก ๆ จากนั้นแต่ละกลุ่มจะเพิ่มฟิลด์ context เฉพาะของตัวเองและ payload เฉพาะเหตุการณ์
ฟิลด์ร่วม (Common fields)
มีอยู่ในทุกการส่ง ไม่ว่ากลุ่มใด:
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| hookId | string | รหัสการตั้งค่า webhook ใน Logto | |
| deliveryId | string | รหัสของการส่งนี้ ไม่ซ้ำกันต่อ webhook และ event และคงอยู่ระหว่าง การ retry อัตโนมัติ ของคำขอเดียวกัน ดู การ deduplicate การส่ง | |
| event | string | เหตุการณ์ที่ทริกเกอร์การส่งนี้ | |
| createdAt | string | เวลาสร้าง payload ในรูปแบบ ISO 8601 | |
| userAgent | string | ✅ | user-agent ของคำขอที่ทริกเกอร์เหตุการณ์นี้ |
แต่ละกลุ่มยังมี IP address ของคำขอที่ทริกเกอร์เหตุการณ์ โดยใช้ชื่อฟิลด์ userIp สำหรับ user-flow events และ ip สำหรับ data-mutation และ exception events ความหมายเหมือนกัน ต่างกันเพื่อความเข้ากันได้กับเวอร์ชันเก่า
การ deduplicate การส่ง (Deduplicating deliveries)
การส่งหนึ่งอาจถึง endpoint ของคุณมากกว่าหนึ่งครั้ง: Logto retry คำขอที่ตอบกลับด้วยสถานะ 500 ขึ้นไป ดังนั้นหาก endpoint ของคุณประมวลผลแล้วแต่ยังตอบ 5xx (เช่น gateway ข้างหน้าตอบ 504 เพราะ handler ช้า) ก็จะได้รับอีกครั้ง ทุกความพยายามของการส่งเดียวกันจะมี deliveryId เดียวกัน ในขณะที่ event ใหม่ หรือ event เดียวกันที่ส่งไป webhook อื่น จะได้ id ใหม่ ให้บันทึก deliveryId ของ payload ที่คุณประมวลผลแล้ว และข้าม payload ที่ id ซ้ำ
deliveryId ยังเป็น id ของ log การตรวจสอบ (audit log) ที่บันทึกการส่งนี้ ดังนั้นค่าที่รายงานโดยบริการของคุณสามารถเปิดดูจาก recent requests ของ webhook ใน Console หรือดึงด้วย GET /api/logs/:id
test payload ก็มี deliveryId เช่นกัน ดังนั้น handler เดียวกันสามารถประมวลผลได้ แม้ว่า test requests จะไม่ถูกบันทึก log
Payload ของเหตุการณ์ user flow
เหตุการณ์: PostRegister, PostSignIn, PostResetPassword
ทริกเกอร์เมื่อผู้ใช้ทำ flow สมัครสมาชิก, ลงชื่อเข้าใช้ หรือรีเซ็ตรหัสผ่านสำเร็จโดย Experience API นอกจาก ฟิลด์ร่วม แล้ว 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;
};
ดู ผู้ใช้ และ แอปพลิเคชัน สำหรับรายละเอียดฟิลด์ทั้งหมด
Payload ของเหตุการณ์ data mutation
เหตุการณ์: ทุกเหตุการณ์ภายใต้ User.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.* ดู เหตุการณ์ Webhooks → Data mutation webhook events สำหรับแคตตาล็อกเต็ม
body จะมีเสมอ:
- ฟิลด์ร่วม
- ฟิลด์
ipคือ IP address ของคำขอที่ทริกเกอร์ (optional, มีเมื่อทราบ) - API context อธิบายว่าเปลี่ยนแปลงนี้เกิดจากอะไร context มี 2 แบบขึ้นกับแหล่งที่มา:
- Experience API context เมื่อเปลี่ยนแปลงมาจาก user-facing flow
- Management API context เมื่อเปลี่ยนแปลงมาจากการเรียก Management API โดยตรง
- payload เฉพาะเหตุการณ์: entity ที่ได้รับผลกระทบใน
dataและ (สำหรับบางเหตุการณ์) ฟิลด์ระดับบนเพิ่มเติม ดู payload ข้อมูลเฉพาะเหตุการณ์
ฟิลด์ context ของ Experience API
มีเมื่อเปลี่ยนแปลงถูกทริกเกอร์โดย 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 ถ้ามี |
ฟิลด์ context ของ Management API
มีเมื่อเปลี่ยนแปลงถูกทริกเกอร์โดยการเรียก Management API
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| path | string | ✅ | path ของ API call ที่ทริกเกอร์ webhook นี้ |
| method | string | ✅ | HTTP method ของ API call |
| status | number | ✅ | รหัสสถานะตอบกลับของ API call |
| params | object | ✅ | koa path params ของ API call |
| matchedRoute | string | ✅ | koa matched route Logto ใช้ฟิลด์นี้จับคู่ webhook event filter ที่เปิดใช้งาน |
Payload ข้อมูลเฉพาะเหตุการณ์
ทุก data-mutation event จะมีฟิลด์ระดับบน data ที่บรรจุ entity ที่ได้รับผลกระทบ หรือ null เมื่อเปลี่ยนแปลงนั้นสรุปเป็น entity เดียวไม่ได้ (เช่น delete และ membership events) บางเหตุการณ์จะมีฟิลด์ระดับบนเฉพาะเหตุการณ์นอกเหนือจาก data เช่น Organization.Membership.Updated ดูรายละเอียดด้านล่าง
เหตุการณ์ผู้ใช้
| 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 ถูกกำหนดให้ (มีเฉพาะเมื่อ event ถูกทริกเกอร์โดยการสร้าง role พร้อม scopes ล่วงหน้า) |
เหตุการณ์สิทธิ์ (Scope)
| 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 ระดับบน ดู Organization.Membership.Updated payload |
Payload ของ Organization.Membership.Updated
นอกจาก ฟิลด์ร่วม และฟิลด์ API-context ที่เกี่ยวข้องกับแหล่งที่มา (Management API context สำหรับ Management API, Experience API context สำหรับ just-in-time provisioning) เหตุการณ์ Organization.Membership.Updated จะมี organizationId และ delta array ระดับบน (ข้าง ๆ event, createdAt ฯลฯ ไม่ใช่ ใน data ซึ่งจะเป็น null เสมอสำหรับ event นี้)
| Field | Type | Optional | หมายเหตุ |
|---|---|---|---|
| organizationId | string | องค์กรที่ membership เปลี่ยนแปลง | |
| addedUserIds | string[] | ✅ | User ID ที่ถูกเพิ่มใหม่โดย trigger นี้ ไม่มีเมื่อไม่มีการเพิ่ม user หรือ trigger ไม่เกี่ยวกับ user membership |
| removedUserIds | string[] | ✅ | User ID ที่ถูกลบโดย trigger นี้ ไม่มีเมื่อไม่มีการลบ user |
| addedApplicationIds | string[] | ✅ | Application ID ที่ถูกเพิ่มใหม่ ไม่มีเมื่อไม่มีการเพิ่ม application หรือ trigger ไม่เกี่ยวกับ application membership |
| removedApplicationIds | string[] | ✅ | Application ID ที่ถูกลบ ไม่มีเมื่อไม่มีการลบ application |
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 เมื่อเพิ่ม user เข้าสู่องค์กรใหม่ | addedUserIds |
delta array ว่างจะถูกละเว้น (absent ≠ empty change)
delta array ที่ว่างจะ ถูกละเว้นทั้งหมด จาก payload เช่น PUT /organizations/:id/users ที่แทนที่สมาชิกด้วยชุดเดิมจะไม่มีการเปลี่ยนแปลงจริง และ payload จะเหลือแค่ { organizationId } โดยไม่มี delta field ทั้งสี่ เช่นเดียวกับการเพิ่มสมาชิกซ้ำหรือรับคำเชิญซ้ำ
consumer ต้องถือว่าฟิลด์ที่ขาดหายคือ "ไม่มีการเปลี่ยนแปลงฝั่งนั้น" ไม่ใช่ "เปลี่ยนแปลงว่าง"
จำกัดจำนวนต่อ array (silent truncation)
แต่ละ delta array จำกัดที่ 5000 รายการ หาก Management API call เดียวเพิ่มหรือลบ user (หรือ application) มากกว่า 5000 รายการ array นั้นจะถูกตัดเหลือ 5000 รายการแรก ไม่มี marker ใน payload ว่าถูกตัด
หากแอปของคุณมี bulk operation ที่อาจกระทบสมาชิกเกิน 5000 คนต่อครั้ง ให้ถือ array ที่มี 5000 รายการว่าเป็นสัญญาณให้ sync สมาชิก authoritative ผ่าน Management API:
GET /organizations/:id/users: สมาชิก user ทั้งหมดGET /organizations/:id/applications: สมาชิก application ทั้งหมด
แนวทางนี้เหมือนกับ event push ของ GitHub ที่จำกัด commits ที่ 20 รายการและแนะนำให้ใช้ compare API เพื่อดูรายการเต็ม
ข้าม event ที่ไม่มีการเปลี่ยนแปลง (no-op)
เพื่อข้ามการส่งที่ไม่มีการเปลี่ยนแปลง (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
เพิ่ม user (POST /organizations/:id/users):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedUserIds": ["u_001"]
}
แทนที่ชุดสมาชิก user (PUT /organizations/:id/users):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"addedUserIds": ["u_002"],
"removedUserIds": ["u_001"]
}
ลบ user (DELETE /organizations/:id/users/:userId):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_001"]
}
เพิ่ม application (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 operation ที่ชน cap 5000 (ถูกตัดแบบเงียบ ๆ):
{
"event": "Organization.Membership.Updated",
"organizationId": "org_abc",
"removedUserIds": ["u_0001", "u_0002", "/* … exactly 5000 entries total */"]
}
เห็น array ที่มี 5000 รายการควร trigger GET /organizations/:id/users (หรือ /applications) เพื่อ sync
เหตุการณ์บทบาทองค์กร (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 ถูกกำหนดให้ (มีเฉพาะเมื่อ event ถูกทริกเกอร์โดยการสร้าง 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 event จะมี ฟิลด์ร่วม และฟิลด์ ip (รูปแบบเดียวกับ data-mutation events) ฟิลด์ที่เหลือขึ้นกับแต่ละ event
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-limit เช่น VerificationCodeSend | |
| recipient | string | email หรือเบอร์โทรศัพท์ที่ชน send rate limit |
Grant.LimitExceeded
ทริกเกอร์เมื่อการอนุญาต (authorization) สำเร็จทำให้ผู้ใช้เกินขีดจำกัด max concurrent authenticated devices (maxAllowedGrants) ของแอป และ Logto จะเพิกถอน grant เก่าที่สุดของแอปนั้น
event นี้ถูกส่งจาก 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 | ขีดจำกัดที่ตั้งไว้บนแอปขณะเกิด event | |
| preRevocationActiveGrantCount | number | จำนวน grant ที่ active ที่ผู้ใช้มีสำหรับแอปนี้ก่อนถูกเพิกถอน รวมอันที่เพิ่งออกด้วย | |
| revokedGrantIds | string[] | ID ของ grant ที่ถูกเพิกถอนจริง (เรียงจากเก่าไปใหม่) |
ตัวอย่าง 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"]
}
หมายเหตุการส่ง:
- บันทึก grant ที่ถูกเพิกถอนจะถูกลบเป็นส่วนหนึ่งของการเพิกถอน ดังนั้น ID ใน
revokedGrantIdsจะไม่สามารถค้นหาได้ผ่าน endpoint รายการ grant หรือ Console — เหล่านั้นจะแสดงเฉพาะ grant ที่ active เท่านั้น ให้ถือ payload นี้เป็นบันทึก และเก็บ ID ฝั่งคุณเองหากต้องการใช้ในอนาคต - event จะถูกส่งเฉพาะเมื่อมี grant ถูกเพิกถอน จริง การอนุญาตที่ยังอยู่ในขีดจำกัดจะไม่เกิด event
- จะถูกส่งทุกครั้งที่การอนุญาตเกินขีดจำกัด ดังนั้นผู้ใช้ที่ลงชื่อเข้าใช้จากอุปกรณ์เกินจำนวนที่อนุญาตจะได้ event หนึ่งต่อการเพิกถอนแต่ละครั้ง
- การส่งเป็นแบบ fire-and-forget: endpoint ที่ช้าหรือผิดพลาดจะไม่บล็อกหรือทำให้การอนุญาตของผู้ใช้ล้มเหลว การส่งที่ล้มเหลวจะถูกบันทึกใน audit log เช่นเดียวกับ webhook อื่น ๆ