OneSignal Push
Transports

OneSignal Push

Send push notifications via the OneSignal HTTP API

The OneSignal Push transport is part of @betternotify/onesignal. It delivers push notifications through the OneSignal HTTP API. It uses plain fetch() with zero external dependencies, so it works in Node.js, Bun, Cloudflare Workers, and any runtime with a global fetch.

The package also ships email and SMS transports — each is independent, pick only what you need.

Getting your API key

  1. Sign up or log in at onesignal.com.
  2. Open your app in the dashboard and copy the App ID from Settings → Keys & IDs.
  3. From the same screen, copy the REST API Key.

Store them as environment variables:

ONESIGNAL_APP_ID=00000000-0000-0000-0000-000000000000
ONESIGNAL_API_KEY=os_v2_app_xxxxxxxxxxxxxxxxxxxxxxxxx

Install

npm install @betternotify/onesignal @betternotify/core @betternotify/push

Usage

import { createNotify, createClient } from '@betternotify/core';
import { pushChannel } from '@betternotify/push';
import { onesignalPushTransport } from '@betternotify/onesignal';

const push = pushChannel();

const rpc = createNotify({ channels: { push } });
const catalog = rpc.catalog({
  /* routes */
});

const notify = createClient({
  catalog,
  transportsByChannel: {
    push: onesignalPushTransport({
      appId: process.env.ONESIGNAL_APP_ID!,
      apiKey: process.env.ONESIGNAL_API_KEY!,
    }),
  },
});

Targeting

Push delivery uses OneSignal subscription IDs (formerly player IDs) via the include_subscription_ids field. Pass them through the to field on RenderedPush.

If you store user external IDs instead, you'll want to add an alias-targeting layer in front of the transport or fetch subscription IDs before sending.

Endpoint: POST /notifications?c=push

Options

Prop

Type

Custom OneSignal fields

The transport maps RenderedPush fields to the OneSignal API automatically. For any OneSignal-specific field not covered by the rendered message, use body (applied to every send) and bodyFor (per-route overrides).

Pass <typeof catalog> as a type parameter to get autocomplete on route names:

const catalog = rpc.catalog({
  orderReady: rpc.push().input(/* ... */).title(/* ... */).body(/* ... */),
  promoAlert: rpc.push().input(/* ... */).title(/* ... */).body(/* ... */),
});

const transport = onesignalPushTransport<typeof catalog>({
  appId: process.env.ONESIGNAL_APP_ID!,
  apiKey: process.env.ONESIGNAL_API_KEY!,
  body: { ttl: 259200, isIos: true, isAndroid: true },
  bodyFor: {
    orderReady: { priority: 10, ios_sound: 'order_ready.wav' },
    promoAlert: { priority: 5, collapse_id: 'promo' },
  },
});

Per-send overrides

You can also pass OneSignal fields per-send via the transport key in .send():

await notify.orderReady.send({
  to: 'subscription-id-abc',
  input: { orderId: 'ORD-4521', storeName: 'Downtown Cafe' },
  transport: {
    onesignal: { priority: 10, ios_sound: 'alarm.wav' },
  },
});

Merge order

body (global) → bodyFor[route] (per-route) → transport.onesignal (per-send) → mapped rendered fields (always win).

Common push fields

ttl, priority, collapse_id, ios_sound, ios_interruption_level, ios_badgeType, buttons, web_buttons, url, app_url, web_url, big_picture, chrome_web_image, android_channel_id, included_segments, filters, include_aliases, send_after, idempotency_key

See the OneSignal API reference for the full list of supported fields.

Error handling

The transport maps HTTP status codes to Better-Notify error codes using the shared mapHttpStatus utility:

HTTP statusBetter-Notify codeRetriable
400VALIDATIONNo
422VALIDATIONNo
401CONFIGNo
403CONFIGNo
429RATE_LIMITEDYes
5xxPROVIDERYes

Network failures are wrapped as PROVIDER errors and timeouts as TIMEOUT errors.

Empty-id responses

OneSignal returns HTTP 200 with id: "" when every targeted subscription, address, or phone number is invalid or unsubscribed. The transport surfaces this as a non-retriable VALIDATION error so failover (via multiTransport) and queue retry layers don't waste attempts on impossible sends.

On this page