ข้ามไปยังเนื้อหาหลัก

คำขอ 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 flowPostRegister, PostSignIn, PostResetPasswordผู้ใช้ทำ flow สมัครสมาชิก, ลงชื่อเข้าใช้ หรือรีเซ็ตรหัสผ่านสำเร็จโดย Experience API
Data mutationUser.*, Role.*, Scope.*, Organization.*, OrganizationRole.*, OrganizationScope.*โมเดลข้อมูลถูกเปลี่ยนแปลงโดย Management API หรือ user flow บน Experience API
ExceptionIdentifier.Lockout, Message.RateLimited, Grant.LimitExceededเหตุการณ์ด้านความปลอดภัย เช่น บัญชีถูกล็อกหลังจากพยายามยืนยันตัวตนผิดหลายครั้ง

ทุกกลุ่มจะมีชุด ฟิลด์ร่วม เล็ก ๆ จากนั้นแต่ละกลุ่มจะเพิ่มฟิลด์ context เฉพาะของตัวเองและ payload เฉพาะเหตุการณ์

ฟิลด์ร่วม (Common fields)​

มีอยู่ในทุกการส่ง ไม่ว่ากลุ่มใด:

FieldTypeOptionalหมายเหตุ
hookIdstringรหัสการตั้งค่า webhook ใน Logto
deliveryIdstringรหัสของการส่งนี้ ไม่ซ้ำกันต่อ webhook และ event และคงอยู่ระหว่าง การ retry อัตโนมัติ ของคำขอเดียวกัน ดู การ deduplicate การส่ง
eventstringเหตุการณ์ที่ทริกเกอร์การส่งนี้
createdAtstringเวลาสร้าง payload ในรูปแบบ ISO 8601
userAgentstring✅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 จะมี:

FieldTypeOptionalหมายเหตุ
interactionEvent'SignIn' | 'Register' | 'ForgotPassword'ประเภทเหตุการณ์ user-flow แมปกับ PostSignIn / PostRegister / PostResetPassword ชื่อฟิลด์ยังคงใช้ชื่อ "interaction" เดิม
sessionIdstring✅Session ID (ไม่ใช่ Interaction ID) สำหรับเหตุการณ์นี้ ถ้ามี
userIpstring✅IP address ของคำขอที่ทริกเกอร์
userIdstring✅User ID ที่เกี่ยวข้องกับเหตุการณ์นี้ ถ้ามี
userUserEntity✅user entity ที่เกี่ยวข้องกับเหตุการณ์นี้ ถ้ามี
applicationIdstring✅Application ID ที่เกี่ยวข้องกับเหตุการณ์นี้ ถ้ามี
applicationApplicationEntity✅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 ระหว่างอัปเดตโปรไฟล์

FieldTypeOptionalหมายเหตุ
interactionEvent'SignIn' | 'Register' | 'ForgotPassword'✅ประเภทเหตุการณ์ user-flow ที่ก่อให้เกิดการเปลี่ยนแปลง ชื่อฟิลด์ยังคงใช้ชื่อ "interaction" เดิม
sessionIdstring✅Session ID (ไม่ใช่ Interaction ID) สำหรับเหตุการณ์นี้ ถ้ามี
applicationIdstring✅Application ID ถ้ามี
applicationApplicationEntity✅application entity ถ้ามี

ฟิลด์ context ของ Management API​

มีเมื่อเปลี่ยนแปลงถูกทริกเกอร์โดยการเรียก Management API

FieldTypeOptionalหมายเหตุ
pathstring✅path ของ API call ที่ทริกเกอร์ webhook นี้
methodstring✅HTTP method ของ API call
statusnumber✅รหัสสถานะตอบกลับของ API call
paramsobject✅koa path params ของ API call
matchedRoutestring✅koa matched route Logto ใช้ฟิลด์นี้จับคู่ webhook event filter ที่เปิดใช้งาน

Payload ข้อมูลเฉพาะเหตุการณ์​

ทุก data-mutation event จะมีฟิลด์ระดับบน data ที่บรรจุ entity ที่ได้รับผลกระทบ หรือ null เมื่อเปลี่ยนแปลงนั้นสรุปเป็น entity เดียวไม่ได้ (เช่น delete และ membership events) บางเหตุการณ์จะมีฟิลด์ระดับบนเฉพาะเหตุการณ์นอกเหนือจาก data เช่น Organization.Membership.Updated ดูรายละเอียดด้านล่าง

เหตุการณ์ผู้ใช้​

EventFieldTypeOptionalหมายเหตุ
User.CreateddataUserEntityuser entity ที่ถูกสร้าง
User.Data.UpdateddataUserEntityuser entity ที่ถูกอัปเดต
User.Deleteddatanull/

เหตุการณ์บทบาท (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;
};
EventFieldTypeOptionalหมายเหตุ
Role.CreateddataRolerole entity ที่ถูกสร้าง
Role.Data.UpdateddataRolerole entity ที่ถูกอัปเดต
Role.Deleteddatanull/
Role.Scopes.UpdateddataScope[]scopes ที่อัปเดตให้กับ role
Role.Scopes.UpdatedroleIdstring✅role ID ที่ scopes ถูกกำหนดให้ (มีเฉพาะเมื่อ event ถูกทริกเกอร์โดยการสร้าง role พร้อม scopes ล่วงหน้า)

