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:

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" }
  }'
FieldRequiredNotes
appId, appTokenyesYour app's pair.
titleyesNotification title.
messageyesBody text. body is accepted as an alias.
audienceyesWho this send reaches — the four shapes are below.
pushDatanoJSON object (or a JSON string) delivered to the app with the notification (deep links, ids).
bigPictureURLnoAbsolute https:// image URL, up to 2000 characters — see Big picture images.
environmentno"production" (default), "staging" or "development" — the send reaches only devices registered in it.
idempotencyKeynoUp 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.
skipInboxnotrue 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 fieldsnosubtitle, badge, ttl, expiration, interruptionLevel, categoryId, channelId, collapseId, contentAvailable, mutableContent, sound — the same fields and validation as Rich Notifications.

Audiences

{ "audience": { "type": "all" } }
{ "audience": { "type": "devices", "deviceIds": ["A1B2C3D4-install-key", "E5F6-install-key"] } }
{ "audience": { "type": "subscribers", "subscriberIds": ["user_8241", "user_8242"] } }
{ "audience": { "type": "group", "key": "weekly-news" } }
  • all — every registered device, with or without a subscriberId.
  • devices — the listed deviceIds, whether or not they were registered with a subscriberId.
  • subscribers — every device registered under the listed subscriberIds.
  • group — 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.

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.

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 and on schedule rules. 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.

TransportOn 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.
WebThe 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

{
  "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"
}
FieldMeaning
sendIdThis 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).
devicesDevices the audience resolved to (a device counts once even when it has several tokens).
attempted / accepted / failedTokens sent to, accepted by the transport, and rejected.
byTypeThe same split per transport. delivered is always null — no transport can prove delivery.
skippedAPNs / FCM tokens with no credentials configured for their transport. An app-config gap, never counted against the token (web push needs no credentials).
inboxSkippedEcho 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 — written in the background, so list it after a short delay.

Errors

StatusCodeMeaning
400invalid_body, missing_field, invalid_fieldBad JSON body, missing appId / title / message, or an invalid appId, rich field, idempotencyKey or skipInbox.
400missing_audience, invalid_audienceNo audience, an unknown type, an empty id list, or a missing group key.
400invalid_environmentenvironment is not production, staging or development.
403trial_expiredThe free trial ended, or sending is paused for an unpaid invoice (Plans & Billing) — nothing was sent.
403plan_upgrade_requiredA group or multi-id audience on the Premium plan — nothing was sent.
403environment_disabledThe target environment is switched off for this app — nothing was sent.
404app_not_foundThe 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.