Webtonative Push Notification API

Send push notifications to your app's users from your own server, and cancel notifications you scheduled earlier.

EndpointPurpose
POST /api/v1/public/push-notification/sendSend a notification now, schedule it for later, or save it as a draft
POST /api/v1/public/push-notification/{id}/cancelCancel a scheduled notification before it goes out

All URLs in this document are relative to the Webtonative API base URL, written as {BASE_URL}.


Before you start

You need:

  1. An organisation API key and secret that have the WEBTONATIVE_NOTIFICATION permission. Create one from your organisation's API key settings in the Webtonative dashboard. The secret is shown only once, so store it safely.
  2. Your app's ID (appId). The app must belong to the same organisation as the API key.
  3. A plan that includes Webtonative Push. It isn't included on one-time plans. The app's demo or subscription must also still be active.
  4. Push set up for each platform you send to:
    • Android: a Firebase project linked in the Webtonative Push addon.
    • iOS: an APNs key added in the Webtonative Push addon. iOS notifications only work once the app has been purchased.

Keep your API secret on your server. Never put it in your website, mobile app or any other client-side code.


Authentication

Send your key and secret in these headers on every request:

HeaderValue
x-api-keyYour API key
x-api-secretYour API secret
Content-Typeapplication/json
ProblemHTTP statusResponse body
A header is missing, the key or secret is wrong, or the key has expired or been disabled401{ "statusCode": 401, "message": "Unauthorized" }
The key doesn't have the WEBTONATIVE_NOTIFICATION permission403{ "statusCode": 403, "message": "Forbidden" }

1. Send a notification

POST {BASE_URL}/api/v1/public/push-notification/send

Request body

FieldTypeRequiredDescription
appIdstringYesYour app's ID.
titlestringYes, unless action is DRAFTNotification title. Up to 120 characters.
bodystringYes, unless action is DRAFTNotification message. Up to 1000 characters.
launchUrlstringNoThe page that opens in the app when the user taps the notification. Must start with http:// or https://.
imageUrlstringNoA public image URL to show in the notification. Must start with http:// or https://.
platformsstring[]No"android", "ios" or both. If you leave this out, every platform that is available and set up for the app is used. Unknown values are ignored.
soundsobjectNoCustom notification sounds. See Sounds.
audienceobjectNoWho receives the notification. Defaults to everyone. See Audience.
scheduleTypestringNo"IMMEDIATE" (default) or "SCHEDULED".
scheduledAtstringWhen scheduleType is SCHEDULEDDelivery time in ISO 8601 format, for example "2026-10-20T09:30:00Z". It must be in the future.
actionstringNo"SEND" (default) sends or schedules the notification. "DRAFT" only saves it as a draft in your dashboard, so nothing is sent.

Text fields are trimmed before they are checked.

Sounds

Use one of the custom notification sounds already added to your app's build, referenced by its file name.

"sounds": {
  "android": { "name": "order_ready.mp3" },
  "ios": { "name": "order_ready.wav" }
}
  • The name must exactly match a sound added to the app for that platform.
  • File names must start with a lowercase letter, contain only lowercase letters, digits and _, and end in .mp3 or .wav.
  • Leave out a platform, or set it to null, to use the default system sound.

Audience

Everyone (default when you leave out audience). This sends to every subscribed device on the selected platforms:

"audience": { "type": "EVERYONE" }

Specific users. You describe the users with groups, which is a list of groups where each group is a list of filters:

  • A device must match all filters in a group (AND).
  • A device only has to match any one group (OR).
"audience": {
  "type": "SPECIFIC",
  "groups": [
    [ { "type": "TAG", "key": "plan", "operator": "IS", "value": "premium" },
      { "type": "LOGIN", "operator": "LOGGED_IN" } ],
    [ { "type": "USER", "field": "EMAIL", "operator": "IS", "value": "jane@example.com" } ]
  ]
}

The example above reaches logged-in users tagged plan = premium, or the user with the email jane@example.com.

Limits: up to 10 groups, and up to 10 filters per group.

Filter types

