Docs Getting Started

Quick Start Guide

Get up and running with OpenPushAPI in under 5 minutes.

Last updated: April 2026 10 min read

Quick Start

OpenPushAPI lets you send push notifications to web, Android, and iOS users from one unified API. This guide walks you through setup in five steps.

Step 1 — Create an account and app

Create a free account at the console. Once logged in, create a new App. You'll receive two credentials:

  • app_key — Public key, used in your JavaScript SDK. Safe to embed in frontend code.
  • app_secret — Private key, used for server-side API calls. Never expose this in client-side code.

Step 2 — Install the JavaScript SDK

Add these two lines to the <head> or end of <body> on every page of your website:

<!-- Add to your website -->
<script src="https://sdk.openpushapi.com/v1/sdk.js"></script>
<script>
  OpenPush.init({
    appKey: 'your-app-key-here',
    autoPrompt: true,
    promptDelay: 3000
  });
</script>

Step 3 — Users opt in automatically

With autoPrompt: true, the SDK will show the browser's native push permission dialog to new visitors after promptDelay milliseconds. Once a user accepts, they're automatically registered as a subscriber in your dashboard.

Step 4 — Send your first notification

Use the REST API to send a push notification to all your subscribers:

curl -X POST https://api.openpushapi.com/v1/notifications \
  -H "Content-Type: application/json" \
  -H "X-App-Key: your-app-key" \
  -H "X-App-Secret: your-app-secret" \
  -d '{
    "title": "Hello World!",
    "body": "Your first push notification from OpenPushAPI.",
    "url": "https://yoursite.com",
    "target_type": "all"
  }'

Response

{
  "notification_id": 12345,
  "status": "queued",
  "total_recipients": 847,
  "is_ab_test": false,
  "message": "Notification queued successfully"
}

Step 5 — Check delivery stats

Check delivery status and click-through rates for any notification:

curl https://api.openpushapi.com/v1/stats/notifications/12345 \
  -H "X-App-Key: your-app-key" \
  -H "X-App-Secret: your-app-secret"

Authentication

OpenPushAPI uses two header-based credentials for authentication:

Header Usage Where to find
X-App-Key All requests (public & private) Console → App → Settings
X-App-Secret Server-side API requests only Console → App → Settings

SDK requests (subscriber registration from the browser) only need X-App-Key. The app key is safe to include in frontend JavaScript.

API requests (sending notifications, managing subscribers) require both headers. The X-App-Secret must never be exposed in frontend code — keep it server-side only.

Security notice: Treat X-App-Secret like a password. Rotate it immediately if you suspect it has been exposed. You can regenerate it from the console.

Your First Notification

Here's a more complete notification with all the commonly used fields:

{
  "title": "New Feature: Dark Mode",
  "body": "We've just launched dark mode. Update your settings to try it.",
  "url": "https://yourapp.com/settings/appearance",
  "icon_url": "https://yourapp.com/icons/feature-icon.png",
  "image_url": "https://yourapp.com/images/dark-mode-preview.jpg",
  "target_type": "tags",
  "target_tags": {
    "plan": "pro"
  },
  "target_platforms": ["web", "android"],
  "actions": [
    { "title": "Try Dark Mode", "url": "https://yourapp.com/settings/appearance" },
    { "title": "Maybe Later" }
  ],
  "ttl_seconds": 86400
}

Targeting options

target_type Description Required field
"all" Send to all active subscribers —
"segment" Send to a saved segment target_segment_id
"tags" Send to subscribers matching tags target_tags
"external_ids" Send to specific user IDs target_ids
"subscriber_ids" Send to specific subscriber IDs target_ids

SDK Installation

The OpenPushAPI JavaScript SDK handles Web Push subscription on your website. It registers a Service Worker, manages the subscription lifecycle, and handles all communication with our API.

SDK versioning: All SDK files are served under a version path (/v1/). Breaking changes will be released as /v2/ — existing /v1/ URLs remain stable indefinitely. Current version: v1

Option 1 — CDN (recommended)

<!-- Standard (readable) -->
<script src="https://sdk.openpushapi.com/v1/sdk.js"></script>

<!-- Minified (production, ~30% smaller) -->
<script src="https://sdk.openpushapi.com/v1/sdk.min.js"></script>

