# Send Notifications

Send one push notification to your audience — every registered device, a device list, a subscriber list, or a group — and read the per-transport accepted/failed result.

One endpoint sends one notification to the devices registered with the API:

```text
POST https://app.nativenotify.com/api/universal/notifications/send
```

## Request

#### cURL

```bash
curl -X POST https://app.nativenotify.com/api/universal/notifications/send \
  -H "Content-Type: application/json" \
  -d '{
    "appId": 123,
    "appToken": "yourAppToken",
    "title": "Service this Sunday",
    "message": "Doors open at 9:00 — the 10:30 service is unchanged.",
    "audience": { "type": "all" }
  }'
```

#### Node.js

```js
const res = await fetch("https://app.nativenotify.com/api/universal/notifications/send", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    appId: 123,
    appToken: "yourAppToken",
    title: "Service this Sunday",
    message: "Doors open at 9:00 — the 10:30 service is unchanged.",
    audience: { type: "all" },
  }),
});
const send = await res.json();
console.log(send.accepted, "accepted of", send.attempted);
```

#### Python

```python
import requests

send = requests.post(
    "https://app.nativenotify.com/api/universal/notifications/send",
    json={
        "appId": 123,
        "appToken": "yourAppToken",
        "title": "Service this Sunday",
        "message": "Doors open at 9:00 — the 10:30 service is unchanged.",
        "audience": {"type": "all"},
    },
    timeout=30,
).json()

print(send["accepted"], "accepted of", send["attempted"])
```