เหตุการณ์สิทธิ์ (Scope)​

EventFieldTypeOptionalหมายเหตุ
Scope.CreateddataScopescope entity ที่ถูกสร้าง
Scope.Data.UpdateddataScopescope entity ที่ถูกอัปเดต
Scope.Deleteddatanull/

เหตุการณ์องค์กร (Organization events)​

type Organization = {
id: string;
name: string;
description?: string;
customData: object;
createdAt: number;
};
EventFieldTypeOptionalหมายเหตุ
Organization.CreateddataOrganizationorganization entity ที่ถูกสร้าง
Organization.Data.UpdateddataOrganizationorganization entity ที่ถูกอัปเดต
Organization.Deleteddatanull/
Organization.Membership.Updateddatanull/การเปลี่ยนแปลงอธิบายด้วย 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 นี้)

FieldTypeOptionalหมายเหตุ
organizationIdstringองค์กรที่ membership เปลี่ยนแปลง
addedUserIdsstring[]✅User ID ที่ถูกเพิ่มใหม่โดย trigger นี้ ไม่มีเมื่อไม่มีการเพิ่ม user หรือ trigger ไม่เกี่ยวกับ user membership
removedUserIdsstring[]✅User ID ที่ถูกลบโดย trigger นี้ ไม่มีเมื่อไม่มีการลบ user
addedApplicationIdsstring[]✅Application ID ที่ถูกเพิ่มใหม่ ไม่มีเมื่อไม่มีการเพิ่ม application หรือ trigger ไม่เกี่ยวกับ application membership
removedApplicationIdsstring[]✅Application ID ที่ถูกลบ ไม่มีเมื่อไม่มีการลบ application

delta array ทั้งสี่นี้ optional และ additive: ไม่เปลี่ยนรูปแบบ payload เดิมสำหรับ consumer ที่ไม่รองรับ และฟิลด์ data: null แบบเดิมยังคงมีอยู่

trigger และ delta field ที่อาจเกิดขึ้น​
Triggerdelta field ที่อาจมี
POST /organizations/:id/usersaddedUserIds
PUT /organizations/:id/usersaddedUserIds, removedUserIds
DELETE /organizations/:id/users/:userIdremovedUserIds
POST /organizations/:id/applicationsaddedApplicationIds
PUT /organizations/:id/applicationsaddedApplicationIds, removedApplicationIds
DELETE /organizations/:id/applications/:applicationIdremovedApplicationIds
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;
};
EventFieldTypeOptionalหมายเหตุ
OrganizationRole.CreateddataOrganizationRoleorganization role entity ที่ถูกสร้าง
OrganizationRole.Data.UpdateddataOrganizationRoleorganization role entity ที่ถูกอัปเดต
OrganizationRole.Deleteddatanull/
OrganizationRole.Scopes.Updateddatanull/
OrganizationRole.Scopes.UpdatedorganizationRoleIdstring✅role ID ที่ scopes ถูกกำหนดให้ (มีเฉพาะเมื่อ event ถูกทริกเกอร์โดยการสร้าง role พร้อม scopes ล่วงหน้า)

เหตุการณ์สิทธิ์องค์กร (Organization permission (scope) events)​

EventFieldTypeOptionalหมายเหตุ
OrganizationScope.CreateddataOrganizationScopeorganization scope entity ที่ถูกสร้าง
OrganizationScope.Data.UpdateddataOrganizationScopeorganization scope entity ที่ถูกอัปเดต
OrganizationScope.Deleteddatanull/

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',
}
FieldTypeOptionalหมายเหตุ
typeSignInIdentifierประเภท identifier ของผู้ใช้ เช่น email, phone หรือ username
valuestringค่า identifier ของผู้ใช้ที่ทริกเกอร์ lockout

Message.RateLimited​

มาจาก user-facing flow ดังนั้น body จะมี Experience API context fields ด้วย และ:

FieldTypeOptionalหมายเหตุ
actionstringaction ที่ถูก rate-limit เช่น VerificationCodeSend
recipientstringemail หรือเบอร์โทรศัพท์ที่ชน send rate limit

Grant.LimitExceeded​

ทริกเกอร์เมื่อการอนุญาต (authorization) สำเร็จทำให้ผู้ใช้เกินขีดจำกัด max concurrent authenticated devices (maxAllowedGrants) ของแอป และ Logto จะเพิกถอน grant เก่าที่สุดของแอปนั้น

event นี้ถูกส่งจาก OIDC authorization endpoint ไม่ใช่ Experience API ดังนั้นจะ ไม่มี interactionEvent หรือ sessionId นอกจากฟิลด์ร่วมและ ip แล้ว body จะมี:

FieldTypeOptionalหมายเหตุ
userIdstringผู้ใช้ที่ grant ถูกเพิกถอน
applicationIdstringแอปที่เกินขีดจำกัด maxAllowedGrants
applicationApplicationEntity✅application entity ไม่มีถ้า resolve ไม่ได้ขณะส่ง
maxAllowedGrantsnumberขีดจำกัดที่ตั้งไว้บนแอปขณะเกิด event
preRevocationActiveGrantCountnumberจำนวน grant ที่ active ที่ผู้ใช้มีสำหรับแอปนี้ก่อนถูกเพิกถอน รวมอันที่เพิ่งออกด้วย
revokedGrantIdsstring[]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 อื่น ๆ