Option 2 — Self-hosted

Download the SDK files and host them yourself. You'll also need to serve the Service Worker file (openpush-sw.js) from the root of your domain.

# Download SDK + Service Worker and place in your web root
curl -O https://sdk.openpushapi.com/v1/sdk.min.js
curl -O https://sdk.openpushapi.com/v1/openpush-sw.js

# Serve openpush-sw.js from https://yourdomain.com/openpush-sw.js

SDK Configuration

Pass a configuration object to OpenPush.init():

OpenPush.init({
  appKey: 'your-app-key',          // Required
  autoPrompt: false,                // Show permission prompt automatically
  promptDelay: 3000,                // Delay in ms before auto-prompt (if autoPrompt: true)
  serviceWorkerUrl: '/openpush-sw.js', // Custom Service Worker path
  softPrompt: {                     // Optional: show a custom UI before the browser dialog
    title: 'Get notified',
    body: 'Stay up to date with the latest news.',
    acceptText: 'Yes, notify me',
    denyText: 'No thanks',
    icon: '/icon.png',              // Optional icon URL
  },
  topics: ['news', 'deals'],        // Auto-subscribe to these topics on registration
  onSubscribe: function(subscriberId) {
    console.log('Subscribed:', subscriberId);
  },
  onError: function(err) {
    console.error('Push error:', err);
  }
});
Option Type Default Description
appKey string — Required. Your public app key.
autoPrompt boolean false Trigger the permission flow automatically on page load.
promptDelay number 0 Delay in ms before auto-prompt fires (requires autoPrompt: true).
serviceWorkerUrl string '/openpush-sw.js' Path to the Service Worker file. Must be served from your domain root.
softPrompt object | null null Show a custom UI card before the native browser permission dialog. Pass null to skip and go straight to the browser dialog. Fields: title, body, acceptText, denyText, icon.
topics string[] [] Topic slugs to subscribe to automatically on first registration.
onSubscribe function null Called with subscriberId after successful registration.
onError function null Called with an error object if subscription fails.

SDK Methods

// Manually trigger the permission flow (shows softPrompt if configured)
await OpenPush.prompt();

// Set tags for the current subscriber
await OpenPush.setTags({ plan: 'pro', cohort: 'beta-users' });

// Remove specific tag keys
await OpenPush.removeTags(['cohort', 'source']);

// Set external ID (your own user ID)
await OpenPush.setExternalId('user_12345');

// Subscribe to topics
await OpenPush.subscribeToTopics(['breaking-news', 'deals']);

// Unsubscribe from topics
await OpenPush.unsubscribeFromTopics(['deals']);

// Get current subscribed topics
const topics = await OpenPush.getTopics();

// Check if user is subscribed
const subscribed = OpenPush.isSubscribed(); // synchronous, returns boolean

// Get current subscriber ID
const subscriberId = OpenPush.getSubscriberId(); // synchronous, returns string | null

// Unsubscribe the current user (removes from API + browser)
await OpenPush.unsubscribe();

Flutter SDK — Installation

The openpushapi_flutter package has no mandatory Google/Firebase dependency. iOS uses Apple's native APNs directly. For Android, you provide the FCM token yourself — using whichever push client you prefer.

1. Download and add the SDK

Download the Flutter SDK package from the console and place it in your project:

# pubspec.yaml — add as a local path dependency
dependencies:
  openpushapi_flutter:
    path: ./packages/openpushapi_flutter

2. iOS — enable Push Notifications capability

In Xcode: open Runner → Signing & Capabilities → click + Capability → add Push Notifications. No other iOS setup is needed — the SDK handles APNs registration automatically.

3. Android — add your push provider (optional)

Android push requires FCM (Google). Add it only if you need Android support:

dependencies:
  firebase_core: ^3.0.0
  firebase_messaging: ^15.0.0

Follow the FlutterFire setup guide to add google-services.json to your Android project. This dependency lives in your app, not in the SDK.

Flutter SDK — Initialization

iOS: one line — the SDK requests APNs permission and registers the device token automatically.

Android: call registerToken() after obtaining the FCM token yourself.

