OneSignal Email
Send emails via the OneSignal HTTP API
The OneSignal Email transport is part of @betternotify/onesignal. It delivers email 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 push and SMS transports — each is independent, pick only what you need.
Getting your API key
- Sign up or log in at onesignal.com.
- Open your app in the dashboard and copy the App ID from Settings → Keys & IDs.
- 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_xxxxxxxxxxxxxxxxxxxxxxxxxInstall
npm install @betternotify/onesignal @betternotify/core @betternotify/emailUsage
import { createNotify, createClient } from '@betternotify/core';
import { emailChannel } from '@betternotify/email';
import { onesignalEmailTransport } from '@betternotify/onesignal';
const email = emailChannel({
defaults: { from: { name: 'My App', email: 'noreply@example.com' } },
});
const rpc = createNotify({ channels: { email } });
const catalog = rpc.catalog({
/* routes */
});
const notify = createClient({
catalog,
transportsByChannel: {
email: onesignalEmailTransport({
appId: process.env.ONESIGNAL_APP_ID!,
apiKey: process.env.ONESIGNAL_API_KEY!,
}),
},
});Targeting
Email addresses are passed through the to field on RenderedMessage and mapped to OneSignal's email_to field.
Endpoint: POST /notifications?c=email
OneSignal is a notification platform first — its email API is oriented toward broadcast and campaigns, not deep transactional metadata. The following RenderedMessage fields are silently dropped: cc, bcc, custom headers, attachments, inlineAssets, tags, and priority. If you need them, prefer a dedicated email provider transport (e.g. Resend, Mailchimp).
Options
Prop
Type
Custom OneSignal fields
The transport maps RenderedMessage 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).
Per-send overrides
You can also pass OneSignal fields per-send via the transport key in .send():
await notify.welcome.send({
to: 'user@example.com',
input: { name: 'Alice' },
transport: {
onesignal: { email_preheader: 'Welcome aboard!' },
},
});Merge order
body (global) → bodyFor[route] (per-route) → transport.onesignal (per-send) → mapped rendered fields (always win).
Common email fields
template_id, email_preheader, 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 status | Better-Notify code | Retriable |
|---|---|---|
| 400 | VALIDATION | No |
| 422 | VALIDATION | No |
| 401 | CONFIG | No |
| 403 | CONFIG | No |
| 429 | RATE_LIMITED | Yes |
| 5xx | PROVIDER | Yes |
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.