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.
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
- Does any downstream consumer read a provider-specific field name or status code?
- Is the mapping table defined in one place, or scattered across the codebase?
- Is the raw webhook payload archived inside the gateway for diagnostics?
- If the same webhook arrives twice, is the same semantic event published twice?
- 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
- Downstream should never see the provider's event name or status code.
- The translator is a mapping table plus normalization logic — it never makes business decisions.
- The raw payload is kept for diagnostics, but stays inside the gateway's own boundary.
- 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
Payment Failure Taxonomy
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
Provider Abstraction Without Leaking SDKs
How the provider gateway owns the PSP SDK while the checkout orchestrator only ever sees a semantic interface — and why card and wallet flows share a…
Same series
Payment Evidence vs. Payment State
Evidence is what the PSP told you. State is what you decided. Conflate the two, and recovery leaves you unsure which one to trust.