import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:openpushapi_flutter/openpushapi_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // iOS: APNs handled automatically — nothing extra needed
  await OpenPushAPI.init(
    config: const OpenPushConfig(
      appKey: 'YOUR_APP_KEY',
      debug: true, // set false in production
    ),
  );

  // Android: provide FCM token yourself
  if (!kIsWeb && Platform.isAndroid) {
    // Uses firebase_messaging — add it to YOUR pubspec, not required by the SDK
    // final token = await FirebaseMessaging.instance.getToken();
    // if (token != null) {
    //   await OpenPushAPI.registerToken(token, platform: 'android');
    // }
    // FirebaseMessaging.instance.onTokenRefresh.listen((t) {
    //   OpenPushAPI.registerToken(t, platform: 'android');
    // });
  }

  runApp(const MyApp());
}

OpenPushConfig options

OptionTypeDefaultDescription
appKeyString—Required. Your public app key.
apiUrlStringhttps://api.openpushapi.com/v1API base URL. Override for testing.
autoRequestPermissionbooltrueRequest notification permission on init (iOS).
vapidKeyString?nullVAPID public key — required for Flutter Web push.
debugboolfalsePrint SDK logs to console.

Flutter SDK — API Reference

MethodDescription
OpenPushAPI.init(config:)Initialize the SDK. On iOS, automatically registers APNs token.
OpenPushAPI.registerToken(token, platform:)Register a push token. Use for Android ('android') and Web ('web').
OpenPushAPI.setExternalId(id)Link subscriber to your own user ID (call after login).
OpenPushAPI.setTags(map)Set or update segmentation tags. Merged server-side.
OpenPushAPI.unsubscribe()Delete subscriber from OpenPushAPI and clear local state.
OpenPushAPI.trackClick(notifId)Track a notification click event.
OpenPushAPI.subscriberIdCurrent subscriber ID, or null if not yet registered.
OpenPushAPI.isInitializedWhether SDK has been successfully initialized.
// Link to your user after login
await OpenPushAPI.setExternalId(currentUser.id);

// Segment by plan and country
await OpenPushAPI.setTags({'plan': 'pro', 'country': 'US'});

// Unsubscribe on logout
await OpenPushAPI.unsubscribe();

// Read the subscriber ID
print(OpenPushAPI.subscriberId);

Flutter Web — Web Push

Web Push uses the standard browser Push API with VAPID — no Firebase required. Get your VAPID public key from the OpenPushAPI console (App → Platform Settings → Web Push), obtain a push subscription in your service worker, and pass the endpoint to registerToken.

// Web — register a VAPID push endpoint
if (kIsWeb) {
  // Obtain the push subscription endpoint from the browser Push API
  // using your service worker, then:
  await OpenPushAPI.registerToken(pushEndpointUrl, platform: 'web');
}

Service worker: place openpush-sw.js at the root of your domain. It handles background message display without any third-party push SDK.

REST API — Authentication

The base URL for all API requests is https://api.openpushapi.com/v1.

Include both headers in every server-side request:

curl https://api.openpushapi.com/v1/subscribers \
  -H "X-App-Key: ak_live_xxxxxxxxxxxxx" \
  -H "X-App-Secret: as_live_xxxxxxxxxxxxx"

Apps API

Manage your applications programmatically.

MethodEndpointDescription
POST/v1/appsCreate a new app
GET/v1/appsList all apps
GET/v1/apps/{id}Get app details
PUT/v1/apps/{id}Update app settings
DELETE/v1/apps/{id}Delete an app

Subscribers API

MethodEndpointDescription
POST/v1/subscribersRegister a subscriber (from SDK)
GET/v1/subscribersList subscribers (filterable)
GET/v1/subscribers/{id}Get subscriber details
PUT/v1/subscribers/{id}Update subscriber (tags, external_id)
DELETE/v1/subscribers/{id}Unsubscribe
POST/v1/subscribers/{id}/tagsAdd tags
DELETE/v1/subscribers/{id}/tagsRemove tags

Filter subscribers

# Filter by platform
GET /v1/subscribers?platform=web

# Filter by active status
GET /v1/subscribers?is_active=1

# Filter by tag
GET /v1/subscribers?tags[plan]=pro

# Pagination
GET /v1/subscribers?limit=50&offset=100

