What is routable
Only the five call events that mean something to an external system:ai_session.*, campaign.*, agent.* and lead.completed are webhook-only
by design. They have no Salesforce meaning, so they are not rows in the
matrix at all rather than rows that cannot be changed.
Destinations
- Salesforce — contribute to this tenant’s
Call_Detail__cwriteback. Requires a configured Salesforce connected app; without one the column is locked in the dashboard, because a checkbox that writes nowhere is worse than an absent one. Three of the five Salesforce cells have no producer at all — see below. - Webhook — publish to your webhook subscriptions.
Cells with no producer
Three Salesforce cells exist in the matrix but nothing reads them:
Yotel’s Salesforce writeback is triggered by the disposition
(
call.disposition_set) and by the recording (call.recording_ready) —
never by a lifecycle event. One Call_Detail__c per call, carrying the outcome
the call actually had.
Every unsupported cell ships off, is locked in the dashboard, and is listed
in the unsupported array of every routing GET — read that array rather than
hard-coding this table, since it is the API’s own answer and it carries a
human-readable reason.
How a cell is resolved
Per cell, most specific first:call.ended leaves every other event resolving through the tenant
default. And a stored false is an override, not an absence — turning a cell
off at tenant level really does turn it off, it does not fall through to the
shipped default.
A campaign is either inheriting — it has no override at all, and tracks
the tenant defaults as they change — or pinned to its own matrix. The
dashboard’s Inherit tenant defaults toggle is that state. Turning it off
seeds the campaign’s matrix from the values the campaign is resolving to right
now, so nothing changes until you change it; turning it back on discards the
override.
Shipped defaults
Chosen to reproduce the behaviour that existed before routing did. If you have never configured anything, this is what you have:
Every webhook column ships on so no existing subscription goes quiet — a
tenant does not notice a webhook that stops arriving, so that guarantee is
pinned by a test rather than left to review.
call.ended → Salesforce ships off because the Salesforce write is
triggered by the disposition, not by the hangup: one Call_Detail__c per
call, carrying the outcome the call actually had.
API
All four routes require theseats:manage permission (tenant admins).
A
GET returns the closed sets alongside the resolved matrix, and each cell
carries the layer that decided it:
unsupported lists every cell that has no producer (see
Cells with no producer). It is returned by all four
routes and is the authoritative list — the dashboard locks its cells from it.
Writing to one of those cells is not an error and the value is echoed back
on the next GET; it simply never does anything.
The campaign routes add campaign_id and inherits. inherits is the
authoritative answer to “is this campaign pinned?” — per-cell source
cannot tell you: a campaign that stored a partial override reports no
campaign sources while still being pinned.
A PUT body wraps the matrix:
call.ended → salesforce would be accepted and reported
back while changing nothing.)
Send {"event_routing": null} to clear — at campaign level that means “go back
to inheriting”, at tenant level “go back to the shipped defaults”. A partial
matrix is legal and is the point: what it does not name keeps resolving
through the layer below.
An unknown event or destination is a 422, never silently stored — a typo
would otherwise resolve to the shipped default and look like it worked.
