[relaybell docs › Sending › Sending notifications](https://relaybell.com/docs/sending) · [All docs as Markdown](https://relaybell.com/docs.md) · [Index for AI assistants](https://relaybell.com/llms.txt)

# Sending notifications

> POST /api/v1/notifications: fields, targeting, scheduling, A/B tests, merge tags and cancelling a scheduled send.

**POST** `/api/v1/notifications` — accepts the send and answers `202`; delivery happens in the background. With no targeting fields it goes to **every subscriber** of the website the API key belongs to. Until the account's email address is verified it answers `403 email_not_verified` (test sends to your own test devices still work).

```bash
curl -X POST https://relaybell.com/api/v1/notifications \
  -H "Authorization: Bearer rb_live_xxxxx" -H "Content-Type: application/json" \
  -d '{
    "title": "Flash sale 🔥",
    "body": "30% off ends tonight — {name|there}!",
    "url": "https://shop.example/sale"
  }'
```

Response (`202`):

```json
{ "ok": true, "notificationId": "5b0c2f7e-1d7a-4c3e-9a51-2f8e6b1d0c42", "status": "queued" }
```

`status` is `"queued"` when sending starts now, or `"scheduled"` when it waits — for a future `sendAt`, or because your quiet hours are on (the send is held until they end). A send to *everyone* from an account less than 7 days old that would reach more than 5,000 browsers answers `"held_for_review"` with a `message`: our team checks a new account's first large sends before they go out (usually within a working day), and you can cancel it meanwhile. A narrower broadcast from such an account can be held the same way just before it goes out. Sends to specific people are never held. Keep `notificationId`: it is how the send appears in [`/stats`](https://relaybell.com/docs/analytics-reports#stats) and in [webhooks](https://relaybell.com/docs/webhooks), and how you cancel it.

| Field | Description |
| --- | --- |
| `title` \* | Notification title. Required — without it the answer is `400 title_required`. Supports [merge tags](https://relaybell.com/docs/sending#merge-tags) such as `{name\|there}` (the subscriber's `name` tag, or the text after `\|` when it's empty). |
| `body` | Message text. Supports [merge tags](https://relaybell.com/docs/sending#merge-tags). |
| `url` | Page opened when the notification is clicked. Left out, your **Default link** (Settings → Sending → Links) is used. Your UTM tags, if set, are added. Supports merge tags. |
| `icon` | Small image (URL) shown next to the text. Left out, your **Default notification logo** (Settings → General) is used. |
| `image` | Large picture (URL). Shown mainly by Chrome and Edge on Windows and Android; Safari, Firefox and Macs leave it out. |
| `badge` | Small one-colour icon (URL of a PNG with a transparent background, about 96×96 px) shown in the Android status bar. |
| `requireInteraction` | `true` keeps the notification on screen until the person clicks or closes it, instead of it fading after a few seconds. Chrome and Edge on computers honour it; Safari and Firefox ignore it. |
| `tag` | A label of your choice. A new notification with the same `tag` replaces one that is still showing instead of stacking beside it — useful for updates like “Your order is out for delivery”. |
| `actions` | Up to 2 buttons: `[{action,title,url}]`. Each needs a `title`; `action` defaults to `btn1`/`btn2`; `url` is kept only when it is an `http(s)` link. |
| `externalId` | Send to **one of your own users**: the user id you gave the browser SDK (`externalId` in `init()`, or `identify()`). It reaches every subscribed browser linked to that id. The id must match exactly (case matters). See [Send to one user](https://relaybell.com/docs/send-to-one-user). |
| `externalIds` | The same for a list: `["user_1", "user_2"]`. Used instead of `externalId` when both are sent. If every id is empty, the answer is `400 external_ids_empty` — it never falls back to sending to everyone. |
| `transactional` | `true` for a message a person is waiting for (order update, sign-in alert, a reply): it goes out straight away even during your quiet hours, skips the daily frequency cap, and is never listed in the public on-site inbox (`showInbox()`). Use it for messages to particular people, not for marketing. |
| `segmentId` / `filters` | Target a saved segment, or inline filters in the [segment filter format](https://relaybell.com/docs/segments-topics#segments). A segment that isn't in this project answers `400 segment_not_found`; one deleted before a scheduled send goes out makes that send fail (`error: "segment_missing"`) — it never falls back to everyone. Only one way of choosing people is used per send: `segmentId` first, then `filters`, then `externalIds`/`externalId` — so don't combine them. |
| `topicId` | Only subscribers of a topic — its id or its slug (e.g. `"tech-news"`, any case). Combines with the fields above. An unknown topic answers `400 unknown_topic`. |
| `excludeSegmentId` / `excludeFilters` | Leave a segment (or the people matching inline filters) out of this send. `excludeSegmentId` wins when both are sent; an unknown one answers `400 segment_not_found`. |
| `sendAt` | ISO 8601 time to schedule the send (status `"scheduled"`). A time in the past, or one that can't be read, sends now. A send with `sendAt` isn't moved by quiet hours. |
| `perTimezone` + `localTime` | Deliver at a local time (`"H:MM"` or `"HH:MM"`, e.g. `"09:00"`) in each subscriber's time zone. `sendAt` is ignored, and without a valid `localTime` it sends normally. |
| `smartTime` | Deliver at each subscriber's most-active hour. `sendAt` is ignored. |
| `ttl` | How long (seconds) the push service keeps trying to reach a device that is offline. Default 24 hours, maximum 28 days. |
| `translations` | `{ "es": {title,body}, ... }` — keyed by two-letter language, picked by each subscriber's browser language; others get the main text. |
| `ab` | A/B test: `{ "b": { title, body, url, image } }` — each browser gets version A or B (fields left out of B come from A). Ignored unless `b` has at least one field. |
| `abTest` | Auto-winner, with `ab`: `{ autoWinner:true, testPercent:20, afterHours:4 }` — sends both versions to `testPercent`% (1–90, default 20), then after `afterHours` (1–168, default 4) sends the version with the higher click rate to the rest (with no clicks yet, the rest get the normal A/B split). |

## Merge tags (personalization) {#merge-tags}

Write `{key}` or `{key|fallback}` in a send's `title`, `body` or `url`, and each subscriber gets their own value. An empty or unknown value becomes the fallback (the text after `|`), or nothing when there is none. Keys are letters, digits, `_`, `.` and `-`, and are case-sensitive.

| Tag | Value |
| --- | --- |
| `{external_id}` | The user id linked to the subscriber (`externalId`). |
| `{url}` | The last page of your site the subscriber was on when they subscribed or were last updated. |
| `{language}` | The browser's language, e.g. `en-GB`. |
| `{country}` | The subscriber's 2-letter, upper-case country code (ISO 3166-1, e.g. `IN`), worked out from the visitor's IP address when the browser subscribes or is saved again; empty when unknown. It can't be set from the SDK — use a tag for that. |
| `{KEY}` | Any of the subscriber's tags (`tags` in `init()`, `setTags()`, an import or a bulk label), e.g. `{name\|there}` or `{plan}`. A tag with the same name as a built-in one wins. |

Tags are filled in only when the main `title`, `body` or `url` contains one; then the A/B version B and the `translations` are personalized too. In [automation](https://relaybell.com/docs/events-automation#automations), [journey](https://relaybell.com/docs/sequences) and [product alert](https://relaybell.com/docs/product-alerts) messages the same `{key|fallback}` syntax takes its values from the **event's properties** instead (for example `{product}`, `{url}`) — not from the subscriber.

## Cancel a scheduled send {#cancel}

**POST** `/api/v1/notifications/:id/cancel` — stops a send that is still `"scheduled"` (a future `sendAt`, or one held for quiet hours) or `"held_for_review"`. Answers `200 { "ok": true, "canceled": true }`. A send that has already started (including a per-time-zone or smart-time send, which starts at once), has finished, was already canceled, doesn't exist or belongs to another website answers `404 { "error": "not_scheduled" }`.

```bash
curl -X POST https://relaybell.com/api/v1/notifications/5b0c2f7e-1d7a-4c3e-9a51-2f8e6b1d0c42/cancel \
  -H "Authorization: Bearer rb_live_xxxxx"
```
