# Overview

Send email and push notifications to a user or a whole workspace through one gated call.

`bb.notification.send()` delivers one notification across email and push at
once. Channel selection is not your decision at the call site — the org
settings, the event config, and the end user's workspace preferences decide
which channels actually fire.

```typescript
import BuildBase from '@buildbase/sdk';

const bb = BuildBase({
  serverUrl: 'https://api.console.buildbase.app',
  orgId: process.env.BUILDBASE_ORG_ID!,
  getSessionId: async () => getCookies().get('bb-session')?.value ?? null,
});

const result = await bb.notification.send(
  workspaceId,
  'comment_added',
  userId,
  {
    message: 'Alice commented on your project',
    url: '/projects/42#comment-8',
  }
);

// `sent` only tells you a recipient was resolved — check `channels`
// to see whether email or push actually went out.
if (!result.channels.email && !result.channels.push) {
  console.warn('Notification resolved a recipient but sent nothing.');
}
```

> **Note:**
  Create the notification event and its email template in the BuildBase console
  under **Notifications**. An event with no template still fires push but
  reports `channels.email: false`.


## Parameters

| Parameter     | Type               | Default  | Description                                               |
| ------------- | ------------------ | -------- | --------------------------------------------------------- |
| `workspaceId` | `string`           | Required | Workspace the notification belongs to                     |
| `event`       | `string`           | Required | Event slug, e.g. `comment_added`                          |
| `userId`      | `string`           | —        | User to notify. **Omit to notify every workspace member** |
| `data`        | `NotificationData` | `{}`     | Merge data for the template and the push payload          |

Omitting `userId` is the workspace broadcast. It is an easy mistake to pass
`null` expecting a no-op and mail the entire workspace instead.

## Notification data

`message` does double duty: it is the email body merge value and the push
notification body.

| Field     | Type                       | Description                                                                                           |
| --------- | -------------------------- | ----------------------------------------------------------------------------------------------------- |
| `title`   | `string`                   | Push title. Falls back to the event name                                                              |
| `message` | `string`                   | Email body and push body                                                                              |
| `icon`    | `string`                   | Push icon URL. Falls back to the org icon                                                             |
| `image`   | `string`                   | Large image in the push body                                                                          |
| `badge`   | `string`                   | Monochrome status-bar icon (Android, ChromeOS)                                                        |
| `url`     | `string`                   | Click destination. Also `{{url}}` in the email template                                               |
| `tag`     | `string`                   | Grouping key — a new notification with the same tag **replaces** the previous one instead of stacking |
| `actions` | `{action, title, icon?}[]` | Action buttons, **max 2**                                                                             |
| `silent`  | `boolean`                  | Deliver without sound or vibration                                                                    |

Three merge fields are injected for you and overwrite anything you pass under
the same key: `name` and `email` from the recipient, and `workspaceName` from
the workspace.

### Restricting channels for one send

`data.channels` overrides the event's configured channels for this call only:

```typescript
// Email only, even if the event also has push enabled
await bb.notification.send(workspaceId, 'invoice_ready', userId, {
  message: 'Your invoice is ready',
  channels: { email: true },
});
```

The override is **opt-in per channel**. Once `channels` is present, a channel
missing from the object is treated as `false` — `{ email: true }` disables push.
Omit `channels` entirely to use the event's configuration.

The override can only narrow, never widen: a channel the gates block stays
blocked whether or not you request it.

## What comes back

```typescript
{
  sent: true,
  channels: { email: true, push: false },
  notifiedCount: 1,
}
```

`channels` reports what was **actually used**, not what was requested. A `false`
means that channel was blocked by the gates, was excluded by a `data.channels`
override, or — for email — that the event has no linked template.

> **Note:**
  **`sent` does not mean delivered.** It is `notifiedCount > 0`, and
  `notifiedCount` counts the target users the call *processed* — it increments
  once per user regardless of whether any channel fired. A call that resolves
  one user and sends nothing returns `sent: true` with
  `channels: { email: false, push: false }`.

Check `channels` to know whether anything actually went out.



`NotificationResult` declares an optional `reason`, but the current server
never populates it. Do not branch on it.

## Errors

| Condition                | Response                                                         |
| ------------------------ | ---------------------------------------------------------------- |
| Workspace does not exist | `404` — `{ success: false, message: 'Workspace "…" not found' }` |
| `userId` does not exist  | `404` — `{ success: false, message: 'User "…" not found' }`      |

A broadcast to a workspace with no members is not an error: it returns
`sent: false` with `notifiedCount: 0`.

## The four gates

Every channel passes through `canSendNotification()` before it fires. The checks
run in order and the first failure wins:

1. **Org global switch** — the developer turned off all email or all push.
2. **Event master switch** — `enabled: false` on the event kills it outright.
3. **Event channel default** — the developer disabled email or push for this
   specific event.
4. **Workspace override** — the end user's preference, honored **only if the
   event is marked `userManaged`**.

Layer 4 is the one that surprises people. An event that is not `userManaged`
ignores workspace preferences entirely, which is what you want for security
alerts and billing failures.

## Fail-open behavior

The gate is deliberately permissive:

| Condition                              | Result                                                                   |
| -------------------------------------- | ------------------------------------------------------------------------ |
| No event config exists in the database | **Sends** — backward compatibility for orgs predating the event registry |
| The gate throws unexpectedly           | **Sends** — it never throws upward                                       |
| Org settings row is missing            | Falls through to the remaining layers                                    |

Notifications fail open so a config gap cannot silently swallow a password
reset. The trade-off is that a typo in an event slug produces a delivered
notification rather than an error. Verify slugs against the
86+ events in
the [event catalog](/webhooks/overview).

## Next Steps

- [Slack notifications](/notifications/slack) — route the same events to a team channel.
- [Push notifications](/push-notifications/overview) — VAPID setup and subscriber management.
- [Webhooks & events](/webhooks/overview) — the full event catalog.