Notifications API

MethodEndpointDescription
POST/v1/notificationsSend a notification
GET/v1/notificationsList notifications
GET/v1/notifications/{id}Get notification + stats
POST/v1/notifications/{id}/cancelCancel a scheduled notification

Full notification payload

{
  "title": "Hi {{first_name}}, Flash Sale — 24 hours only!",
  "body": "50% off all Pro plans. Offer ends tonight.",
  "url": "https://yourapp.com/sale",
  "icon_url": "https://yourapp.com/icons/sale.png",
  "image_url": "https://yourapp.com/images/sale-banner.jpg",
  "badge_url": "https://yourapp.com/icons/badge.png",
  "target_type": "segment",
  "target_segment_id": 42,
  "target_platforms": ["web", "android", "ios"],
  "target_countries": ["US", "GB", "DE"],
  "channels": "push",
  "actions": [
    { "title": "Claim Offer", "url": "https://yourapp.com/sale" },
    { "title": "Remind Me Later" }
  ],
  "ttl_seconds": 43200,
  "send_at": "2026-04-24T09:00:00Z",
  "timezone_delivery": true,
  "send_time_optimized": false,
  "is_ab_test": false
}

Delivery channels

channels decides where a campaign goes: push (default), push_inapp (push and a copy in the subscriber's in-app inbox) or inapp (inbox only — no push is sent). The inbox is what the notification center widget and GET /v1/inapp show. Any other value returns 422.

Segments API

MethodEndpointDescription
POST/v1/segmentsCreate a segment
GET/v1/segmentsList segments
PUT/v1/segments/{id}Update segment rules
DELETE/v1/segments/{id}Delete segment
GET/v1/segments/{id}/countCount matching subscribers
// Create a segment: Pro users in the US on web
{
  "name": "Pro Web US",
  "rules": {
    "platform": "web",
    "country_code": "US",
    "tags.plan": "pro"
  }
}

Stats API

MethodEndpointDescription
GET/v1/stats/overviewOverall subscriber & delivery stats
GET/v1/stats/notifications/{id}Per-notification stats

How metrics are defined

These definitions are the contract behind every number in the console and the Stats API (metrics contract v1, 2026-09).

MetricDefinition
total_recipientsDevices targeted by the campaign after segment, tag, topic, platform, quiet-hours and frequency-cap filters. For A/B tests: the test sample only; the holdout is counted on the winner ("W") notification.
deliveredThe push provider (browser push service, FCM, APNs) accepted the message. This does not prove it was displayed on the device.
failedThe provider rejected the message permanently (invalid token, missing credentials, 4xx other than 429). Transient errors (429, 5xx, network) are retried and are not counted as failed until retries are exhausted.
expiredSubscription no longer exists (410/404) — the subscriber is unsubscribed automatically.
clickedUnique clicks: at most one per subscriber per notification. Clicks that cannot be tied to a subscriber are counted at most once per notification per client address per 24 h.
CTRclicked / delivered (provider-accepted deliveries), not clicked / total_recipients.
ConversionsEvents posted to /v1/conversions. Revenue is only accepted from your server (app secret / AI token) and is reported per currency — amounts in different currencies are never summed.
A/B winnerHigher CTR after a 4 h window with at least 100 provider-accepted deliveries per variant. A two-proportion z-score is recorded (ab_winner_z; |z| ≥ 1.96 ≈ 95 % confidence). The winner is then sent to the holdout.

Tracking API

Use the tracking endpoint to record notification click events from mobile apps and custom integrations. The JavaScript SDK and Flutter SDK call this automatically.

MethodEndpointDescription
POST/v1/track/clickRecord a notification click
curl -X POST https://api.openpushapi.com/v1/track/click \
  -H "Content-Type: application/json" \
  -H "X-App-Key: your-app-key" \
  -d '{
    "notification_id": "12345",
    "subscriber_id": "98765"
  }'

The click is deduplicated per subscriber — calling this endpoint multiple times for the same subscriber and notification only increments the counter once.

Click tracking in mobile apps

// Flutter — track click when user taps notification
FirebaseMessaging.onMessageOpenedApp.listen((message) {
  final notifId = message.data['opn_notification_id'];
  if (notifId != null) OpenPushAPI.trackClick(notifId);
});

