> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bodhveda.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mediums

> In-app and email delivery in Bodhveda

A **medium** is a delivery transport. Bodhveda supports two today:

* **`in_app`** — the notification inbox you read via the [notifications API](/api-reference/endpoint/recipients/notifications/list-notifications). 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](https://console.bodhveda.com).

A single [send](/api-reference/endpoint/notifications/send-notification) 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.

```json theme={null}
{
    "recipient_id": "recipient_123",
    "target": { "channel": "digest", "topic": "none", "event": "sent" },
    "payload": { "title": "Your daily digest is ready." },
    "email": {
        "subject": "Your daily digest",
        "html": "<h1>Your daily digest</h1><p>3 new follow-ups today.</p>"
    }
}
```

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**:

```json theme={null}
{
    "recipient_id": "recipient_123",
    "target": { "channel": "conversation", "topic": "thread_7", "event": "reply" },
    "email": {
        "subject": "3 new messages on your thread",
        "html": "<p>Vikram and 2 others replied.</p>"
    }
}
```

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.

<Note>
  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.
</Note>

The notification still exists — you get an `id` back, and it carries the email delivery outcome for [retrieval](/api-reference/endpoint/notifications/get-notification) 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](/api-reference/endpoint/recipients/notifications/list-notifications), their [unread count](/api-reference/endpoint/recipients/notifications/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.

<Warning>
  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.
</Warning>

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](/docs/concepts/preferences).
2. **Preference** — the recipient's **email** preference for that target is enabled.
3. **Contact** — the recipient has a **primary email** [contact](/docs/concepts/recipients).

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](/api-reference/endpoint/notifications/get-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](/docs/concepts/unsubscribe) for both surfaces, and for what a spam complaint does.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.