Event & Webhook Catalog
Two related but distinct concepts: internal events (on event-service's bus, consumed by
TBBN's own services) and merchant webhooks (HTTP callbacks to/from merchant systems). Every
merchant webhook is triggered by (usually the same-named) internal event, but not every internal
event has a corresponding merchant webhook.
Internal event bus (event-service)
Naming: noun.past_tense_verb. All 31 events below are implemented (packages/event-contracts's
src/events.ts) and actually published on the shared tbbn:events Redis channel by the WORKING
service that owns them.
Two services subscribe to every event on the bus, unfiltered (not per-event — worth knowing before assuming a new event type needs a code change in either):
audit-service— writes every event toaudit_logsverbatim, full audit trail.webhook-service— treats every event as a delivery candidate, then filters internally by which merchants have an active subscription matching that event type.
Everything else subscribes to the whole bus but filters to specific event types in its own
handler logic (event.type === EVENT_TYPES.X checks, not a server-side subscription filter):
| Service | Reacts to |
|---|---|
notification-service | offer.created/accepted/rejected/cancelled/expired/countered, trade.created/accepted/completed/cancelled, checkout.started/completed/failed, fulfillment.started/completed — see messages.ts's NOTIFICATION_MESSAGES map for the exact set with copy |
fraud-service | offer.created, moderation.flagged — evaluates rules, may publish fraud_signal.detected |
reputation-service | trade.completed, checkout.completed, fraud_signal.detected |
trade-session-service | offer.accepted only (see offer-accepted.subscriber.ts) |
checkout-service | trade.accepted only (see trade-accepted.subscriber.ts) |
analytics-service and search-service do not subscribe to the event bus at all — both
compute directly against Postgres on read (analytics-service's overview() aggregates
TradeSession/Offer rows live; search-service queries Listing rows live). Don't assume
either one reacts to events; if a future change needs analytics to react in near-real-time instead
of on-read, that's a new subscriber to add, not a bug to fix in an existing one.
Known attribution gap (documented, not silently guessed at): checkout.failed and
trade.cancelled carry no actor field, so fraud-service/reputation-service cannot safely
attribute blame for a cancellation/failure to one side of a trade — see those two services' own
READMEs.
Merchant webhooks — TBBN → Merchant
Delivered by webhook-service with HMAC-SHA256 signing (see docs/api/api-conventions.md).
listing.created, listing.updated, offer.created, offer.accepted, offer.rejected,
trade.created, trade.accepted, trade.completed, checkout.started, checkout.completed,
reservation.created, reservation.expired, fulfillment.started, fulfillment.completed,
seller.linked.
Merchant webhooks — Merchant → TBBN
Received by checkout-service (folds in what was planned as a standalone payment-status-service
— see docs/architecture/service-catalog.md) and listing-service.
payment.completed, payment.failed, item.reserved, item.unreserved, item.sold,
item.unavailable, fulfillment.started, fulfillment.completed (aliases fulfilled),
returned, cancelled.
Webhook payload shape (checkout.started example)
{
"event": "checkout.started",
"tradeSessionId": "ts_01hxyz...",
"side": "A",
"originalPrice": 500,
"tradeAmount": 50,
"currency": "USD",
"timestamp": "2026-07-01T12:00:00Z"
}
Signature scheme
Every outbound webhook includes X-TBBN-Signature: t=<unix_ts>,v1=<hmac_sha256_hex> computed
over ${timestamp}.${rawBody} using the merchant's webhook signing secret (issued alongside
their API key). Merchants must reject any request older than a 5-minute replay window. Full
verification code ships in every SDK (verifyWebhookSignature()).
Delivery guarantees
At-least-once delivery, exponential backoff retry (5 attempts over ~24h), dead-letter queue with
manual replay available in the merchant dashboard and via POST /v1/webhooks/deliveries/:id/replay
(see docs/api/api-conventions.md's idempotency-key requirement — replays must be idempotent on
the merchant's end, keyed by tradeSessionId + event).