Webhooks

Webhooks allow OpenPushAPI to push delivery events to your server in real time. You can use them to sync delivery stats, trigger workflows, or log events.

MethodEndpointDescription
POST/v1/webhooksRegister a webhook endpoint
GET/v1/webhooksList registered webhooks
PUT/v1/webhooks/{id}Update a webhook
DELETE/v1/webhooks/{id}Delete a webhook

Registering a webhook

curl -X POST https://api.openpushapi.com/v1/webhooks \
  -H "Content-Type: application/json" \
  -H "X-App-Key: your-app-key" \
  -H "X-App-Secret: your-app-secret" \
  -d '{
    "url": "https://yourserver.com/hooks/push",
    "events": ["delivered", "clicked", "failed"],
    "is_active": true
  }'

Available events

EventTriggered when
deliveredA push notification is successfully delivered to a device
clickedA subscriber clicks/taps the notification
failedDelivery fails (invalid token, device unreachable, etc.)

Webhook payload

{
  "event": "clicked",
  "notification_id": 12345,
  "subscriber_id": 98765,
  "platform": "android",
  "timestamp": "2026-04-06T10:23:11Z"
}

Webhook requests are signed with an X-OpenPush-Signature header (HMAC-SHA256 of the raw body using your webhook secret). Always verify this signature before processing the payload.

Error Handling

All error responses follow a consistent JSON format:

{
  "error": "Subscriber not found"
}

HTTP status codes

CodeMeaning
200OK — request succeeded
201Created — resource created successfully
204No Content — deleted successfully
400Bad Request — missing or invalid parameters
401Unauthorized — missing or invalid X-App-Key / X-App-Secret
403Forbidden — action not allowed (e.g. plan limit reached)
404Not Found — resource does not exist
422Unprocessable Entity — validation failed
429Too Many Requests — rate limit exceeded
500Internal Server Error — contact support if this persists

Rate limits

API rate limits are returned in response headers:

HeaderDescription
X-RateLimit-LimitMaximum requests per minute
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when the window resets

Quotas & test apps

Server-side calls (X-App-Secret or an AI token) count towards your plan's monthly API call quota, shared by all apps you own. SDK calls from browsers and devices (public app key only) do not count. Every counted response carries:

HeaderMeaning
X-Quota-LimitCalls allowed this month (unlimited on Enterprise, test-app for test apps)
X-Quota-UsedCalls used this month, including this one
X-Quota-ResetUnix time when the quota resets (1st day of next month, UTC)

Over the limit the API answers 429 with "error": "monthly_api_quota_exceeded", the limit, used, reset and an upgrade_url. The per-second rate limit is separate and unchanged.

Test apps

Mark an app as a test app in App settings → General. Test apps are meant for development and staging: their API calls, subscribers and notifications are not counted towards your plan limits or usage, and they carry a Test badge in the console. Don't use a test app for production traffic.

Device token (SDK)

Since Web SDK 1.2.0, registration returns a subscriber_token that proves the browser owns its subscriber record. Every later SDK call about that subscriber (update, tags, topics, in-app inbox, conversions) must send it as X-Subscriber-Token; without it the API returns 403 subscriber_token_required and the SDK registers again automatically. Server-side calls with your app secret don't need it.

Personalization

Use {{variable}} placeholders in the title, body, or url of any notification. Each subscriber receives their own version with the placeholders replaced.

Supported variables

VariableSource
{{first_name}}Subscriber's first_name field
{{last_name}}Subscriber's last_name field
{{external_id}}Your own user ID linked to this subscriber
{{platform}}web, android, or ios
{{country}}2-letter country code (US, DE, …)
{{city}}Subscriber's city
{{language}}Subscriber's browser/device language
{{tags.KEY}}Any tag value — e.g. {{tags.plan}}

Missing or unknown variables are silently replaced with an empty string.

{
  "title": "Hey {{first_name}}, your {{tags.plan}} trial ends soon!",
  "body": "Upgrade today and keep all your {{tags.plan}} features.",
  "url": "https://yourapp.com/upgrade?ref={{external_id}}",
  "target_type": "all"
}

