Playbook

Semantic Events Over Raw Provider Payloads (Semantic Events Over Raw Provider Payloads)

Should the webhook the provider gateway receives reach downstream consumers under the PSP's own event name, or as a semantic event like…

Distributed Payment Engine

Part 10 of 22

A series on distributed payment architecture — the gap between capture and complete.

Distributed payment engine architecture diagram

The previous part established that the orchestrator should never see a PSP SDK. The same boundary reappears one step further out: the webhook a provider sends back.

A PSP webhook usually carries its own internal data model: a provider-specific event type name, provider-specific status codes, a provider-specific object shape. Pushing that payload straight onto a queue or event stream means leaking the provider's schema to every downstream consumer.

PSP Webhook
  { type: 'charge.succeeded', data: { object: {...} } }
      ▼
Provider Gateway (translation)
      ▼
Semantic event
  PaymentCaptured { paymentId, amount, currency }

This part looks at why that translation layer is a responsibility you cannot skip.

Where the concepts first show up

📦 Semantic event
An event named in business language, with zero reference to the provider: PaymentCaptured, PaymentFailed.

📦 Raw provider payload
The original data a PSP sends in its webhook body, carrying its own internal model.

📦 Translator
The component, living inside the gateway, that reads the raw payload and produces the semantic event.

📦 Event contract ownership
Who decides the fields and meaning of the semantic event — always the gateway, never the PSP.

charge.succeeded is a provider's own internal vocabulary; PaymentCaptured is a fact in your domain. The two never change at the same time: the provider can rename its event, and your semantic event name stays exactly as it was.

The cost of passing raw payloads through

The fastest integration path is to push the webhook body straight onto a broker without parsing it. It works in the short term — the consumer parses the same payload. But it means every provider schema change propagates directly to every consumer. When a PSP renames a field, an event outside your control breaks multiple systems at once.

If raw payload is broadcast
  PSP schema change → N consumers affected simultaneously

If semantic event is broadcast
  PSP schema change → only the gateway's translator needs updating

What the translator does, and doesn't do

The translator maps provider-specific status codes to a semantic enum, normalizes inconsistent or missing fields, and fills gaps (amount, currency) from the local record when needed. What it must never do is make a business decision: 'why did this payment fail, what should happen next' is the orchestrator's job, not the translator's.

Webhook arrives
  → read provider event type
  → look up status mapping table
  → build semantic event
  → correlate with local id
  → publish (via Outbox)

A mapping table is a concrete design artifact

A provider might expose dozens of event types; your semantic event set should stay far smaller and far more stable.

Provider event Semantic event
charge.succeeded PaymentCaptured
charge.failed PaymentFailed
charge.dispute.created PaymentDisputed
payment_intent.requires_action PaymentActionRequired

This table should be legible in a code review; when a new provider event shows up, 'what semantic event does this map to' should be a one-line decision, not an if-else chain scattered across the codebase.

Ordering and redelivery still apply

The translator must also handle duplicate or out-of-order webhooks. A provider may resend the same webhook after a network hiccup; producing the semantic event must therefore be idempotent — the same webhook id arriving twice should not republish the same semantic event (or downstream must be designed to be idempotent regardless).

Distinctions that get blurred

❌ Webhook = Event
✓ A webhook is a notification trigger; a semantic event is a fact in business language

❌ Storing the raw payload is unnecessary
✓ The raw payload is kept for diagnostics, but only inside the gateway's own archive

❌ The mapping table is written once and done
✓ As the provider adds event types, the table is a living contract

Raw pass-through vs. semantic translation

Criterion Raw pass-through Semantic translation
Downstream needs provider knowledge Yes No
Fragility to schema change High Low
Raw data for diagnostics Can be lost Kept in the gateway
Adding a new PSP Affects consumers Affects only the mapping table

Checklist when designing the translator

  1. Does any downstream consumer read a provider-specific field name or status code?
  2. Is the mapping table defined in one place, or scattered across the codebase?
  3. Is the raw webhook payload archived inside the gateway for diagnostics?
  4. If the same webhook arrives twice, is the same semantic event published twice?
  5. What happens when an unmapped provider event type shows up — is it silently swallowed, or does it raise a visible alert?

The fifth question matters most: silently swallowed unknown events are one of the quietest forms of data loss in production.

What to take away

  1. Downstream should never see the provider's event name or status code.
  2. The translator is a mapping table plus normalization logic — it never makes business decisions.
  3. The raw payload is kept for diagnostics, but stays inside the gateway's own boundary.
  4. Unmapped provider events must never be swallowed silently; they need a visible signal.

The simplest test for whether an event is truly semantic: you can read its name from your own domain glossary, not from the provider's docs.

The next part goes deeper into the failure information these semantic events carry: not every PaymentFailed means the same thing — we need a taxonomy.

FAQ

Frequently asked questions

What is Semantic event?

An event named in business language, with zero reference to the provider: PaymentCaptured, PaymentFailed.

What is Raw provider payload?

The original data a PSP sends in its webhook body, carrying its own internal model.

Is it true that "Webhook = Event"?

A webhook is a notification trigger; a semantic event is a fact in business language

What does this part lock in?

This part looks at why that translation layer is a responsibility you cannot skip. Downstream should never see the provider's event name or status code. The previous part established that the orchestrator should never see a PSP SDK. The same boundary reappears one step further out: the webhook a provider sends back.

Engineering Principles Learned

  • Downstream should see your domain's fact, never the provider's event name.
  • A mapping table is a living contract, not a one-time snippet.
  • An unmapped provider event must be visible, never silently swallowed.

Continue reading

Continue reading

Next in series

ESSAY

A timeout, a 429, a 5xx, a business decline, and an infrastructure fault are not the same failure. Each category needs its own retry policy.

Next in series

Same series

Paylaş