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

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

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

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

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

FieldTypeOptionalหมายเหตุ
hookIdstringรหัสการตั้งค่า webhook ใน Logto
eventstringเหตุการณ์ที่ทริกเกอร์การส่งนี้
createdAtstringเวลาสร้าง payload ในรูปแบบ ISO 8601
userAgentstringuser-agent ของคำขอที่ทริกเกอร์เหตุการณ์

แต่ละกลุ่มยังมี IP address ของคำขอที่ทริกเกอร์เหตุการณ์ โดยใช้ชื่อฟิลด์ userIp สำหรับ user-flow events และ ip สำหรับ data-mutation และ exception events ความหมายเหมือนกัน ต่างกันแค่ชื่อเพื่อความเข้ากันได้ย้อนหลัง

Payload ของเหตุการณ์ user flow

เหตุการณ์: PostRegister, PostSignIn, PostResetPassword

เกิดขึ้นเมื่อผู้ใช้ดำเนินการลงทะเบียน, ลงชื่อเข้าใช้ หรือรีเซ็ตรหัสผ่านผ่าน Experience API นอกจาก ฟิลด์ร่วม (common fields) แล้ว body จะมี:

FieldTypeOptionalหมายเหตุ
interactionEvent'SignIn' | 'Register' | 'ForgotPassword'ประเภทเหตุการณ์ user-flow แมปกับ PostSignIn / PostRegister / PostResetPassword ชื่อฟิลด์ยังคงใช้ชื่อ "interaction" เดิม
sessionIdstringSession ID (ไม่ใช่ Interaction ID) ของเหตุการณ์นี้ ถ้ามี
userIpstringIP address ของคำขอที่ทริกเกอร์เหตุการณ์
userIdstringUser ID ที่เกี่ยวข้องกับเหตุการณ์นี้ ถ้ามี
userUserEntityuser entity ที่เกี่ยวข้องกับเหตุการณ์นี้ ถ้ามี
applicationIdstringApplication ID ที่เกี่ยวข้องกับเหตุการณ์นี้ ถ้ามี
applicationApplicationEntityapplication 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 ระหว่างอัปเดตโปรไฟล์

FieldTypeOptionalหมายเหตุ
interactionEvent'SignIn' | 'Register' | 'ForgotPassword'ประเภทเหตุการณ์ user-flow ที่ทำให้เกิดการเปลี่ยนแปลง ชื่อฟิลด์ยังคงใช้ชื่อ "interaction" เดิม
sessionIdstringSession ID (ไม่ใช่ Interaction ID) ของเหตุการณ์นี้ ถ้ามี
applicationIdstringApplication ID ถ้ามี
applicationApplicationEntityapplication entity ถ้ามี

ฟิลด์ Management API context

มีเมื่อการเปลี่ยนแปลงเกิดจากการเรียก Management API

FieldTypeOptionalหมายเหตุ
pathstringpath ของ API call ที่ทริกเกอร์ webhook นี้
methodstringHTTP method ของ API call
statusnumberresponse status code ของ API call
paramsobjectkoa path params ของ API call
matchedRoutestringkoa matched route Logto ใช้ฟิลด์นี้จับคู่ webhook event filter ที่เปิดใช้งาน

Payload ข้อมูลเฉพาะเหตุการณ์ (Event-specific data payloads)

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

เหตุการณ์ผู้ใช้ (User events)

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.UpdatedroleIdstringrole ID ที่ scopes ถูกกำหนดให้ (มีเฉพาะเมื่อเหตุการณ์เกิดจากการสร้าง role พร้อมกำหนด scopes ล่วงหน้า)

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

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 ระดับบน (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 เสมอสำหรับเหตุการณ์นี้)

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

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 เมื่อเพิ่มผู้ใช้สู่องค์กรใหม่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;
};
EventFieldTypeOptionalหมายเหตุ
OrganizationRole.CreateddataOrganizationRoleorganization role entity ที่ถูกสร้าง
OrganizationRole.Data.UpdateddataOrganizationRoleorganization role entity ที่ถูกอัปเดต
OrganizationRole.Deleteddatanull/
OrganizationRole.Scopes.Updateddatanull/
OrganizationRole.Scopes.UpdatedorganizationRoleIdstringrole ID ที่ scopes ถูกกำหนดให้ (มีเฉพาะเมื่อเหตุการณ์เกิดจากการสร้าง 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 จะมี ฟิลด์ร่วม (common fields) และฟิลด์ ip (รูปแบบเดียวกับ data-mutation events) ฟิลด์ที่เหลือขึ้นกับแต่ละเหตุการณ์

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 เช่น VerificationCodeSend
recipientstringอีเมลหรือเบอร์โทรที่ชน send rate limit

Grant.LimitExceeded

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

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

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