Channels
Apex sends communications through three channels: email, in-app, and mobile push. Every communication declares which channels it can be sent on; every end-user has per-channel preferences; every audience can predicate on those preferences. This page maps the chain end-to-end so operators know what each surface controls.
The chain
The same system serves transactional sends, adaptive journeys, and one-off broadcasts — only the gating differs. Here's the pipeline a single send travels:
- End-user channel preference — set by the end-user via the public preferences page (
/preferences/[token]) or by your code via the SDK. Stored aschannelPreferences[channel]: boolean | undefined. - Audience predicate — operator-authored. Optionally references the channel preference via
channel_opted_inorchannel_opted_outpredicates. Filters the recipient list at audience-resolution time. - Eligibility evaluator — runtime gate. Checks suppression, global opt-out, channel preference, journey-level overrides, quiet hours, and frequency caps. Runs once per (recipient, channel) pair, immediately before send.
- Send — the channel-specific delivery layer (SES for email, web/mobile push for push, in-app store for in-app). Records a
CommunicationRecordkeyed onmessageId.
The audience predicate (step 2) is permissive by design — it surfaces who's eligible. The eligibility evaluator (step 3) is conservative — it blocks the send if anything's off. This split keeps the operator's audience-size estimate honest while preserving every regulatory and user-preference gate at the actual send moment.
Channel preferences
EndUserCommPreferences carries a per-channel boolean:
channelPreferences: {
email: true, // explicit opt-in
in_app_push: false, // explicit opt-out
mobile_push: undefined, // not set yet
}
Three states matter:
true— explicit opt-in. The user actively chose this channel.false— explicit opt-out. The user actively chose to NOT receive this channel.undefined(or no record at all) — the user never set a preference. By default Apex treats this as opted-in for marketing — the same default the eligibility evaluator uses, so audience-size estimates match deliverable counts.
The "default opt-in" semantic is important: it means a freshly imported user can be included in an early-lifecycle audience without first being asked to set channel preferences. Jurisdictions that require express consent (CASL §6, GDPR Art. 7) should use the Strict consent checkbox in the audience builder to switch to explicit_optin mode (see below).
Audience predicates that touch channel preferences
The audience builder offers two channel-preference predicates:
Channel preference (channel_opted_in)
Includes users who CAN receive on a channel. Two modes:
any_not_optedout(default) — matches when the channel preference istrueorundefined(no record). Mirrors the eligibility evaluator's default-opted-in semantics. Use this for general marketing audiences in jurisdictions where prior consent isn't required.explicit_optin— matches ONLY when the channel preference istrue. Required by CASL and GDPR for express-consent obligations. Toggled via the audience builder's "Strict consent (CASL/GDPR)" checkbox.
Re-engagement audience (channel_opted_out)
Includes users who EXPLICITLY opted out of a channel (channel preference is false). This is a regulatory red zone:
- Workspace-admin only — the audience builder hides the entry for non-admins; the API gates the save.
- Audit-logged — every save writes a JOURNEY_AUDIT entry with the operator's identity and the channel(s) targeted.
- Right-to-object — GDPR Art. 21 and CASL §10 protect users who opted out from being targeted without a lawful re-engagement basis. Use this audience only with a regulator-defensible reason.
The predicate matches ONLY explicit false. Default-opted-in users (no record / undefined) are NOT included; otherwise an operator could re-target every user who never set a preference, which defeats consent.
Sends vs Recipients
When you send a comm to an audience, the SendMenu shows two numbers:
- Sends — total
(recipient × channel)units. Each one is a billable send and gets its ownCommunicationRecord. - Recipients — unique humans who receive at least one channel.
These are the same number when you send on a single channel. They diverge as you add channels — a user who's on both email and push counts twice in Sends, once in Recipients. The gap line ("K recipients receive on multiple channels") makes this explicit.
The reach preview applies per-recipient channel-preference filtering at preview time, so the numbers match what the runtime will actually fan out to. Suppression, global opt-out, and quiet-hours gates run at SEND time and are not reflected in the preview — those are conditions that can change between preview and send (e.g. a fresh bounce flips the suppression bit).
Channel completeness
Every comm declares which channels it supports, but declaring a channel doesn't mean content is authored for it. The composer surfaces a per-channel Draft / Ready state:
- Email is Ready when subject is set AND the body has at least one block beyond brand-header/brand-footer.
- In-app is Ready when
inAppTitleis set. - Push is Ready when
pushTitleis set.
Anything else is Draft. Draft channels show as outline-style chips on template cards and are disabled in the SendMenu's channel picker (with a link back to the composer to author content). The runtime never sends Draft channels.
v1 limitations
- Test sends are email-only. The composer's "Send test to me" / "Send test to specific address" run against the email channel regardless of the comm's declared channels. Other channels show as disabled in the test-channel picker; the audience tab supports all channels for real sends.
- No k-anonymity floor on reach numbers. The Sends/Recipients preview shows the literal computed counts, even on small audiences. We chose this over flooring at, say, 20 recipients because operators told us obscured numbers eroded their trust faster than the privacy benefit warranted.
- Channel preferences don't fan out. Setting
email = falsefor a user disables email only — they may still receive in-app and push if those channel preferences aretrueorundefined. The global opt-out (separate field) disables every channel.
Related
- Journey Audiences — the predicate language overview
- Adaptive Journeys — how journeys consume audiences
- Goals — what counts as a successful send