> ## 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.

# Send notification

> Start sending notifications through Bodhveda API.

A send names an optional `target` (`channel`/`topic`/`event`) and carries a `payload` (in-app), an `email` block, or both. The mediums it asks for are the mediums it carries.

### Targets and your catalog

By default you can send **any** target, cataloged or not. Cataloging a target — creating a project preference for it — is what puts it on your recipients' preference screen and lets `email` resolve to deliverable; an uncataloged target still sends in-app, but no recipient can mute it.

If the project has **strict targets** turned on (a per-project setting, **off by default**), this endpoint instead rejects a send whose `(target, medium)` has no matching catalog entry — see the `400` below. A `topic: any` catalog entry satisfies the check for every concrete topic beneath it, so one entry covers an unbounded set of runtime-generated topics; you never need one entry per resource id. A send carrying no `target` at all is never checked.

Send a **direct** notification to a specific recipient or send a **broadcast** to all recipients subscribed to a `target`.

* **Direct**: If `recipient_id` is set, `target` is optional.
* **Broadcast**: If `recipient_id` is null, `target` is required.

A send must carry **at least one content block** — `payload` for in-app, `email` for email. Omitting both returns a `400`, since such a send names no medium.

<Warning>
  **The target must be cataloged.** If your project has no project-level preference for the `(target, medium)` a send is asking for, the request is rejected with `400` and nothing is written — for direct and broadcast alike. A `topic: any` catalog entry satisfies the gate for every concrete topic beneath it. See [the catalog is a gate](/docs/concepts/preferences#the-catalog-is-a-gate).
</Warning>

<Note>
  Sending is **asynchronous**. A direct send returns as soon as the notification is accepted — the response carries the notification `id` with `status: "enqueued"`. Preference gating, quota, and email are all resolved by a background worker. Read the resolved outcome back with [retrieve a notification](/api-reference/endpoint/notifications/get-notification).
</Note>

## Delivering email

Include the optional typed `email` block (`subject`, `html`, `text`) to also send an **email**. Its presence is your "email is eligible" signal — omit it and no email is sent (there is no fallback that derives an email from `payload`). See [mediums](/docs/concepts/mediums) for the full model.

* **Broadcast email is capped.** A broadcast carrying an `email` block fans out to eligible recipients up to the project's `max_broadcast_recipients_for_email`. Exceeding the cap **blocks email entirely** rather than mailing an arbitrary subset — a truncated blast looks like it worked.
* **No templating.** You render the subject/HTML/text yourself (e.g. with `@react-email`) and pass the result. `text` is optional and derived from `html` when omitted.
* Past the gate, email fires only when the recipient's email preference is enabled and they have a **primary email** [contact](/api-reference/endpoint/recipients/contacts/create-contact). Otherwise the send still succeeds and the email is skipped.
* The email is delivered by a background worker, so its outcome is **not** on the send response. Read it back on the notification's `email` field via [retrieve a notification](/api-reference/endpoint/notifications/get-notification).

## Email without an in-app notification

On a **direct** send, `payload` is optional. Omit it and Bodhveda sends the email **without** creating an in-app notification — useful when the two mediums want different timing (an instant in-app row per event, but one debounced email covering several).

```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>"
    }
}
```

The notification is still created and still returns an `id` — you need one to read the email outcome back — but 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. The recipient's **in-app** preference does not apply to it; the email gates above still do.

An explicit `"payload": null` is treated the same as omitting it. `payload` remains **required on a broadcast**.


## OpenAPI

````yaml POST /notifications/send
openapi: 3.0.3
info:
  title: Bodhveda Notifications API
  version: '1.0'
  description: API for sending notifications to recipients or broadcasting to all.
servers: []
security: []
paths:
  /notifications/send:
    post:
      tags:
        - Notifications
      summary: Send a notification (direct or broadcast)
      description: >-
        Start sending notifications through Bodhveda API.


        A send names an optional `target` (`channel`/`topic`/`event`) and
        carries a `payload` (in-app), an `email` block, or both. The mediums it
        asks for are the mediums it carries.


        ### Targets and your catalog


        By default you can send **any** target, cataloged or not. Cataloging a
        target — creating a project preference for it — is what puts it on your
        recipients' preference screen and lets `email` resolve to deliverable;
        an uncataloged target still sends in-app, but no recipient can mute it.


        If the project has **strict targets** turned on (a per-project setting,
        **off by default**), this endpoint instead rejects a send whose
        `(target, medium)` has no matching catalog entry — see the `400` below.
        A `topic: any` catalog entry satisfies the check for every concrete
        topic beneath it, so one entry covers an unbounded set of
        runtime-generated topics; you never need one entry per resource id. A
        send carrying no `target` at all is never checked.
      operationId: sendNotification
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendNotificationPayload'
      responses:
        '200':
          description: Notification response
          content:
            application/json:
              examples:
                Direct notification:
                  summary: Direct Notification Response
                  value:
                    message: >-
                      Direct notification queued for delivery to recipient
                      recipient_123.
                    data:
                      notification:
                        id: 42069
                        recipient_id: recipient_123
                        payload:
                          title: Elon, Zuck and others commented on your post.
                          post_url: /posts/post_id_123#comments
                        broadcast_id: null
                        target:
                          channel: posts
                          topic: post_id_123
                          event: new_comment
                        read: false
                        opened: false
                        created_at: '2025-11-07T05:31:56Z'
                        updated_at: '2025-11-07T05:31:56Z'
                        status: enqueued
                      broadcast: null
                Broadcast notification:
                  summary: Broadcast Notification Response
                  value:
                    message: >-
                      Broadcast notification sent successfully. It will be
                      delivered to all elligible recipients.
                    data:
                      notification: null
                      broadcast:
                        id: 42069
                        target:
                          channel: announcements
                          topic: product
                          event: new_feature
                        payload:
                          title: Big News!
                          message: We just launched a new feature.
                        created_at: '2025-08-11T20:54:25.039856+05:30'
                        updated_at: '2025-08-11T20:54:25.039856+05:30'
                Direct notification rejected:
                  summary: Rejected Notification Based on Preferences
                  value:
                    message: >-
                      No notification was sent. Recipient's preferences do not
                      allow delivery.
                    data:
                      notification: null
                      broadcast: null
                Direct notification with email:
                  summary: Direct Notification Fanned Out to Inbox + Email
                  value:
                    message: >-
                      Direct notification sent successfully to recipient
                      recipient_123.
                    data:
                      notification:
                        id: 42070
                        recipient_id: recipient_123
                        payload:
                          title: Your daily digest is ready.
                        broadcast_id: null
                        target:
                          channel: digest
                          topic: none
                          event: sent
                        read: false
                        opened: false
                        created_at: '2025-11-07T05:31:56Z'
                        updated_at: '2025-11-07T05:31:56Z'
                      broadcast: null
                      deliveries:
                        - medium: email
                          status: pending
                          address: alice@example.com
                          created_at: '2025-11-07T05:31:56Z'
                          updated_at: '2025-11-07T05:31:56Z'
        '400':
          description: >-
            Invalid send. **Only projects with strict targets enabled** return
            the uncataloged-target error — with the setting off (the default) an
            uncataloged target is accepted. The check runs per medium, against
            the mediums the send actually asks for, and applies to direct sends
            and broadcasts alike. It tests for the **existence** of a catalog
            entry, not whether it is enabled: an entry with `default_enabled:
            false` is a real state (defined, switched off) and a send to it is
            accepted.
          content:
            application/json:
              examples:
                TargetNotCataloged:
                  summary: Target not in the project's catalog (strict targets on)
                  value:
                    message: >-
                      Target billing/none/invoice_paid is not in this project's
                      catalog for the in_app medium. Create a project preference
                      for it before sending, or turn off strict targets in your
                      project settings.
      security:
        - BearerAuthWithAPIKeyWithFullAcessScope: []
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl -X POST https://api.bodhveda.com/notifications/send \
              -H "Authorization: Bearer bv_xxxxxxxxx" \
              -H "Content-Type: application/json" \
              -d '{
                "recipient_id": "recipient_123",
                "target": {
                  "channel": "posts",
                  "topic": "post_id_123",
                  "event": "new_comment"
                },
                "payload": {
                  "title": "Elon, Zuck and others commented on your post.",
                  "post_url": "/posts/post_id_123#comments"
                }
              }' 
        - lang: curl
          label: cURL — with email
          source: |-
            # Direct send fanned out to inbox + email.
            # The `email` block makes email eligible (direct sends only).
            curl -X POST https://api.bodhveda.com/notifications/send \
              -H "Authorization: Bearer bv_xxxxxxxxx" \
              -H "Content-Type: application/json" \
              -d '{
                "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>",
                  "text": "Your daily digest\n3 new follow-ups today."
                }
              }' 
        - lang: javascript
          label: JavaScript
          source: >-
            import { Bodhveda } from "@bodhveda/js";


            const bodhveda = new Bodhveda("bv_xxxxxxxxx");


            const res = await bodhveda.notifications.send({
                recipient_id: "recipient_123",
                target: { channel: "posts", topic: "post_id_123", event: "new_comment" },
                payload: {
                    title: "Elon, Zuck and others commented on your post.",
                    post_url: "/posts/post_id_123#comments",
                },
            });

            // res.notification (direct) or res.broadcast, plus res.deliveries
            for email.
        - lang: go
          label: Go
          source: >-
            import (
                "context"
                "encoding/json"

                bodhveda "github.com/MudgalLabs/bodhveda/sdk/go"
            )


            ctx := context.Background()

            client := bodhveda.NewClient("bv_xxxxxxxxx", nil)


            recipientID := "recipient_123"

            payload, _ := json.Marshal(map[string]any{
                "title":    "Elon, Zuck and others commented on your post.",
                "post_url": "/posts/post_id_123#comments",
            })

            res, _ := client.Notifications.Send(ctx,
            &bodhveda.SendNotificationRequest{
                RecipientID: &recipientID,
                Target:      &bodhveda.Target{Channel: "posts", Topic: "post_id_123", Event: "new_comment"},
                Payload:     payload,
            })

            // res.Notification (direct) or res.Broadcast, plus res.Deliveries
            for email.
