Customer Management¶
Complete guide to managing customers, preferences, appointment history, and analytics in the Reserva platform.
Overview¶
The customer management system provides comprehensive tools for:
- Customer CRUD Operations - Create, read, update, and delete customer profiles
- Advanced Search - Full-text search across name, email, and phone
- Duplicate Detection - Automatic validation to prevent duplicate entries
- Appointment History - Track customer booking history and statistics
- Preference Management - Service, staff, and communication preferences
- Analytics & Reporting - Customer statistics and business insights
- Walk-in Support - Customers without password (staff-created)
- Self-Registration - Customers with password (portal access)
Key Concepts:
- Walk-in Customer = Staff-created, no password, no portal access
- Registered Customer = Self-created or with password, has portal access
- Soft Delete = Customer marked inactive, data preserved for audit
- Hard Delete = Permanent removal (SUPER_ADMIN only)
- Tenant Isolation = All operations scoped to current tenant
List All Customers¶
Retrieve all customers with advanced search and filtering capabilities.
Endpoint¶
Authentication: Required (JWT token) Access: All staff members
Query Parameters¶
| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
page |
integer | No | Page number (default: 1) | 1 |
size |
integer | No | Items per page (default: 20, max: 100) | 20 |
search |
string | No | Search in name, email, or phone | "John" |
tags |
string | No | Comma-separated tags to filter | "vip,regular" |
has_password |
boolean | No | Filter by password presence (True=registered, False=walk-in) | true |
email_verified |
boolean | No | Filter by email verification status | true |
created_from |
date | No | Filter customers created from this date | "2025-01-01" |
created_to |
date | No | Filter customers created until this date | "2025-12-31" |
Response¶
{
"items": [
{
"id": "507f1f77bcf86cd799439011",
"tenant_id": "507f1f77bcf86cd799439010",
"email": "john.doe@example.com",
"phone": "+628123456789",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"date_of_birth": "1990-05-15",
"gender": "male",
"address": "Jl. Sudirman No. 123, Jakarta",
"avatar_url": "https://cdn.example.com/avatars/john.jpg",
"tags": ["vip", "regular", "staff-created"],
"email_verified": true,
"is_active": true,
"preferences": {
"preferred_services": ["507f1f77bcf86cd799439020"],
"preferred_staff": ["507f1f77bcf86cd799439021"],
"preferred_outlets": ["507f1f77bcf86cd799439022"],
"communication_channels": {
"sms": true,
"email": true,
"push": false
},
"marketing_consent": true,
"language": "id"
},
"loyalty_points": 1250,
"total_spent": 5000000,
"total_appointments": 28,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2025-01-20T14:45:00Z"
}
],
"total": 145,
"page": 1,
"size": 20,
"pages": 8
}
Search Behavior¶
Full-text search across:
- First name and last name
- Email address
- Phone number
Tag filtering:
- Supports multiple tags (comma-separated)
- Returns customers matching ANY of the specified tags
- Common tags:
vip,regular,walk-in,staff-created
Subscription Limits¶
Customer list access is not limited by subscription plan. However, the number of active customers may be tracked for usage analytics.
Usage Tracking:
- Total active customers count
- Customer growth metrics
- Available in Usage Dashboard for PRO and ENTERPRISE plans
See Subscription Management - Usage Tracking for details.
Create New Customer¶
Create a new customer with automatic duplicate detection.
Endpoint¶
Authentication: Required (JWT token) Access: All staff members
Request Body¶
{
"email": "jane.smith@example.com",
"phone": "+628123456780",
"first_name": "Jane",
"last_name": "Smith",
"date_of_birth": "1995-08-20",
"gender": "female",
"address": "Jl. Thamrin No. 45, Jakarta",
"password": "SecurePass123!",
"tags": ["new-customer"],
"preferences": {
"communication_channels": {
"sms": true,
"email": true,
"push": true
},
"marketing_consent": true,
"language": "en"
}
}
Query Parameters¶
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
check_duplicate |
boolean | No | Check for duplicate customers | true |
Request Fields¶
| Field | Type | Required | Description | Validation |
|---|---|---|---|---|
email |
string | Yes* | Customer email address | Valid email format, unique per tenant |
phone |
string | Yes* | Customer phone number | Valid phone format, unique per tenant |
first_name |
string | Yes | Customer first name | 1-100 characters |
last_name |
string | No | Customer last name | 1-100 characters |
date_of_birth |
date | No | Customer birth date | Valid date, not in future |
gender |
string | No | Customer gender | male, female, other |
address |
string | No | Customer address | Max 500 characters |
password |
string | No | Password for customer portal access | Min 8 characters if provided |
avatar_url |
string | No | Profile picture URL | Valid URL |
tags |
array | No | Customer tags for categorization | Array of strings |
preferences |
object | No | Customer preferences | See CustomerPreferences schema |
Note: Either email OR phone must be provided (at least one required).
Response¶
{
"id": "507f1f77bcf86cd799439030",
"tenant_id": "507f1f77bcf86cd799439010",
"email": "jane.smith@example.com",
"phone": "+628123456780",
"first_name": "Jane",
"last_name": "Smith",
"full_name": "Jane Smith",
"date_of_birth": "1995-08-20",
"gender": "female",
"address": "Jl. Thamrin No. 45, Jakarta",
"avatar_url": null,
"tags": ["new-customer", "staff-created"],
"email_verified": false,
"is_active": true,
"preferences": {
"preferred_services": [],
"preferred_staff": [],
"preferred_outlets": [],
"communication_channels": {
"sms": true,
"email": true,
"push": true
},
"marketing_consent": true,
"language": "en"
},
"loyalty_points": 0,
"total_spent": 0,
"total_appointments": 0,
"created_at": "2025-01-22T09:15:00Z",
"updated_at": "2025-01-22T09:15:00Z"
}
Business Rules¶
-
Duplicate Detection:
-
Email must be unique within tenant
- Phone must be unique within tenant
-
Set
check_duplicate=falseto bypass (not recommended) -
Password Handling:
-
If password provided → Customer can access portal
- If no password → Walk-in customer, no portal access
-
Password is hashed using bcrypt before storage
-
Automatic Tagging:
-
staff-createdtag added automatically -
Additional tags can be specified in request
-
Default Preferences:
-
All communication channels enabled by default
- Marketing consent defaults to false
- Language defaults to tenant's default language
Error Responses¶
409 Conflict - Duplicate Email:
409 Conflict - Duplicate Phone:
400 Bad Request - Validation Error:
{
"detail": [
{
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"type": "value_error.email"
}
]
}
Subscription Limits¶
Customer creation is NOT directly limited by subscription plans. However, subscription plans may limit related features:
FREE Plan:
- Unlimited customer creation
- Limited to 100 appointments/month (affects customer bookings)
- Single outlet only
PRO Plan:
- Unlimited customer creation
- 2,000 appointments/month
- Up to 10 outlets
ENTERPRISE Plan:
- Unlimited everything
See Subscription Management - Available Plans for complete plan details.
Get Customer Details¶
Retrieve detailed information about a specific customer.
Endpoint¶
Authentication: Required (JWT token) Access: All staff members
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_id |
string | Yes | Customer ID (MongoDB ObjectId) |
Response¶
{
"id": "507f1f77bcf86cd799439011",
"tenant_id": "507f1f77bcf86cd799439010",
"email": "john.doe@example.com",
"phone": "+628123456789",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"date_of_birth": "1990-05-15",
"gender": "male",
"address": "Jl. Sudirman No. 123, Jakarta",
"avatar_url": "https://cdn.example.com/avatars/john.jpg",
"tags": ["vip", "regular"],
"email_verified": true,
"is_active": true,
"preferences": {
"preferred_services": ["507f1f77bcf86cd799439020"],
"preferred_staff": ["507f1f77bcf86cd799439021"],
"preferred_outlets": ["507f1f77bcf86cd799439022"],
"communication_channels": {
"sms": true,
"email": true,
"push": false
},
"marketing_consent": true,
"language": "id"
},
"loyalty_points": 1250,
"total_spent": 5000000,
"total_appointments": 28,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2025-01-20T14:45:00Z"
}
Error Responses¶
404 Not Found:
403 Forbidden - Wrong Tenant:
Update Customer¶
Update customer information with duplicate validation.
Endpoint¶
Authentication: Required (JWT token) Access: TENANT_ADMIN, OUTLET_MANAGER, or SUPER_ADMIN
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_id |
string | Yes | Customer ID (MongoDB ObjectId) |
Request Body¶
All fields are optional. Only provided fields will be updated.
{
"email": "john.newemail@example.com",
"phone": "+628123456790",
"first_name": "Jonathan",
"last_name": "Doe",
"date_of_birth": "1990-05-15",
"gender": "male",
"address": "Jl. Sudirman No. 456, Jakarta",
"avatar_url": "https://cdn.example.com/avatars/john-new.jpg",
"tags": ["vip", "premium"],
"is_active": true
}
Response¶
Returns updated customer profile (same format as Get Customer Details).
Business Rules¶
-
Duplicate Validation:
-
Email uniqueness checked if email is changed
- Phone uniqueness checked if phone is changed
-
Returns 409 Conflict if duplicate found
-
Protected Fields:
-
tenant_idcannot be changed created_atcannot be changed-
loyalty_points,total_spent,total_appointmentsmanaged by system -
Tag Management:
-
Tags array completely replaces existing tags
staff-createdtag preserved automatically
Error Responses¶
409 Conflict - Duplicate Email:
409 Conflict - Duplicate Phone:
Delete Customer¶
Delete a customer with soft delete option for data preservation.
Endpoint¶
Authentication: Required (JWT token) Access: TENANT_ADMIN or SUPER_ADMIN
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_id |
string | Yes | Customer ID (MongoDB ObjectId) |
Query Parameters¶
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
permanent |
boolean | No | Permanently delete (True) or soft delete (False) | false |
Response¶
Delete Types¶
Soft Delete (Default):
- Sets
is_deleted=Trueandis_active=False - Customer data preserved in database
- Excluded from normal queries
- Can be restored by admin
- Preserves appointment history
Hard Delete (permanent=true):
- Permanently removes customer from database
- Requires SUPER_ADMIN role
- Cannot be undone
- Use with extreme caution
Business Rules¶
-
Data Preservation:
-
Soft delete recommended for audit trail
-
Hard delete only for GDPR/data removal requests
-
Related Data:
-
Appointment history preserved in both delete types
-
Customer references in appointments remain valid
-
Restoration:
-
Soft deleted customers can be restored by setting
is_deleted=False - Restoration requires direct database access or admin endpoint
Error Responses¶
403 Forbidden - Insufficient Permissions:
Get Customer Appointments¶
Retrieve appointment history and statistics for a specific customer.
Endpoint¶
Authentication: Required (JWT token) Access: All staff members
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_id |
string | Yes | Customer ID (MongoDB ObjectId) |
Query Parameters¶
| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
page |
integer | No | Page number (default: 1) | 1 |
size |
integer | No | Items per page (default: 20) | 20 |
status |
string | No | Filter by appointment status | "confirmed" |
from_date |
date | No | Appointments from this date | "2025-01-01" |
to_date |
date | No | Appointments until this date | "2025-12-31" |
Appointment Statuses¶
pending- Awaiting confirmationconfirmed- Confirmed by customer/staffchecked_in- Customer arrivedin_progress- Service in progresscompleted- Service completedcancelled- Cancelled by customer/staffno_show- Customer didn't show up
Response¶
{
"appointments": [
{
"id": "507f1f77bcf86cd799439040",
"tenant_id": "507f1f77bcf86cd799439010",
"outlet_id": "507f1f77bcf86cd799439022",
"customer_id": "507f1f77bcf86cd799439011",
"staff_id": "507f1f77bcf86cd799439021",
"service_id": "507f1f77bcf86cd799439020",
"appointment_date": "2025-01-25",
"start_time": "14:00:00",
"end_time": "15:30:00",
"status": "confirmed",
"total_price": 350000,
"notes": "Customer prefers window seat",
"created_at": "2025-01-20T10:00:00Z",
"updated_at": "2025-01-20T10:00:00Z"
}
],
"statistics": {
"total_appointments": 28,
"completed": 24,
"cancelled": 2,
"no_show": 1,
"upcoming": 3
},
"pagination": {
"total": 28,
"page": 1,
"size": 20,
"pages": 2
}
}
Statistics Calculation¶
Computed in real-time:
total_appointments- Total count matching filterscompleted- Count of completed appointmentscancelled- Count of cancelled appointmentsno_show- Count of no-show appointmentsupcoming- Count of future appointments (date >= today)
Note: For better performance with large datasets, consider using the Customer Analytics endpoint instead.
Related Endpoints¶
- Appointment Management - Full appointment CRUD operations
- Customer Analytics - Detailed statistics and insights
Update Customer Preferences¶
Update customer preferences for bookings and communications.
Endpoint¶
Authentication: Required (JWT token) Access: All staff members
Path Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_id |
string | Yes | Customer ID (MongoDB ObjectId) |
Request Body¶
{
"preferred_services": [
"507f1f77bcf86cd799439020",
"507f1f77bcf86cd799439021"
],
"preferred_staff": [
"507f1f77bcf86cd799439030"
],
"preferred_outlets": [
"507f1f77bcf86cd799439022"
],
"communication_channels": {
"sms": true,
"email": true,
"push": false
},
"marketing_consent": true,
"language": "id",
"accessibility_needs": "Wheelchair accessible entrance required"
}
Preference Fields¶
| Field | Type | Description | Default |
|---|---|---|---|
preferred_services |
array | List of preferred service IDs | [] |
preferred_staff |
array | List of preferred staff IDs | [] |
preferred_outlets |
array | List of preferred outlet IDs | [] |
communication_channels |
object | Channel preferences (sms, email, push) | All true |
marketing_consent |
boolean | Consent to marketing communications | false |
language |
string | Preferred language (id, en) |
"id" |
accessibility_needs |
string | Special accessibility requirements | null |
Response¶
Returns complete customer profile with updated preferences (same format as Get Customer Details).
How Preferences Are Used¶
Booking Recommendations:
- Preferred services shown first in booking flow
- Preferred staff highlighted in staff selection
- Preferred outlets used for default location
Communication Delivery:
- SMS notifications sent only if
sms: true - Email notifications sent only if
email: true - Push notifications sent only if
push: true - Marketing messages require
marketing_consent: true
Accessibility:
- Staff can view accessibility needs before appointment
- Outlet selection shows accessibility features
- Special accommodations can be prepared
Business Rules¶
-
Privacy Compliance:
-
Marketing consent must be explicit opt-in
- Customers can withdraw consent anytime
-
Transactional messages (appointment confirmations) sent regardless of marketing consent
-
Preference Validation:
-
Service IDs must exist and belong to tenant
- Staff IDs must exist and belong to tenant
-
Outlet IDs must exist and belong to tenant
-
Default Behavior:
-
If no preferred staff → Show all available staff
- If no preferred outlets → Show all outlets
- If no preferred services → Show all services
Get Customer Analytics¶
Retrieve comprehensive customer analytics and statistics.
Endpoint¶
Authentication: Required (JWT token) Access: TENANT_ADMIN or SUPER_ADMIN
Query Parameters¶
| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
from_date |
date | No | Statistics from this date | "2025-01-01" |
to_date |
date | No | Statistics until this date | "2025-12-31" |
Response¶
{
"statistics": {
"total_customers": 145,
"active_customers": 138,
"inactive_customers": 7,
"verified_emails": 92,
"customers_with_password": 115,
"walk_in_customers": 30,
"new_customers_in_period": 12,
"total_appointments": 3456,
"total_revenue": 125000000,
"total_loyalty_points": 45000,
"avg_appointments_per_customer": 23.8,
"avg_spent_per_customer": 862068,
"retention_rate": 78.5,
"top_customers_by_appointments": [
{
"customer_id": "507f1f77bcf86cd799439011",
"full_name": "John Doe",
"email": "john.doe@example.com",
"total_appointments": 52,
"total_spent": 4500000
}
],
"top_customers_by_revenue": [
{
"customer_id": "507f1f77bcf86cd799439012",
"full_name": "Jane Smith",
"email": "jane.smith@example.com",
"total_appointments": 38,
"total_spent": 6200000
}
],
"customer_segments": {
"vip": 25,
"regular": 90,
"new": 12,
"at_risk": 18
}
},
"period": {
"from": "2025-01-01",
"to": "2025-12-31"
},
"generated_at": "2025-10-08T10:00:00Z"
}
Metric Definitions¶
Customer Counts:
total_customers- All customers (excluding soft-deleted)active_customers- Customers withis_active=Trueinactive_customers- Customers withis_active=Falseverified_emails- Customers withemail_verified=Truecustomers_with_password- Registered customers (portal access)walk_in_customers- Staff-created without passwordnew_customers_in_period- Customers created within date range
Financial Metrics:
total_appointments- Sum of all appointmentstotal_revenue- Sum of completed appointment paymentstotal_loyalty_points- Sum of all customer loyalty pointsavg_appointments_per_customer- Total appointments ÷ total customersavg_spent_per_customer- Total revenue ÷ total customers
Retention Metrics:
retention_rate- % of customers with appointment in last 90 days
Customer Segments:
vip- Customers with "vip" tagregular- Customers with "regular" tagnew- Customers created in last 30 daysat_risk- No appointment in last 60 days
Performance Notes¶
Large Dataset Handling:
- Analytics calculations use MongoDB aggregation pipelines
- May take several seconds for tenants with 10,000+ customers
- Consider caching results for dashboard views
- Use date range filters to improve performance
Related Endpoints¶
- Subscription Usage Tracking - Plan limit monitoring
- Appointment Analytics - Booking insights
Customer Preferences Schema¶
Complete reference for the CustomerPreferences object.
Schema Definition¶
class CustomerPreferences(BaseModel):
preferred_services: List[str] = []
preferred_staff: List[str] = []
preferred_outlets: List[str] = []
communication_channels: CommunicationChannels = CommunicationChannels()
marketing_consent: bool = False
language: str = "id"
accessibility_needs: Optional[str] = None
class CommunicationChannels(BaseModel):
sms: bool = True
email: bool = True
push: bool = True
Field Descriptions¶
| Field | Type | Default | Description |
|---|---|---|---|
preferred_services |
array | [] |
List of service IDs customer prefers |
preferred_staff |
array | [] |
List of staff IDs customer prefers |
preferred_outlets |
array | [] |
List of outlet IDs customer prefers |
communication_channels.sms |
boolean | true |
Receive SMS notifications |
communication_channels.email |
boolean | true |
Receive email notifications |
communication_channels.push |
boolean | true |
Receive push notifications |
marketing_consent |
boolean | false |
Consent to marketing communications |
language |
string | "id" |
Preferred language (id or en) |
accessibility_needs |
string | null |
Special accessibility requirements |
Best Practices¶
For Customer Creation¶
✅ DO:
- Always check for duplicates before creating (default behavior)
- Provide password for customers who need portal access
- Use meaningful tags for customer segmentation
- Initialize preferences with sensible defaults
- Validate email and phone formats client-side
❌ DON'T:
- Skip duplicate checks (prevents data quality issues)
- Store plain-text passwords (always use password field)
- Create customers without email OR phone (at least one required)
- Assign customers to wrong tenant
For Customer Updates¶
✅ DO:
- Validate uniqueness when changing email/phone
- Preserve important tags like
vip,staff-created - Update preferences separately using preferences endpoint
- Use soft delete for data preservation
❌ DON'T:
- Change tenant_id (violates tenant isolation)
- Modify system-calculated fields (loyalty_points, total_spent)
- Hard delete without GDPR/legal requirement
- Update email/phone without duplicate check
For Customer Search¶
✅ DO:
- Use pagination for large result sets
- Combine multiple filters for precise results
- Use tags for customer segmentation
- Cache frequently-used search results
❌ DON'T:
- Fetch all customers without pagination (performance issue)
- Use wildcard searches on large datasets
- Query customers across multiple tenants
For Privacy Compliance¶
✅ DO:
- Respect marketing consent settings
- Provide easy preference update mechanism
- Honor communication channel preferences
- Implement data export on request
- Support soft delete for data retention
❌ DON'T:
- Send marketing messages without consent
- Share customer data across tenants
- Ignore opt-out requests
- Keep data indefinitely without retention policy
Error Handling¶
Common Error Codes¶
| Status Code | Error Type | Description | Resolution |
|---|---|---|---|
| 400 | Validation Error | Invalid input data | Check request body format and field values |
| 401 | Not Authenticated | Missing or invalid JWT token | Provide valid authentication token |
| 403 | Forbidden | Insufficient permissions | Check user role and permissions |
| 404 | Not Found | Customer not found | Verify customer ID exists |
| 409 | Conflict | Duplicate email or phone | Use different email/phone or update existing |
Example Error Responses¶
Validation Error:
{
"detail": [
{
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"type": "value_error.email"
},
{
"loc": ["body", "phone"],
"msg": "phone number must start with +",
"type": "value_error"
}
]
}
Authorization Error:
Tenant Isolation Error:
Integration Examples¶
Create Walk-in Customer (No Portal Access)¶
curl -X POST https://api.example.com/api/v1/customers \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"phone": "+628123456789",
"first_name": "Walk-in",
"last_name": "Customer",
"tags": ["walk-in"]
}'
Create Registered Customer (With Portal Access)¶
curl -X POST https://api.example.com/api/v1/customers \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "customer@example.com",
"phone": "+628123456789",
"first_name": "Registered",
"last_name": "Customer",
"password": "SecurePass123!",
"preferences": {
"communication_channels": {
"sms": true,
"email": true,
"push": true
},
"marketing_consent": true
}
}'
Search VIP Customers¶
curl -X GET "https://api.example.com/api/v1/customers?tags=vip&page=1&size=50" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Update Customer Preferences¶
curl -X PUT https://api.example.com/api/v1/customers/507f1f77bcf86cd799439011/preferences \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"preferred_services": ["507f1f77bcf86cd799439020"],
"communication_channels": {
"sms": true,
"email": true,
"push": false
},
"marketing_consent": false
}'
Get Customer Analytics¶
curl -X GET "https://api.example.com/api/v1/customers/statistics/summary?from_date=2025-01-01&to_date=2025-12-31" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
API Reference Summary¶
| Endpoint | Method | Purpose | Access Level |
|---|---|---|---|
/customers |
GET | List all customers with search/filters | All staff |
/customers |
POST | Create new customer | All staff |
/customers/{customer_id} |
GET | Get customer details | All staff |
/customers/{customer_id} |
PUT | Update customer | TENANT_ADMIN+ |
/customers/{customer_id} |
DELETE | Delete customer (soft/hard) | TENANT_ADMIN+ |
/customers/{customer_id}/appointments |
GET | Get appointment history | All staff |
/customers/{customer_id}/preferences |
PUT | Update preferences | All staff |
/customers/statistics/summary |
GET | Get analytics | TENANT_ADMIN+ |
Related Documentation¶
- Subscription Management - Plan limits and usage tracking
- Appointment Management - Booking system integration
- User Management - Staff and authentication
- Outlet Management - Location management
Next Steps:
- Review customer schema and validation rules
- Test customer creation with duplicate detection
- Implement customer search in your application
- Set up preference management UI
- Configure analytics dashboard with customer metrics
For webhook integration and real-time updates, refer to the Webhook Architecture section.