Send one push to every device, a list of devices or subscribers, or a group, and read the result per transport.
POST https://app.nativenotify.com/api/universal/notifications/send
Request
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" }
}'
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);
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 receives it. See Audiences. |
pushData | no | A JSON object (or JSON string) delivered to the app with the notification, for deep links and ids. |
bigPictureURL | no | Absolute https:// image URL, up to 2000 characters; anything else is 400 invalid_field. See Images. |
environment | no | "production" (default), "staging" or "development". Reaches only devices in that environment. |
idempotencyKey | no | Up to 200 chars. A retry with the same key writes no duplicate inbox entries. The push itself is never deduplicated. |
skipInbox | no | true writes no inbox entries for this send; the push and analytics are unchanged. A non-boolean is 400 invalid_field. |
| Rich fields | no | subtitle, badge, ttl, expiration, interruptionLevel, categoryId, channelId, collapseId, contentAvailable, mutableContent, sound, as in Rich notifications. |
Sends are live and can't be recalled, so test with a devices audience of one device you own.
Audiences
{ "audience": { "type": "all" } }
Every registered device, with or without a subscriberId.
{ "audience": { "type": "devices", "deviceIds": ["A1B2C3D4-install-key", "E5F6-install-key"] } }
The listed deviceIds, whether or not they have a subscriberId.
{ "audience": { "type": "subscribers", "subscriberIds": ["user_8241", "user_8242"] } }
Every device registered under the listed subscriberIds.
{ "audience": { "type": "group", "key": "weekly-news" } }
Every device subscribed to a group, by subscriber id or device key. An unknown or empty group sends to nobody (devices: 0), not an error.
On Premium, a group audience, or a devices or subscribers list of more than one id, answers 403 plan_upgrade_required and sends nothing (what each plan includes).
Images
bigPictureURL also works on test sends and schedule rules. Blank means no image.
| Transport | On the device |
|---|---|
| iOS (APNs) | The source for the Notification Service Extension's attachment. Without an extension the notification shows title and body only. |
| Android (FCM) | Rendered in the notification. |
| Web | Passed to the service worker as image. Dropped first when the payload exceeds 3,993 bytes. |
The device fetches the image, so host it anywhere public. If the image fails to load, the notification still arrives.
Response
{
"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 },
"health": {
"apns": { "struck": 1, "dead": 0 },
"fcm": { "struck": 2, "dead": 0 },
"web": { "struck": 0, "dead": 0 }
},
"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 | The nn_notification_id the push carries for open analytics. null if the analytics record couldn't be written; the send still went out. |
devices | Devices the audience resolved to. A device with several tokens counts once. |
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 and FCM tokens with no credentials saved. A setup gap, never counted against the token. Web push needs none. |
health | Per transport: tokens that took a failure strike (struck) and tokens this send retired (dead). |
inboxSkipped | Echoes skipInbox. |
Only dead-token answers (BadDeviceToken, Unregistered, FCM UNREGISTERED, web 404 or 410) add a strike. Credential and transient errors never do. Each targeted device also gets one inbox entry, written in the background, so list it after a short delay.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | invalid_body, missing_field, invalid_field | Bad JSON, a missing appId, title or message, or an invalid appId, rich field, bigPictureURL, 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 | Not production, staging or development. |
403 | trial_expired | The free trial ended, or sending is paused for an unpaid invoice (details). |
403 | plan_upgrade_required | A group or multi-id audience on Premium. |
403 | environment_disabled | The target environment is switched off for this app. |
404 | app_not_found | The app isn't one this caller can access. |
Every 403 means nothing was sent. A wrong appId and appToken pair answers 401 with a plain-text message, like the rest of the API.