components:
  schemas:
    SendNotificationPayload:
      type: object
      properties:
        recipient_id:
          type: string
          nullable: true
          description: |-
            The `id` of a recipient.
            - If null → this is treated as a **broadcast** notification.
            - If set → this is a **direct** notification to that recipient. 

             💡 If no recipient exists with the `recipient_id`, Bodhveda will create a new recipient with that `recipient_id`.
        target:
          $ref: '#/components/schemas/Target'
        payload:
          type: object
          description: >-
            Arbitrary JSON payload for the notification. This is the **in-app**
            content block — its presence is what makes the notification eligible
            for in-app delivery.


            **Optional on a direct send.** Omit it to deliver on email *without*
            creating an in-app notification (an *email-only* send) — the `email`
            block then stands alone. An explicit `null` counts as omitted.


            **Required on a broadcast**, which is in-app only.


            ⚠️ A send must carry at least one content block. Omitting both
            `payload` and `email` is a `400` — such a send names no medium.
          nullable: true
        email:
          $ref: '#/components/schemas/EmailContent'
    Target:
      type: object
      description: >-
        A **target** is the structure used to describe _who should get a
        notification_ and _how it should be categorized_.
      properties:
        channel:
          type: string
          description: Broad category for a notification (e.g., "posts").
        topic:
          type: string
          description: >-
            Subcategory inside the channel (e.g., "post_id_123"). 

            - `any` and `none` have special meaning. 

            - `any` means **all topics under this channel/event**. 

            - `none` means **only the base channel/event**. 

            - `any` is **not** a valid `topic` when sending a notification, but
            both are valid for preferences. 

             A preference with the target `posts:any:new_comment` will match a notification with target `posts:post_id_123:new_comment` but a preference with the target `posts:none:new_comment` will **only** match a notification with target `posts:none:new_comment`.
        event:
          type: string
          description: Event that triggered this notification (e.g., "new_comment").
      required:
        - channel
        - topic
        - event
    EmailContent:
      type: object
      description: >-
        Optional typed **email** content block. Its **presence** is your signal
        that email is eligible for this send (content-block-implies-intent).
        Absence ⇒ no email — there is **no** fallback that derives email from
        `payload`.


        - **Direct sends only.** Including an `email` block on a broadcast
        returns `400` (email is never sent on broadcasts).

        - Bodhveda does **no** templating in v1 — you render your own
        subject/HTML/text (e.g. with `@react-email`) and pass the result.

        - Email still fires only if the `(target, email)` pair is in the project
        catalog, the recipient's email preference is enabled, and the recipient
        has a primary email contact.
      properties:
        subject:
          type: string
          description: The email subject line. Required when an `email` block is present.
        html:
          type: string
          description: >-
            Rendered HTML body. At least one of `html` or `text` must be
            provided.
        text:
          type: string
          description: >-
            Plain-text body. Recommended for deliverability. If omitted, it is
            derived from `html`.
      required:
        - subject
  securitySchemes:
    BearerAuthWithAPIKeyWithFullAcessScope:
      type: http
      scheme: bearer
      bearerFormat: APIKey
      description: >-
        Bearer authentication header of the form `Bearer bv_xxxxxxxxx`, where
        `bv_xxxxxxxxx` is your **API Key** with `Full access` scope.

````

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