Event Catalog
Events are business facts that occur in the system — immutable, append-only. Every event sent to PBCP must follow this convention.
Naming convention
{entity}.{action}
{namespace}.{entity}.{action} ← for multi-service systemsExamples:
order.created✅customer.registered✅pharmacy.prescription.dispensed✅ (namespaced)OrderCreated❌ (not snake_case)created_order❌ (entity must come first)
Event envelope (used by all domains)
{
"schemaVersion": "1.0",
"tenantId": "your-tenant-id",
"eventId": "uuid-v4",
"eventName": "order.created",
"occurredAt": "2026-07-15T10:00:00.000Z",
"actor": {
"type": "user | system | staff | device",
"id": "actor-id"
},
"source": {
"system": "your-system-name",
"channel": "web | mobile | pos | api | iot"
},
"payload": {
// domain-specific fields
}
}Pharmacy
Tenant: pharmacy-demo (pilot) | {pharmacy-slug} (production)
| Event | actor.type | payload fields |
|---|---|---|
customer.registered | staff / system | customerId, channel, registeredVia |
customer.visited | system | customerId, visitType |
product.searched | staff | query, resultCount |
product.viewed | staff | productId, sku, name |
cart.updated | staff | customerId, lineItems[], total |
prescription.created | staff | prescriptionId, doctorName, items[] |
prescription.dispensed | staff | prescriptionId, customerId, items[] |
order.created | system | orderId, customerId, lineItems[], total |
order.completed | system | orderId, paidAt, paymentMethod |
payment.completed | system | orderId, amount, currency, method |
loyalty.points_earned | system | customerId, points, reason |
coupon.redeemed | staff | customerId, couponCode, discountAmount |
Payload example — prescription.dispensed:
{
"prescriptionId": "RX-20260715-001",
"customerId": "CUST-1",
"doctorName": "พญ. สมหญิง รักสุขภาพ",
"items": [
{ "productId": "PROD-42", "name": "Amlodipine 5mg", "qty": 30, "unit": "เม็ด" }
],
"dispensedBy": "staff-7"
}E-commerce
| Event | actor.type | payload fields |
|---|---|---|
customer.registered | system | customerId, registeredVia |
session.started | system | sessionId, customerId?, channel, device |
product.viewed | system | productId, sku, name, price, categoryId |
product.searched | system | query, resultCount, filters |
product.wishlisted | system | customerId, productId |
cart.item_added | system | customerId, productId, qty, price |
cart.item_removed | system | customerId, productId, reason? |
cart.abandoned | system | customerId, cartTotal, itemCount |
checkout.started | system | customerId, cartTotal |
payment.initiated | system | orderId, amount, method |
payment.completed | system | orderId, amount, currency, method |
order.created | system | orderId, customerId, lineItems[], total |
order.shipped | system | orderId, trackingNumber, carrier |
order.delivered | system | orderId, deliveredAt |
return.requested | system | orderId, reason, items[] |
coupon.applied | system | customerId, couponCode, discountAmount |
Payload example — cart.abandoned:
{
"customerId": "CUST-99",
"sessionId": "SES-abc",
"cartTotal": 1850,
"itemCount": 3,
"lastProductViewed": "PROD-101",
"minutesInCart": 12
}Logistics
| Event | actor.type | payload fields |
|---|---|---|
shipment.created | system | shipmentId, senderId, recipientId, origin, destination |
shipment.assigned | system | shipmentId, driverId, vehicleId |
shipment.picked_up | driver | shipmentId, driverId, pickedUpAt, location |
shipment.in_transit | system | shipmentId, currentLocation, eta |
shipment.out_for_delivery | driver | shipmentId, driverId, departedHub |
shipment.delivery_attempted | driver | shipmentId, attemptNumber, reason, location |
shipment.delivered | driver | shipmentId, deliveredAt, signedBy?, proofUrl? |
shipment.failed | driver | shipmentId, reason, returnToHub |
shipment.returned | system | shipmentId, returnedAt |
checkpoint.scanned | device | shipmentId, checkpointId, checkpointName, scannedAt |
driver.shift_started | driver | driverId, shiftId, vehicleId |
driver.shift_ended | driver | driverId, shiftId, deliveriesCompleted, failedCount |
customer.notified | system | shipmentId, channel, messageType, eta |
Payload example — shipment.delivery_attempted:
{
"shipmentId": "SHP-20260715-0042",
"driverId": "DRV-7",
"attemptNumber": 1,
"reason": "recipient_not_home",
"location": { "lat": 13.7563, "lng": 100.5018 },
"nextAttemptScheduled": "2026-07-16T10:00:00.000Z"
}SaaS / App
| Event | actor.type | payload fields |
|---|---|---|
user.signed_up | system | userId, email, plan, signupVia |
user.verified_email | system | userId |
onboarding.step_completed | user | userId, step, stepName, duration |
onboarding.completed | system | userId, totalDays |
session.started | system | userId, sessionId, platform, device |
feature.used | user | userId, featureName, context? |
feature.first_used | system | userId, featureName |
workspace.created | user | workspaceId, userId, name |
workspace.member_invited | user | workspaceId, invitedEmail, role |
workspace.member_joined | system | workspaceId, userId |
subscription.upgraded | user | userId, fromPlan, toPlan |
subscription.downgraded | user | userId, fromPlan, toPlan, reason? |
subscription.cancelled | user | userId, plan, reason, cancelledAt |
payment.completed | system | userId, amount, plan, period |
api.called | system | userId, endpoint, method, statusCode |
integration.connected | user | userId, integrationName |
Payload example — subscription.cancelled:
{
"userId": "usr_abc123",
"plan": "pro",
"reason": "too_expensive",
"feedback": "ราคาสูงเกินไปสำหรับทีมเล็ก",
"cancelledAt": "2026-07-15T09:00:00.000Z",
"daysActive": 127
}Education
| Event | actor.type | payload fields |
|---|---|---|
student.registered | system | studentId, email, registeredVia |
enrollment.created | student | studentId, courseId, courseName, plan |
lesson.started | student | studentId, courseId, lessonId, lessonName |
lesson.completed | system | studentId, lessonId, watchPercent, durationMs |
lesson.skipped | student | studentId, lessonId, reason? |
quiz.started | student | studentId, quizId, lessonId |
quiz.submitted | student | studentId, quizId, answers, score |
quiz.passed | system | studentId, quizId, score, passingScore, attemptNumber |
quiz.failed | system | studentId, quizId, score, passingScore, attemptNumber |
assignment.submitted | student | studentId, assignmentId, submittedAt |
assignment.graded | instructor | assignmentId, score, feedback |
course.completed | system | studentId, courseId, completionDate, totalDays |
certificate.issued | system | studentId, courseId, certificateId, issuedAt |
discussion.posted | student | studentId, lessonId, postId |
live_session.attended | system | studentId, sessionId, joinedAt, duration |
enrollment.cancelled | student | studentId, courseId, reason?, refundRequested |
Payload example — quiz.failed:
{
"studentId": "STU-001",
"quizId": "QUIZ-L2-01",
"lessonId": "L-02",
"courseId": "python-beginners",
"score": 55,
"passingScore": 70,
"attemptNumber": 2,
"wrongTopics": ["loops", "list_comprehension"]
}Beauty
| Event | actor.type | payload fields |
|---|---|---|
customer.registered | system | customerId, registeredVia, referredBy? |
consultation.completed | staff | customerId, staffId, skinType, concerns[], sensitivities[] |
appointment.booked | system / customer | customerId, appointmentId, treatmentId, staffId?, scheduledAt |
appointment.confirmed | staff | appointmentId, confirmedBy |
appointment.reminder_sent | system | appointmentId, customerId, channel, scheduledAt |
appointment.checked_in | staff | appointmentId, customerId, checkedInAt |
appointment.no_show | system | appointmentId, customerId |
appointment.cancelled | customer / staff | appointmentId, reason, cancelledBy |
treatment.started | staff | appointmentId, treatmentId, treatmentName, staffId |
treatment.completed | staff | appointmentId, treatmentId, durationMin, staffId |
product.recommended | staff | customerId, staffId, productId, productName, reason |
product.purchased | system | customerId, productId, qty, amount, linkedAppointmentId? |
review.submitted | customer | customerId, appointmentId, rating, comment?, staffId? |
membership.enrolled | system | customerId, tier, startDate |
membership.upgraded | system | customerId, fromTier, toTier |
package.purchased | system | customerId, packageId, packageName, sessions, amount |
package.session_used | staff | customerId, packageId, appointmentId, sessionsRemaining |
loyalty.points_earned | system | customerId, points, reason, totalPoints |
coupon.redeemed | staff | customerId, couponCode, discountAmount, appointmentId? |
referral.made | system | referrerId, referredCustomerId |
Payload example — treatment.completed:
{
"appointmentId": "APT-20260715-042",
"treatmentId": "TRT-facial-hydra",
"treatmentName": "Hydra Facial 60 นาที",
"staffId": "staff-12",
"durationMin": 65,
"products_used": ["PROD-serum-01", "PROD-mask-03"],
"notes": "ผิวแห้ง ใช้ hydrating mask เพิ่ม"
}Payload example — consultation.completed:
{
"customerId": "CUST-B001",
"staffId": "staff-5",
"skinType": "combination",
"concerns": ["acne", "dark_spots", "dehydration"],
"sensitivities": ["fragrance"],
"recommendedTreatments": ["hydra-facial", "vitamin-c-booster"]
}Hotel
| Event | actor.type | payload fields |
|---|---|---|
reservation.created | system / staff | reservationId, guestId, roomType, checkIn, checkOut, pax, channel |
reservation.confirmed | system | reservationId, bookingRef |
reservation.modified | staff / guest | reservationId, changes[] |
reservation.cancelled | guest / staff | reservationId, reason, cancellationFee |
pre_arrival.message_sent | system | guestId, reservationId, channel |
pre_arrival.preferences_received | guest | guestId, preferences (pillow, dietary, arrivalTime) |
check_in.completed | staff | guestId, reservationId, roomId, checkedInAt |
room_upgrade.offered | system / staff | guestId, fromRoomType, toRoomType, surcharge |
room_upgrade.accepted | guest | guestId, reservationId, toRoomType |
room_service.ordered | guest | guestId, roomId, items[], total |
amenity.used | system | guestId, amenityType (spa, pool, gym, restaurant) |
complaint.raised | guest | guestId, roomId, issue, severity |
complaint.resolved | staff | guestId, issueId, resolution, staffId |
check_out.completed | staff | guestId, reservationId, checkedOutAt |
payment.completed | system | guestId, reservationId, amount, method |
review.submitted | guest | guestId, reservationId, rating, platform, comment? |
loyalty.points_earned | system | guestId, points, reason |
loyalty.tier_upgraded | system | guestId, fromTier, toTier |
Payload example — pre_arrival.preferences_received:
{
"guestId": "GUEST-001",
"reservationId": "RES-20260715-0021",
"preferences": {
"pillow": "soft",
"dietary": ["no_seafood"],
"estimatedArrivalTime": "14:00",
"specialOccasion": "anniversary",
"floorPreference": "high"
}
}Restaurant
Actor types: customer (self-service app), staff (POS/table system), system (automatic)
| Event | actor.type | payload fields |
|---|---|---|
customer.registered | staff / system | customerId, channel, registeredVia |
reservation.created | customer / staff | reservationId, customerId, partySize, scheduledAt, branch |
reservation.confirmed | system | reservationId, tableId? |
table.seated | staff | customerId?, tableId, branchId, partySize, seatedAt |
order.placed | staff | tableId, customerId?, items[], orderId |
order.item_added | staff | tableId, orderId, items[] |
promotion.applied | staff | customerId, orderId, promoCode, discountAmount |
bill.requested | customer / staff | tableId, orderId, requestedAt |
payment.completed | system | orderId, amount, method, tableId |
table.released | staff | tableId, branchId, releasedAt, durationMin |
review.submitted | customer | customerId, branchId, rating, comment? |
loyalty.points_earned | system | customerId, points, orderId |
loyalty.points_redeemed | staff | customerId, points, discountAmount, orderId |
membership.upgraded | system | customerId, fromTier, toTier |
birthday.offer_sent | system | customerId, channel, offerDetails |
birthday.offer_redeemed | staff | customerId, orderId, offerCode |
delivery.ordered | system | customerId, branchId, items[], total, platform |
delivery.completed | system | customerId, orderId, deliveredAt |
Payload example — table.seated:
{
"tableId": "B7",
"branchId": "branch-central-westgate",
"customerId": "CUST-M001",
"partySize": 4,
"seatedAt": "2026-07-15T19:04:00.000Z",
"reservationId": "RES-20260715-042"
}Payload example — order.placed:
{
"orderId": "ORD-20260715-B7-001",
"tableId": "B7",
"customerId": "CUST-M001",
"items": [
{ "menuItemId": "MENU-001", "name": "หมูสไลด์", "qty": 1, "price": 189 },
{ "menuItemId": "MENU-012", "name": "กุ้งสด", "qty": 1, "price": 249 },
{ "menuItemId": "MENU-031", "name": "น้ำซุป MK", "qty": 1, "price": 59 }
],
"itemTotal": 497
}Agriculture
| Event | actor.type | payload fields |
|---|---|---|
farm.registered | system | farmId, farmerId, province, totalAreaRai |
plot.created | farmer | plotId, farmId, areaRai, gpsPolygon, soilType |
crop.planted | farmer | plotId, cropId, variety, plantedAt, seedQtyKg, methodId |
crop.fertilized | farmer | plotId, productId, productName, qtyKgPerRai, method |
crop.irrigated | farmer / device | plotId, volumeLiters, method (drip/sprinkler/flood), source |
crop.sprayed | farmer | plotId, productId, qtyMlPerRai, reason, targetPest? |
crop.disease_detected | farmer / system | plotId, diseaseType, affectedAreaRai, severity |
crop.disease_treated | farmer | plotId, treatmentId, productId, durationDays |
sensor.reading | device | plotId, sensorId, readings{} (soil_moisture, soil_temp, air_temp, humidity, ph, ec) |
sensor.alert | system | plotId, sensorId, alertType, value, threshold, recommendation |
weather.event | system | plotId, eventType (heavy_rain, drought, storm), severity, rainfallMm? |
crop.harvest_started | farmer | plotId, cropId, startedAt, estimatedYieldKg |
crop.harvested | farmer | plotId, harvestId, yieldKg, qualityNotes? |
harvest.quality_checked | staff / buyer | harvestId, grade, moisturePercent, checkedBy |
harvest.sold | farmer | harvestId, buyerId, pricePerKg, weightKg, totalAmount |
harvest.delivered | driver | harvestId, destinationId, deliveredAt, proofUrl? |
payment.received | system | harvestId, amount, method, receivedAt |
input.purchased | farmer | farmId, inputType (seed/fertilizer/pesticide), productId, qty, amount |
loan.applied | farmer | farmId, amount, purpose, season |
loan.approved | system | farmId, loanId, approvedAmount, yieldHistoryUsed |
certification.applied | farmer | farmId, certType (GAP/Organic/GlobalGAP) |
certification.approved | system | farmId, certId, certType, validUntil |
Payload example — crop.harvested:
{
"plotId": "plot-B2",
"harvestId": "HRV-20260522-B2",
"cropId": "crop-20260315-B2",
"yieldKg": 3200,
"yieldPerRai": 1600,
"harvestedAt": "2026-05-22T08:00:00.000Z",
"qualityNotes": "grade A 80%, grade B 20%, size uniform"
}Payload example — sensor.alert:
{
"plotId": "plot-B2",
"sensorId": "sensor-B2-001",
"alertType": "soil_moisture_low",
"value": 22.3,
"threshold": 30,
"unit": "%",
"recommendation": "irrigate within 12 hours",
"severity": "warning"
}Adding a new domain
To add a new domain:
- Define entity types — What are the "things with stories" in this business (customer? shipment? student?)
- Define events — What happens, who does it, when
- Follow naming —
{entity}.{action}always snake_case - Keep payload flat — nested objects are allowed but should not exceed 2 levels deep
- Instrument at the moment it occurs — do not backfill from DB (events = facts that actually occurred)