typeFieldsMatches devices where…
TAGkey, operator: IS | IS_NOT | EXISTS | NOT_EXISTS, value (required for IS and IS_NOT)The device's tag key equals the value, doesn't equal it, is set, or isn't set. Keys may use letters, numbers, _ and -, up to 64 characters. Values are cut to 256 characters.
USERfield: EMAIL | EXTERNAL_USER_ID, operator: IS | IS_NOT, valueThe user's email or your own user ID matches (or doesn't match) the value. Emails are compared in lowercase.
LOGINoperator: LOGGED_IN | ANONYMOUSThe user is logged in (has an external user ID), or isn't.
CREATED_AToperator: BEFORE | AFTER, date (ISO date)The device subscribed before the given day, or after the end of the given day.
SESSIONfield: FIRST_SESSION | LAST_SESSION, operator: LESS_THAN | GREATER_THAN, value (number > 0), unit: MINUTES | HOURS | DAYSThe first or last session happened less than, or more than, the given time ago. For example, LAST_SESSION GREATER_THAN 7 DAYS means "hasn't opened the app in 7 days".
APP_VERSIONplatform: android | ios, operator: IS | IS_NOT, versions (string[], 1–50 items, each up to 64 characters)The device is on that platform and its app version is (or isn't) one of the listed versions.

Examples:

{ "type": "TAG", "key": "city", "operator": "EXISTS" }
{ "type": "USER", "field": "EXTERNAL_USER_ID", "operator": "IS", "value": "user_8812" }
{ "type": "CREATED_AT", "operator": "AFTER", "date": "2026-09-01" }
{ "type": "SESSION", "field": "LAST_SESSION", "operator": "GREATER_THAN", "value": 7, "unit": "DAYS" }
{ "type": "APP_VERSION", "platform": "android", "operator": "IS_NOT", "versions": ["1.0.0", "1.0.1"] }

Examples

Send to everyone now:

curl -X POST "{BASE_URL}/api/v1/public/push-notification/send" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-api-secret: YOUR_API_SECRET" \
  -d '{
    "appId": "YOUR_APP_ID",
    "title": "Flash sale is live",
    "body": "Up to 50% off for the next 2 hours.",
    "launchUrl": "https://example.com/sale",
    "imageUrl": "https://example.com/images/sale.png"
  }'

Schedule a notification for one user on Android:

curl -X POST "{BASE_URL}/api/v1/public/push-notification/send" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-api-secret: YOUR_API_SECRET" \
  -d '{
    "appId": "YOUR_APP_ID",
    "title": "Your order has shipped",
    "body": "Order #1042 is on its way.",
    "platforms": ["android"],
    "audience": {
      "type": "SPECIFIC",
      "groups": [[{ "type": "USER", "field": "EXTERNAL_USER_ID", "operator": "IS", "value": "user_8812" }]]
    },
    "scheduleType": "SCHEDULED",
    "scheduledAt": "2026-10-20T09:30:00Z"
  }'

Node.js:

const res = await fetch(`${BASE_URL}/api/v1/public/push-notification/send`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": process.env.W2N_API_KEY,
    "x-api-secret": process.env.W2N_API_SECRET,
  },
  body: JSON.stringify({
    appId: process.env.W2N_APP_ID,
    title: "Flash sale is live",
    body: "Up to 50% off for the next 2 hours.",
  }),
})
const data = await res.json()
if (!data.isSuccess) throw new Error(data.error)
console.log(data.notification._id, data.notification.status)

Success response

HTTP 201:

{
  "isSuccess": true,
  "notification": {
    "_id": "6703f1c2a9e4b2d1c8e0f123",
    "appId": "YOUR_APP_ID",
    "source": "API",
    "title": "Flash sale is live",
    "body": "Up to 50% off for the next 2 hours.",
    "launchUrl": "https://example.com/sale",
    "imageUrl": "https://example.com/images/sale.png",
    "platforms": ["android", "ios"],
    "sounds": {},
    "audience": { "type": "EVERYONE", "groups": [] },
    "scheduleType": "IMMEDIATE",
    "scheduledAt": null,
    "status": "SENDING",
    "stats": {
      "targeted": 0,
      "accepted": 0,
      "failed": 0,
      "delivered": 0,
      "opened": 0,
      "errors": []
    },
    "createdAt": "2026-10-08T10:15:30.000Z",
    "updatedAt": "2026-10-08T10:15:30.000Z"
  }
}

Save notification._id. You need it to cancel a scheduled notification.

notification.status tells you what happened:

StatusMeaning
SENDINGThe notification is being delivered now. Delivery runs in the background, so stats starts at zero.
SCHEDULEDThe notification will be sent at scheduledAt. You can still cancel it.
DRAFTSaved as a draft (action: "DRAFT"). Nothing was sent.

Once delivery is finished, the notification moves to SENT, FAILED or NO_RECIPIENTS. You can see the final status and delivery stats in the Webtonative dashboard.

Error responses

Validation errors are returned with HTTP 201 and isSuccess: false, so always check isSuccess:

