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"
}
}'{
"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
}
}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.
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"
}
}'{
"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
}
}target.
- Direct: If
recipient_idis set,targetis optional. - Broadcast: If
recipient_idis null,targetis required.
payload for in-app, email for email. Omitting both returns a 400, since such a send names no medium.
(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.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.Delivering email
Include the optional typedemail 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 for the full model.
- Broadcast email is capped. A broadcast carrying an
emailblock fans out to eligible recipients up to the project’smax_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.textis optional and derived fromhtmlwhen omitted. - Past the gate, email fires only when the recipient’s email preference is enabled and they have a primary email 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
emailfield via retrieve a 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).
{
"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>"
}
}
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, their 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.Authorizations
Bearer authentication header of the form Bearer bv_xxxxxxxxx, where bv_xxxxxxxxx is your API Key with Full access scope.
Body
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.
A target is the structure used to describe who should get a notification and how it should be categorized.
Show child attributes
Show child attributes
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.
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
emailblock on a broadcast returns400(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.
Show child attributes
Show child attributes
Response
Notification response