Skip to main content
A medium is a delivery transport. Bodhveda supports two today:
  • in_app — the notification inbox you read via the notifications API. This is the default and always available.
  • email — an email delivered through your own email provider (Resend in v1). Configured per project in the Console.
A single send can fan out to both. You control which mediums are attempted per send by the content blocks you include.

Content block implies intent

Each medium has its own content block on the send call, and including a block is how you say “attempt this medium”:
  • payload ⇒ the notification is written to the in-app inbox.
  • email ⇒ email is eligible for this send.
There is no mediums array to set, and no fallback that derives an email from payload — if there is no email block, no email is sent.
The rule cuts both ways: omit payload and no in-app notification is created.

Email-only sends

On a direct send, payload is optional. A send carrying only an email block delivers the email and creates no in-app notification:
This exists because in-app and email often want different timing. A support inbox wants one in-app row per reply, the instant it happens — but a single debounced email covering the last five minutes, rather than five separate emails. That is two sends: an instant in-app one, and a later email-only one. Without this, the debounced email would drop a duplicate row into the feed minutes after the instant one.
A send must carry at least one content block. Omitting both payload and email returns a 400 — such a send names no medium, so it would do nothing. An explicit "payload": null counts as omitted.
The notification still exists — you get an id back, and it carries the email delivery outcome for retrieval and shows up in the Console. It just isn’t a recipient-facing one: its status is not_requested, and it is excluded from the recipient’s feed, their unread count, and mark-all-read. Because in-app was never requested, the recipient’s in-app preference is irrelevant to such a send. It is still gated by the email catalog, the recipient’s email preference, and a primary email contact, exactly like any other email. payload remains required on a broadcast — see below.

Email on a broadcast

A broadcast may carry an email block, and each matching recipient gets their own email. The per-medium gates below apply per recipient, so a broadcast reaches the intersection of its audience and the recipients whose email preference is on and who have a primary email contact. Two rules are specific to broadcasts:
  • payload is required. Email-only broadcasts are not supported, unlike email-only direct sends. A broadcast is the high blast-radius path, and “it also landed in their inbox” is what makes a mistaken send visible and recoverable afterwards. Without this rule, a fire-and-forget mass email would leave no in-product trace.
  • The email fan-out is capped per project, at 100 recipients by default. This is a safety rail, not a billing limit — raise it in your project’s email settings when you mean to.
Exceeding the cap blocks the email half of the broadcast entirely rather than emailing the first 100. A partial mass email is worse than none: you cannot tell who got it without reading the delivery rows, and re-sending double-sends everyone who did. The in-app half still delivers to the full audience.
The cap is enforced at fan-out, not at request time — the audience is not known when the send is accepted.

Each medium is gated independently

Even when an email block is present, email fires only when all of the following hold:
  1. Catalog — the project has declared the (target, email) pair as a subscribable preference.
  2. Preference — the recipient’s email preference for that target is enabled.
  3. Contact — the recipient has a primary email contact.
In-app and email preferences are toggled independently for the same target, so a recipient can keep the in-app bell while turning email off (and vice-versa). When a medium is skipped or fails, the send still succeeds (a partial-medium failure never rejects the whole send). Because sends resolve asynchronously, the per-medium outcome is not in the send response — read it back from retrieve a notification, which carries the in-app status and the email delivery outcome.

Unsubscribe

Every outbound email carries the one-click List-Unsubscribe headers that Gmail and Yahoo require. Bodhveda hosts that page itself — when a recipient clicks it, Bodhveda flips that recipient’s email preference for that target off, exactly as if they had toggled it in your settings UI. Subsequent sends then record the email as muted. That surface is automatic and there is nothing to call. The visible unsubscribe link in your email body is yours to build, and the same bulk-sender rules expect one. See Unsubscribe for both surfaces, and for what a spam complaint does.