{ "isSuccess": false, "error": "Add a title and a message before sending" }
errorCause
Title can be at most 120 characterstitle is too long.
Message can be at most 1000 charactersbody is too long.
Launch URL must start with http:// or https://launchUrl isn't a valid URL.
Image URL must start with http:// or https://imageUrl isn't a valid URL.
Invalid delivery timescheduledAt can't be read as a date.
Add a title and a message before sendingtitle or body is empty.
Select at least one platformplatforms contains no valid platform, or none is available.
This app has no Android build / This app has no iOS buildThe app wasn't built for that platform.
iOS notifications are available once your app is purchasedYou sent to iOS on an unpaid app.
Set up Android push first — link a Firebase project in the WebToNative Notification addonAndroid push isn't set up.
Set up iOS push first — add your APNs key in the WebToNative Notification addoniOS push isn't set up.
Pick a delivery timescheduleType is SCHEDULED, but scheduledAt is missing.
Delivery time must be in the futurescheduledAt is in the past.
"<name>" isn't one of your app's Android notification sounds (or iOS)The sound doesn't exist in the app, or its file name is invalid.
audience.type must be EVERYONE or SPECIFIC, Add at least one filter for specific users, Invalid tag operator, …The audience object is invalid. The message says which part is wrong.

App access errors come back with an HTTP error status:

HTTP statusBodyCause
401{ "isSuccess": false, "error": "UNAUTHENTICATED_USER" }appId is missing, doesn't exist, or belongs to a different organisation than the API key.
403{ "isSuccess": false, "error": "DEMO_EXPIRED", "message": "The demo period for this app has expired" }The app's demo has expired.
403{ "isSuccess": false, "error": "PLAN_EXPIRED", "message": "The plan for this app has expired" }The app's subscription has expired.
403{ "isSuccess": false, "error": "FEATURE_NOT_ALLOWED", "message": "Webtonative notification is not available on the current plan" }The app's plan doesn't include Webtonative Push.

Rate limit

Send calls are limited per app, in 15-minute windows. A window starts at the first call and lasts 15 minutes.

limit per window = max(subscribed devices × 10, 100)

So an app with 5 subscribers can make 100 calls per window, and an app with 2,000 subscribers can make 20,000. The limit is worked out once at the start of each window. Every send call counts towards it, including calls that fail validation.

Each send response includes these headers:

HeaderMeaning
X-RateLimit-LimitCalls allowed in the current window
X-RateLimit-RemainingCalls left in the current window
X-RateLimit-ResetSeconds until the window resets

When you go over the limit, you get HTTP 429 with a Retry-After header (in seconds):

{
  "isSuccess": false,
  "errorCode": "RATE_LIMIT_EXCEEDED",
  "error": "Rate limit of 100 requests per 15 minutes exceeded. Try again in 412 seconds.",
  "retryAfter": 412
}

Tip: Don't make one call per user. One call with an audience filter (for example a TAG filter) reaches many users at once.


2. Cancel a scheduled notification

POST {BASE_URL}/api/v1/public/push-notification/{id}/cancel

This cancels a notification whose status is SCHEDULED. Notifications that are already sending, sent or saved as drafts can't be cancelled.

Path parameter

ParameterDescription
idThe notification._id returned by the send call.

Request body

FieldTypeRequiredDescription
appIdstringYesThe app the notification belongs to.

Example

curl -X POST "{BASE_URL}/api/v1/public/push-notification/6703f1c2a9e4b2d1c8e0f123/cancel" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "x-api-secret: YOUR_API_SECRET" \
  -d '{ "appId": "YOUR_APP_ID" }'

Success response

HTTP 201:

{ "isSuccess": true }

Error responses

All of these come back with HTTP 201 and isSuccess: false:

BodyCause
{ "isSuccess": false, "error": "Invalid id" }id isn't a valid notification ID.
{ "isSuccess": false, "error": "This notification is no longer scheduled" }The notification doesn't exist in this app, or it isn't SCHEDULED (it has already been sent, was cancelled before, or is a draft).
{ "isSuccess": false, "error": "UNAUTHENTICATED_USER" }appId is missing, doesn't exist, or belongs to a different organisation than the API key.
{ "isSuccess": false, "error": "DEMO_EXPIRED", ... }The app's demo has expired.
{ "isSuccess": false, "error": "PLAN_EXPIRED", ... }The app's subscription has expired.
{ "isSuccess": false, "error": "FEATURE_NOT_ALLOWED", ... }The app's plan doesn't include Webtonative Push.

The cancel endpoint isn't rate limited.


Handling responses

  1. Check the HTTP status. 401 and 403 mean an authentication or app access problem, and 429 means you hit the rate limit (wait for Retry-After seconds).
  2. Check isSuccess. If it's false, the request was rejected and error says why.
  3. If it's true, keep notification._id and notification.status from the send response.