Personalization works on all platforms — Web Push, Android (FCM), and iOS (APNs). The substitution happens in the delivery engine, so your original notification record stores the template, not the resolved text.

A/B Testing

Test two notification variants against each other. Subscribers are randomly split according to the ab_split_percent you specify. The winner (highest CTR) is automatically selected after both variants have been delivered for at least 4 hours and each has reached 100+ deliveries.

Sending an A/B test

Set is_ab_test: true and include the variant B fields in the payload:

{
  "title": "New Feature Available",
  "body": "We just launched something you'll love.",
  "url": "https://yourapp.com/new",
  "target_type": "all",
  "is_ab_test": true,
  "ab_split_percent": 50,
  "title_b": "You're going to love this",
  "body_b": "Check out the latest update — it's live now."
}

A/B test fields

FieldTypeDescription
is_ab_testbooleanEnable A/B split for this notification
ab_split_percentinteger% of the test sample that receives variant A (10–90, default 50)
ab_test_pctinteger% of the audience used as the test sample (5–100, default 20). The remaining subscribers are the holdout and receive the winning variant automatically after 4 hours. 100 = plain split test without a winner send.
title_bstringTitle for variant B
body_bstringBody text for variant B

Variants A and B share the same targeting, URL, icon, schedule, and TTL. Stats for both variants appear side by side in the console notification detail page.

A/B testing requires the Pro plan or higher.

Geo-targeting

Restrict delivery to subscribers in specific countries using the target_countries field. Pass an array of ISO 3166-1 alpha-2 country codes.

{
  "title": "Black Friday — US & Canada only",
  "body": "Exclusive deal available in your region.",
  "url": "https://yourapp.com/black-friday",
  "target_type": "all",
  "target_countries": ["US", "CA"]
}

Geo-targeting can be combined with any target_type — it acts as an additional filter on top of segment, tag, or ID targeting. If target_countries is omitted or empty, the notification is delivered to all countries.

Country codes are matched against the country_code field on the subscriber, which is automatically detected from the subscriber's IP address at registration time.

Smart Send Time

When send_time_optimized: true, the delivery engine sends the notification to each subscriber at their personal optimal hour — the time of day when that subscriber has historically clicked push notifications most often.

{
  "title": "Weekly digest is ready",
  "body": "Here's what happened this week.",
  "target_type": "all",
  "send_time_optimized": true
}

The preferred hour per subscriber is computed nightly from their delivery and click history. Subscribers without enough history receive the notification immediately.

How it works: The engine delivers only to subscribers whose preferred_hour matches the current local hour (±1). Subscribers not yet in their window are skipped in the current run and picked up in subsequent runs. Subscribers without a recorded preferred hour receive the notification immediately.

In-App Notification Bell

The notification center widget adds a bell icon to your web app that shows a dropdown inbox of recent push notifications. It does not require browser permission — it fetches from our API and is ideal for logged-in app experiences.

Installation

<!-- Add a target element where the bell should appear -->
<div id="openpush-center"></div>

<!-- Load the widget -->
<script src="https://sdk.openpushapi.com/v1/notification-center.js"></script>
<script>
  OpenPushCenter.init({
    appKey: 'your-app-key',
    subscriberId: currentUser.opnSubscriberId, // your subscriber's ID
    targetSelector: '#openpush-center',        // where to mount the bell
    maxItems: 20,                              // max notifications to show
  });
</script>

In-App API endpoints

MethodEndpointDescription
GET/v1/inapp?subscriber_id={id}List unread in-app notifications
PUT/v1/inapp/{id}/readMark single notification as read
POST/v1/inapp/read-allMark all as read for a subscriber
# Fetch unread in-app notifications
curl "https://api.openpushapi.com/v1/inapp?subscriber_id=98765&limit=20" \
  -H "X-App-Key: your-app-key"

# Mark all as read
curl -X POST https://api.openpushapi.com/v1/inapp/read-all \
  -H "Content-Type: application/json" \
  -H "X-App-Key: your-app-key" \
  -d '{"subscriber_id": 98765}'

The widget polls for new notifications every 60 seconds automatically. The unread badge count updates in real time when new items arrive.

Automation (Journeys)

