Skip to main content
Back to blog

Semantic channel resolution — why raw event buses are not the answer to omnichannel

A raw event bus gives you every provider webhook and delivery receipt in order. What it rarely gives you is the thing you actually wanted — "the customer asked about their order on WhatsApp, got angry, and the agent fixed it on a call." Devotel Orbit ships that second thing: raw events resolved into conversation records behind one shared channel vocabulary. Here is why the resolution layer, not the bus, is the product.

Orbit Editorial Team

Most CPaaS evaluations go wrong at the same sentence: "it all flowed into one event bus." That sentence sounds like omnichannel. It is actually the easy half of omnichannel. A raw event bus tells you that something happened: a webhook from a messaging provider, a delivery receipt from a voice carrier, an email bounce. What it does not tell you, and what your support team, your analytics, and your CDP actually need, is what it meant on the channel: which customer this is, whether it is the next turn of a conversation that already exists or the start of a new one, whether "delivered" means the same thing on SMS and on a Meta DM. Turning raw events into those records is the resolution layer, and the resolution layer is the part of an omnichannel platform that does the work.

This post is the argument in full: why the raw-bus-first mental model fails, what semantic channel resolution has to do for you, and how Devotel Orbit ships it — raw events resolved into conversations, contacts, and quality records behind one channel vocabulary, with the raw stream still there when you genuinely need it.

What a raw event bus actually gives you

Take event-driven architecture seriously and you get a real improvement over the naïve pattern. Producers emit; consumers subscribe; everything is async; the history is replayable. Build this for customer communications and a day later you have a firehose of every inbound message, every delivery receipt, every call leg, every opt-out, every bounce. Ordered, replayable, and utterly unreadable as a customer history.

The reasons are structural, not implementation details:

  • The bus sees events; it never sees relationships. An SMS reply is a record with a phone number and a timestamp. Whether it belongs to the thread the agent closed five minutes ago, or reopens it, or starts something new, is a judgement the bus refuses to make. So you make it — in every consumer that reads the bus, or you give up reading the bus and keep your channel lists in separate tabs.
  • Channels disagree on the happy path. "Delivered" means one thing on SMS (a carrier DLR), a weaker thing on Meta DMs (accept is counted as delivered when the recipient is opted in), and a different thing again on email (no bounce ≠ read). Three channels, three definitions, one word — and the bus preserves all three under whichever vendor's status strings happen to arrive.
  • The identity field moves. SMS carries a phone number, WhatsApp carries a phone number, email carries an address, Instagram carries a handle, voice carries a number. There is no schema-level guarantee that two events naming the same human share any field at all. The bus has no obligation to know Mary Chen from SMS equals Mary Chen from the call at 2:14 pm.
  • The answers people actually need are per-event-in-isolation answers. "Show me everything that happened with this customer" is not a filter on a raw stream; it is a join over resolved records. The bus can only ever tell you that an event arrived. It cannot answer the next question — whose conversation did it advance.

An event bus is, in the limit, a very reliable way to move your integration problems downstream. Every one of these gaps is solvable. The trick is they are not solvable in the bus: they are solved at the boundary where raw events become semantics.

The resolution layer — what it has to do

Semantic channel resolution is the layer that takes a raw inbound event and returns what it means on the channel before anything downstream reads it. Concretely, four jobs, in order:

  1. Route. The event arrives on some transport with a channel-agnostic shape — an HTTP webhook, an SMPP bind, a provider callback. Routing decides which channel vocabulary it speaks (SMS, WhatsApp, email, voice, a Meta DM, an RCS thread) and which tenant-owned account it belongs to. Until an event is routed, it has no channel at all — only a transport.
  2. Thread. The routed event either extends an existing conversation or starts one. The thread decision is a domain judgement — status machine, not foreign key — and one platform must own it; every consumer that re-does it is a bug farm.
  3. Identify. The channel-side identifier (phone number, email, handle) resolves to the contact's golden record in the CDP, so the event lands as "something Mary Chen did" rather than "something a number did."
  4. Normalize status. Channel vocabularies collapse into the platform's own: delivered/undelivered/failed on one axis, active/closed on the lifecycle axis. A failed Meta DM and a failed SMS must be the same Failed — the channel difference (carrier DLR vs Meta accept) belongs in the per-channel detail, not in the headline.

An event that survives all four is no longer an event. It is a conversation record — the thing an inbox shows, the thing a quality evaluation scores, the thing a sentiment roll-up aggregates, the thing a compliance review exports. That is the product.

The failure gallery — what skipping the layer costs

Teams that adopt a raw-bus posture and skip the resolution layer do not skip it for free. They pay in a small set of ways that recur so reliably they deserve names.

The re-keyed query. An operator wants "everything from this customer, last 7 days." Behind a raw bus that is a filtered scan with hand-written joins across identifiers; behind a resolution layer it is one free-text field. The sentinel symptom is the second tab: if your answer to "did the customer get SMS and call?" is to open the SMS list and the call log side by side, your bus is leaking work to your operators.

The false all-clear. "Delivered" means a carrier DLR on one channel and an accept-and-count on another. Aggregate those naively and the dashboard lights green while a channel the count depends on is quietly degrading. The failure mode is not a wrong number; it is a green number that stopped meaning anything.

