TBBN.Merchant Platform docs
Architecturedocs/architecture/event-webhook-catalog.md

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 to audit_logs verbatim, 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):

ServiceReacts to
notification-serviceoffer.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-serviceoffer.created, moderation.flagged — evaluates rules, may publish fraud_signal.detected
reputation-servicetrade.completed, checkout.completed, fraud_signal.detected
trade-session-serviceoffer.accepted only (see offer-accepted.subscriber.ts)
checkout-servicetrade.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).