Journeys let you build multi-step automated notification sequences triggered by subscriber events. For example: send a welcome push immediately after signup, wait 3 days, then send a follow-up only if the subscriber hasn't clicked anything.

Journey triggers

TriggerWhen it fires
subscriber_createdA new subscriber registers
tag_addedA specific tag is added to a subscriber
segment_enterA subscriber enters a segment
eventA custom event is tracked via the API
manualManually enrolled via the console or API

Step types

TypeDescription
send_pushSend a push notification
waitPause for N minutes / hours / days
conditionBranch based on subscriber field value
ab_splitRandomly split enrollment into A/B paths

Journeys are configured and managed from the console at Automation → Journeys. The engine processes active journey enrollments every minute.

Console only: Journeys are currently managed exclusively from the console builder at Automation → Journeys. REST API support for journeys is coming soon.

Unsubscribe Reason Tracking

When a subscriber opts out, you can capture why they unsubscribed. This data appears in the Analytics section of the console and helps you improve retention.

Passing a reason

Include a reason field when calling the unsubscribe endpoint:

curl -X DELETE https://api.openpushapi.com/v1/subscribers/98765 \
  -H "Content-Type: application/json" \
  -H "X-App-Key: your-app-key" \
  -d '{"reason": "too_many"}'

Valid reason values

ValueMeaning
too_manyReceiving too many notifications
not_relevantNotifications are not relevant
never_signed_upSubscriber didn't intentionally opt in
otherOther / not specified

The Web SDK can also show a built-in reason dialog before unsubscribing:

// Show reason dialog, then unsubscribe
await OpenPush.unsubscribe({ showReasonDialog: true });

// Or unsubscribe with a known reason directly
await OpenPush.unsubscribe({ reason: 'too_many' });

Web Push (VAPID)

Web Push uses the VAPID (Voluntary Application Server Identification) protocol. OpenPushAPI automatically generates VAPID keys for each app — you don't need to manage them yourself.

Browser support: Chrome 50+, Firefox 44+, Edge 17+, Safari 16+ (macOS Ventura), Opera 42+.

The SDK registers a Service Worker in the browser, which handles incoming push messages even when the page is closed. The Service Worker is automatically generated and versioned per your app.

Custom Service Worker integration

If you already have a Service Worker, you can import ours from within it:

// your-service-worker.js
importScripts('https://sdk.openpushapi.com/v1/openpush-sw.js');

// Your existing SW code below...
self.addEventListener('fetch', (event) => {
  // ...
});

Android — Firebase Cloud Messaging (FCM)

To send push notifications to Android apps, you need to configure FCM credentials in your OpenPushAPI app settings.

  1. Create a Firebase project at console.firebase.google.com
  2. Go to Project Settings → Cloud Messaging and copy your Server Key and Project ID
  3. In OpenPushAPI console, navigate to your App → Platform Settings → Android and enter the credentials
  4. In your Android app, use the Firebase SDK to get the FCM registration token and register it via our API
// Android — Register FCM token with OpenPushAPI
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
    val token = task.result

    // Register with OpenPushAPI
    val client = OkHttpClient()
    val body = JSONObject().apply {
        put("platform", "android")
        put("subscription_token", token)
    }.toString()

    val request = Request.Builder()
        .url("https://api.openpushapi.com/v1/subscribers")
        .addHeader("X-App-Key", "your-app-key")
        .post(body.toRequestBody("application/json".toMediaType()))
        .build()

    client.newCall(request).enqueue(...)
}

iOS — Apple Push Notification Service (APNs)

To send push notifications to iOS apps, you need an APNs .p8 key from your Apple Developer account. The key file is uploaded directly in the console — no server-side configuration needed.

Step 1 — Get your APNs key from Apple

  1. Log in to developer.apple.com
  2. Navigate to Certificates, Identifiers & Profiles → Keys
  3. Click + to create a new key, enable Apple Push Notifications service (APNs)
  4. Download the .p8 file — you can only download it once
  5. Note your Key ID (10 characters) and Team ID (found under Membership)

Step 2 — Upload in OpenPushAPI Console

  1. Go to Console → Apps → select your app → Platform Settings → iOS (APNs)
  2. Fill in Key ID, Team ID, and Bundle ID (e.g. com.yourcompany.yourapp)
  3. Click Choose file, select your .p8 file, and click Save APNs Settings
  4. A green checkmark confirms the key is stored. You can replace it at any time by uploading a new file, or remove it with the Remove checkbox.