| Field               | Required | Notes                                                                                                                                                                                                                                     |
| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `appId`, `appToken` | yes      | Your app's pair.                                                                                                                                                                                                                          |
| `title`             | yes      | Notification title.                                                                                                                                                                                                                       |
| `message`           | yes      | Body text. `body` is accepted as an alias.                                                                                                                                                                                                |
| `audience`          | yes      | Who this send reaches — the four shapes are below.                                                                                                                                                                                        |
| `pushData`          | no       | JSON object (or a JSON string) delivered to the app with the notification (deep links, ids).                                                                                                                                              |
| `bigPictureURL`     | no       | Absolute `https://` image URL, up to 2000 characters — see [Big picture images](#big-picture-images).                                                                                                                                     |
| `environment`       | no       | `"production"` (default), `"staging"` or `"development"` — the send reaches only devices registered in it.                                                                                                                                |
| `idempotencyKey`    | no       | Up to 200 chars. A retried send with the same key writes no duplicate inbox entries for devices that already have one. The push itself is never deduplicated.                                                                             |
| `skipInbox`         | no       | `true` writes **no notification-inbox entries** for this send — the push, the send record and analytics are unchanged. A non-boolean is a `400 invalid_field`.                                                                            |
| rich fields         | no       | `subtitle`, `badge`, `ttl`, `expiration`, `interruptionLevel`, `categoryId`, `channelId`, `collapseId`, `contentAvailable`, `mutableContent`, `sound` — the same fields and validation as [Rich Notifications](/docs/rich-notifications). |

## Audiences

```json
{ "audience": { "type": "all" } }
```

```json
{ "audience": { "type": "devices", "deviceIds": ["A1B2C3D4-install-key", "E5F6-install-key"] } }
```

```json
{ "audience": { "type": "subscribers", "subscriberIds": ["user_8241", "user_8242"] } }
```

```json
{ "audience": { "type": "group", "key": "weekly-news" } }
```

- `all` — every registered device, with or without a `subscriberId`.
- `devices` — the listed `deviceId`s, whether or not they were registered with a `subscriberId`.
- `subscribers` — every device registered under the listed `subscriberId`s.
- `group` — every device subscribed to a [group](/docs/push/groups), by subscriber id or device key. An unknown or empty group sends to nobody (`devices: 0`), not an error.

All four are filtered to the send's `environment` (production when omitted).

> **Plans:**
>
> On the **Premium** plan, a `group` audience — and a `devices` or `subscribers` list with more than one id — is a Pro feature: it answers `403 plan_upgrade_required` and nothing is sent. `all` and one subscriber work on every plan. See [what each plan includes](/docs/billing#what-each-plan-includes).

> **Live sends:**
>
> Sends are live and cannot be recalled. A dry run means a `devices` audience with one device of your own. Agents are instructed to confirm title, body and audience with you before sending.

## Big picture images

`bigPictureURL` attaches one image to the notification, on this endpoint, on [test sends](/docs/push/verification) and on [schedule rules](/docs/push/scheduling). It must be an absolute `https://` URL of at most 2000 characters — plain `http://` or anything else is a `400 invalid_field`, and blank or absent means no image.

| Transport     | On the device                                                                                                                              |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| iOS (APNs)    | The URL is the **Notification Service Extension**'s attachment source. An app without an extension shows the normal title + body.          |
| Android (FCM) | Android renders the image in the notification.                                                                                             |
| Web           | The service worker passes it to `showNotification`, and drops it first when the payload would not fit the push service's 4,096-byte limit. |

The image is fetched by the device, so host it anywhere public — the notification still arrives if the image does not.

## Response — `200`

```json
{
  "ok": true,
  "environment": "production",
  "sendId": 1000000412,
  "devices": 1284,
  "attempted": 1301,
  "accepted": 1298,
  "failed": 3,
  "byType": {
    "apns": { "attempted": 640, "accepted": 639, "failed": 1, "delivered": null },
    "fcm": { "attempted": 641, "accepted": 639, "failed": 2, "delivered": null },
    "web": { "attempted": 20, "accepted": 20, "failed": 0, "delivered": null }
  },
  "skipped": { "apns": 0, "fcm": 4 },
  "inboxSkipped": false,
  "note": "accepted = what the transport accepted at send time. delivered stays null — APNs / FCM cannot prove delivery and this service never fabricates it.",
  "checkedAt": "2026-09-23T09:30:00.000Z"
}
```

| Field                               | Meaning                                                                                                                                                       |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sendId`                            | This send's id — the `nn_notification_id` the push carries for open analytics. `null` if the analytics record could not be written (the send still went out). |
| `devices`                           | Devices the audience resolved to (a device counts once even when it has several tokens).                                                                      |
| `attempted` / `accepted` / `failed` | Tokens sent to, accepted by the transport, and rejected.                                                                                                      |
| `byType`                            | The same split per transport. `delivered` is always `null` — no transport can prove delivery.                                                                 |
| `skipped`                           | APNs / FCM tokens with no credentials configured for their transport. An app-config gap, never counted against the token (web push needs no credentials).     |
| `inboxSkipped`                      | Echo of the `skipInbox` choice — `true` = this send wrote no inbox entries, by request.                                                                       |

Failures are per token: only dead-token answers (`BadDeviceToken`, `Unregistered`, FCM `UNREGISTERED`, web `404`/`410`, …) add a health strike; credential or transient errors never do. Every targeted device also gets one entry in its [notification inbox](/docs/push/web-inbox) — written in the background, so list it after a short delay.

### Errors

| Status | Code                                             | Meaning                                                                                                                                                              |
| ------ | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `invalid_body`, `missing_field`, `invalid_field` | Bad JSON body, missing `appId` / `title` / `message`, or an invalid `appId`, rich field, `idempotencyKey` or `skipInbox`.                                            |
| `400`  | `missing_audience`, `invalid_audience`           | No `audience`, an unknown `type`, an empty id list, or a missing group `key`.                                                                                        |
| `400`  | `invalid_environment`                            | `environment` is not `production`, `staging` or `development`.                                                                                                       |
| `403`  | `trial_expired`                                  | The free trial ended, or sending is paused for an unpaid invoice ([Plans & Billing](/docs/billing#what-the-api-returns-while-sending-is-paused)) — nothing was sent. |
| `403`  | `plan_upgrade_required`                          | A group or multi-id audience on the Premium plan — nothing was sent.                                                                                                 |
| `403`  | `environment_disabled`                           | The target environment is switched off for this app — nothing was sent.                                                                                              |
| `404`  | `app_not_found`                                  | The app is not one this caller can access.                                                                                                                           |

A wrong `appId` / `appToken` pair answers `401` with a plain-text message, like the rest of the API.