The perpetual split-brain inbox. Two threads per customer per channel, forever, because threading lived in whichever adapter you wrote last quarter. The inbox shows a "conversation" per transport event instead of per human interaction; agents fix it manually with copy-paste, one thread at a time.

The half-resolved identity. The bus knows a phone number. The CDP knows a golden record. In between, somebody's nightly job. If Mary Chen's SMS and Mary Chen's call resolve to different Marys, your contact history, your sentiment trend, and your consent posture are all slightly wrong in ways no single query surfaces.

None of these are exotic failures. They are the default outcomes of letting raw events reach application code un-resolved.

Where Orbit resolves — the record surfaces

Orbit's omnichannel dashboards do not read the event stream and improvise a vocabulary. They read the records the resolution layer already wrote, because that is the difference. Concretely, the unified surfaces sit on resolved records:

  • [Interactions](/interactions) — messaging conversations and voice calls in one recency-ordered list, with shared type, channel, status, contact, and time filters, and a CSV export of whatever you narrowed down. "Everything that happened with this customer" is one free-text field here, not a raw scan.
  • The [inbox](/inbox) — agents work the queue over resolved conversations, with lifecycle status (Active/Closed) and delivery health (Delivered/Undelivered/Failed) shown as separate indicators, because a conversation that is open whose latest message failed is a different situation from a healthy open one.
  • [Quality → Evaluations](/quality/evaluations) — reviewers score resolved conversations and calls; the same channel and status vocabulary as Interactions, so a review starting from "all failed Viber deliveries this week" is one filter set, not a re-keyed query.
  • [Sentiment](/insights/sentiment), [Contact Reasons](/insights/contact-reasons), [Topic Intelligence](/insights/topic-intelligence) — the analytics portfolio. Resolution-correlation (resolved vs unresolved sentiment lift) is only computable on resolved conversation records, because "was it resolved" is a thread-level predicate, not an event-level one.
  • The native CDP — identity resolution and golden records, so the channel-side identifier an event carried lands as a person.

The common thread is the vocabulary: one channel namespace, one lifecycle namespace, one delivery namespace. "Failed" reads the same on the inbox, on Interactions, on Quality, and on the sentiment by-channel table, because they are all reading the same resolution output rather than each reconstructing it.

The raw tier, kept on purpose

None of this argues against the stream. It argues against mistaking it for the destination. Orbit ships the raw tier next to the resolved tier, because they serve different consumers:

  • `GET /events` — a cursor-paged history of recent platform events for the tenant.
  • `GET /events/stream` — an SSE live tail of the same stream, resumable by Last-Event-ID.
  • Consumer groups over `/events/consume` and `/events/consume/commit` — Kafka-style server-side offsets (at-least-once, monotonic commit, seek-to-replay) so high-volume pipelines do not have to hold their own cursor.
  • Event sinks and sources — push the same stream out to a tenant-owned Kafka topic or HTTP collector, and ingest the tenant's own external events in.

The rule of thumb ends up simple: build on conversations if what you need is a customer's history, a quality review, or an analyst's trend; reach for the stream when you are building the stream for someone else — a data-platform pipeline, a compliance store, a webhook fan-out of your own — and you will apply your own resolution on the far side. Consuming the raw bus when you wanted the resolved record is how teams end up shipping the failure gallery above.

If you are buying: three questions

Walk a CPaaS/CCaaS demo with these, and you will learn whether you are looking at resolution or at bus cosmetics.

  1. Show me one customer across two channels in one list — without opening a second tab. If the demo pivots to a CSV, an API call, or a second list, "unified" meant "we ingest raw events into one store," not "we resolve them."
  2. **Does delivered mean the same thing on every channel — and could you tell if a channel silently redefined it?** Channels drift. The resolution layer is the place that notices, because it owns the normalized vocabulary.
  3. Where does threading live? If the answer is "each consumer decides," the bus is not doing resolution; it is doing routing. Those are different jobs.

The answer to all three should be a surface, not a service — something you can point to that uses the resolved vocabulary, in production, today.

Frequently asked questions

Is this a rant against events?

No. Orbit runs on an event-driven backbone; the per-tenant event stream is the same plumbing the analytics, the compliance store, and the customer's own pipelines replay. The argument is narrower: an event stream is a transport-and-replay primitive, not a customer-history primitive. Use it to move and rebuild the resolved records, not in place of them.

Where does the resolution judgement live in Orbit?

In the shared routing and inbox layer, ahead of any dashboard or analytics surface. Each inbound event is routed to its channel and account, threaded into a conversation, linked to the contact's record, and normalized onto the platform's channel/status vocabulary before a surface can read it. A surface that re-does any of those four steps is a bug, not a design.

Can I still get the raw events out?

Yes — that is what the raw tier is for. GET /events pages the recent history, GET /events/stream is the live SSE tail, and consumer groups give you Kafka-style offsets over the same stream. Event sinks push it to your own Kafka topic or HTTP collector if your data platform wants the firehose. Raw where it is useful, resolved where it is read.

Semantic channel resolution — why raw event buses are not the answer to omnichannel — Orbit by Devotel