Note: The .p8 key file is stored securely on the server and is never exposed via the API. It cannot be set through the REST API — only through the Console UI.
// iOS — Register APNs token with OpenPushAPI
func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()

    // Register with OpenPushAPI
    var request = URLRequest(url: URL(string: "https://api.openpushapi.com/v1/subscribers")!)
    request.httpMethod = "POST"
    request.setValue("your-app-key", forHTTPHeaderField: "X-App-Key")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try? JSONSerialization.data(withJSONObject: [
        "platform": "ios",
        "subscription_token": token
    ])

    URLSession.shared.dataTask(with: request).resume()
}

Migrate from OneSignal

Browsers grant notification permission to your site, not to a push provider. Visitors who allowed notifications for OneSignal have already allowed them for you — after the switch the SDK re-subscribes them without showing a new prompt the next time they visit. You can also bring your existing subscriber list along so nobody is lost in between.

1. Create the app and choose your keys

  1. Create an app in the console and add your site's origin to Allowed origins.
  2. If you have your own VAPID key pair (you generated it yourself or your provider let you export it), open App settings → Web Push and paste the private key. The public key is derived and checked automatically. Existing subscriptions made with that key keep working — import them in step 3 and you can send to them straight away.
  3. If you don't have the private key (the usual case with OneSignal-managed keys), keep the keys OpenPushAPI generated. Old subscriptions can't be reused with a new key, but the SDK replaces them automatically on each visitor's next visit (step 2).

2. Swap the SDK and the service worker

Remove the OneSignal script and init code, delete OneSignalSDKWorker.js (or make it an empty file for a while so browsers that still request it get a valid response), and add OpenPushAPI:

<script src="https://sdk.openpushapi.com/v1/sdk.js"></script>
<script>
  OpenPush.init({ appKey: 'YOUR_APP_KEY' });
  // Visitors who already allowed notifications (e.g. via OneSignal) are
  // re-subscribed silently — no permission prompt is shown to them.
  if ('Notification' in window && Notification.permission === 'granted') {
    OpenPush.prompt();
  }
</script>

Serve /openpush-sw.js from your site root (see Installation). Registering it at the same scope replaces the OneSignal worker. Web SDK 1.2.1+ removes a leftover subscription that was made with a different key before subscribing again.

3. Import your subscribers (optional)

In OneSignal, export your subscribers as CSV and upload the file unchanged under Import / Export. The importer recognises OneSignal's column names:

OneSignal columnOpenPushAPI field
identifierpush endpoint
web_p256, web_authsubscription keys
external_user_idexternal_id
language, tagssame
invalid_identifier = truerow is skipped

Imported subscribers keep their external_id and tags, so your segments and server-side targeting by user ID keep working. Duplicates (same endpoint) are skipped and your plan's subscriber limit applies.

Import only if you imported the key pair in step 1. A subscription only works with the key it was created with; without OneSignal's private key the imported web subscribers would just fail on every send. In that case skip the import — each returning visitor is re-subscribed by the SDK (step 2), and your site sets their external ID again when they sign in (OpenPush.setExternalId(userId)).

4. Update your server code

OneSignalOpenPushAPI
POST /notifications with include_aliases.external_idPOST /v1/notifications with "target_type": "external_ids", "target_ids": [...]
included_segments: ["Subscribed Users"]"target_type": "all"
filters on tags"target_type": "tags", "target_tags": {...} or a segment
headings / contents / urltitle / body / url
REST API keyX-App-Key + X-App-Secret headers (server-side only)

5. Check it

Use Test in your browser to confirm the keys and delivery work, then watch the subscriber count grow as visitors return. Keep OneSignal running in parallel until you have switched every place that sends.

Test in your browser

App settings → Web Push → Test in this browser subscribes the browser you are using as a test subscriber of the app and sends it one real push through the delivery engine. It shows each step — permission, subscription, queued, accepted by the push service, shown, clicked — and the push service's error message if something fails (for example a mismatched VAPID key). It needs the send permission on the app and is limited to 20 tests per hour.