OCM Pulse — Client API Documentation
This document describes the public HTTP API for OCM Pulse. It covers how to create an API token, how to authenticate requests, how rate limiting works, and every endpoint available to API consumers, with request/response examples.
All paths in this document are relative to your OCM Pulse installation's base URL — e.g.
https://Pulse.example.com. Replacepulse.orangeclickmedia.comin the examples with the actual host.
Table of Contents
- Overview
- Getting Started: Creating an API Token
- Authentication
- Token Abilities (Scopes)
- Rate Limiting
- Pagination
- Errors
- Data Types & Status Enums
- Client API endpoints
- Public (browser) endpoints — used by the JS SDK
Overview
The OCM Pulse API is organized into three tiers:
| Tier | Prefix | Auth |
|---|---|---|
| Client API | /api/v1/client/* | Laravel Sanctum personal access token with ability scopes |
| Public API | /api/v1/public/* | None (origin-checked CORS, browser-only) |
Most integrators will only need the Client API, which is documented in full detail below.
Request/response bodies are JSON. All timestamps are ISO 8601 strings in UTC (e.g. 2026-04-15T10:30:00+00:00).
Getting Started: Creating an API Token
API tokens are issued per user account and are scoped to that user's organization. Only resources inside the user's organization are accessible.
Step 1 — Log into the dashboard
Sign in to the OCM Pulse dashboard with your regular credentials (any user role can mint a token for themselves):
https://pulse.orangeclickmedia.com/login
Step 2 — Open API Tokens settings
From the avatar menu or sidebar, navigate to Settings → API Tokens:
https://pulse.orangeclickmedia.com/settings/api-tokens
Step 3 — Create a new token
- Click Create token.
- Give the token a descriptive name (e.g.
"CI deployment bot","Newsroom automation"). - Select one or more abilities (scopes) that describe what the token can do. See Token Abilities below.
- Click Create.
The raw token string (e.g. 17|aBc1...XyZ) is shown only once. Copy it immediately and store it somewhere safe — you cannot retrieve it later. If lost, revoke the token and create a new one.
Step 4 — Revoke a token
From the same page you can revoke any existing token. Revocation is immediate; in-flight requests using that token will start returning 401 Unauthorized.
Authentication
Every Client API request must include an Authorization header with the token as a bearer credential:
Authorization: Bearer 17|aBc1...XyZ
Accept: application/json
Content type for POST/PATCH/PUT requests must be application/json:
Content-Type: application/json
Missing or invalid tokens return 401 Unauthorized. Tokens that lack the required ability for an endpoint return 403 Forbidden with a body like:
{ "message": "Token does not have the required ability: deliveries:write" }
Token Abilities (Scopes)
Tokens are granted one or more abilities. Endpoints check for the specific ability they need.
| Ability | Label | Description |
|---|---|---|
apps:read | View Apps | View your apps and their configuration |
apps:write | Manage Apps | Create, update, and delete apps |
subscriptions:read | View Subscriptions | View subscriber lists and details |
subscriptions:write | Manage Subscriptions | Manage subscriptions and tags |
deliveries:read | View Deliveries | View delivery history and status |
deliveries:write | Send Notifications | Create, send, and cancel push notifications |
analytics:read | View Analytics | Access analytics and statistics |
Tip: Follow least-privilege. A newsroom automation that only publishes notifications only needs deliveries:write (plus apps:read if it looks up the app by identifier).
Rate Limiting
The Client API is rate limited to 60 requests per minute per token (per-user bucket). Exceeding the limit returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 45
Respect the Retry-After header value (seconds) before retrying.
Pagination
List endpoints (GET .../apps, .../subscriptions, .../deliveries) return Laravel-style paginated collections.
Query parameters:
| Parameter | Type | Default | Max |
|---|---|---|---|
page | integer | 1 | — |
per_page | integer | 25 | 100 |
Response envelope:
{
"data": [ /* array of resource objects */ ],
"links": {
"first": "https://pulse.orangeclickmedia.com/api/v1/client/apps?page=1",
"last": "https://pulse.orangeclickmedia.com/api/v1/client/apps?page=4",
"prev": null,
"next": "https://pulse.orangeclickmedia.com/api/v1/client/apps?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 4,
"path": "https://pulse.orangeclickmedia.com/api/v1/client/apps",
"per_page": 25,
"to": 25,
"total": 92
}
}
Errors
All errors are returned as JSON with an appropriate HTTP status:
| Status | Meaning |
|---|---|
400 | Malformed request |
401 | Missing/invalid token |
403 | Token lacks the required ability or the resource does not belong to your organization |
404 | Resource not found (or not visible to your org) |
422 | Validation error |
429 | Rate limited |
5xx | Server error |
Validation errors use the standard Laravel shape:
{
"message": "The name field is required.",
"errors": {
"name": ["The name field is required."]
}
}
Data Types & Status Enums
Delivery status (status on a delivery):
pending, scheduled, queued, processing, partial, success, failed, cancelled
Subscription status: subscribed, unsubscribed
Client API Endpoints
Base path: https://pulse.orangeclickmedia.com/api/v1/client
App-scoped endpoints identify the app by its identifier (a slug-like string, not its numeric id) — e.g. acme-news. You can list apps to discover their identifiers.
Apps
Manage the apps owned by the authenticated user's organization.
List apps
GET /api/v1/client/apps
Ability: apps:read
Query parameters:
| Param | Type | Notes |
|---|---|---|
per_page | int | 1–100, default 25 |
page | int | — |
Example
curl -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
"https://pulse.orangeclickmedia.com/api/v1/client/apps?per_page=50"
Response 200 OK
{
"data": [
{
"identifier": "acme-news",
"name": "ACME News",
"site_domain": "https://news.acme.com",
"frequency_cap_max_notifications": 5,
"frequency_cap_window_minutes": 1440,
"default_ttl_seconds": 86400,
"default_require_interaction": false,
"default_silent": false,
"default_vibrate_pattern": [200, 100, 200],
"auto_delete_unsubscribed_enabled": true,
"auto_delete_unsubscribed_days": 90,
"default_query_params": [
{ "key": "utm_source", "value": "push" }
],
"subscriber_count": 12483,
"active_subscriber_count": 10210,
"created_at": "2026-01-05T09:12:44+00:00",
"updated_at": "2026-04-10T14:01:00+00:00"
}
],
"links": { "...": "..." },
"meta": { "...": "..." }
}
Get app
GET /api/v1/client/apps/{identifier}
Ability: apps:read
Example
curl -H "Authorization: Bearer $TOKEN" \
"https://pulse.orangeclickmedia.com/api/v1/client/apps/acme-news"
Returns the same resource shape as the list endpoint (single object inside data).
Create app
POST /api/v1/client/apps
Ability: apps:write
Body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ✓ | ≤ 255 chars |
site_domain | url | ✓ | Must not end with / |
frequency_cap_max_notifications | int | – | 1–100; requires frequency_cap_window_minutes |
frequency_cap_window_minutes | int | – | One of 60, 360, 720, 1440, 10080 |
default_ttl_seconds | int | – | 0–2 419 200 (28 days) |
default_require_interaction | bool | – | — |
default_silent | bool | – | — |
default_vibrate_pattern | int[] | – | ≤ 10 values, each 0–10 000 |
auto_delete_unsubscribed_enabled | bool | – | — |
auto_delete_unsubscribed_days | int | – | 1–365; required when auto-delete is enabled |
default_query_params | array | – | ≤ 20 {key, value} objects; keys unique, [\w\-]+ |
organization_id | int | super-admins only | — |
Example
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
"https://pulse.orangeclickmedia.com/api/v1/client/apps" \
-d '{
"name": "ACME News",
"site_domain": "https://news.acme.com",
"frequency_cap_max_notifications": 5,
"frequency_cap_window_minutes": 1440,
"default_ttl_seconds": 86400
}'
Response 201 Created — the created AppResource.
Update app
PUT /api/v1/client/apps/{identifier}
PATCH /api/v1/client/apps/{identifier}
Ability: apps:write
Accepts the same body as create (fields are validated whether present or not — supply a full payload). Returns 200 OK with the updated resource.
Delete app
DELETE /api/v1/client/apps/{identifier}
Ability: apps:write
Returns 204 No Content on success.
Subscriptions
All subscription endpoints are scoped to an app.
List subscriptions
GET /api/v1/client/apps/{identifier}/subscriptions
Ability: subscriptions:read
Query parameters
| Param | Type | Notes |
|---|---|---|
search | string | Free-text search |
status | string | subscribed | unsubscribed |
device_type | string | — |
browser | string | ≤ 255 chars |
country | string | 2-letter ISO code |
tag | array | tag[utm_source]=newsletter matches subs tagged with key utm_source = newsletter |
created_after | date | — |
created_before | date | — |
sort | string | created_at | last_seen_at | last_clicked_at | notifications_sent |
direction | string | asc | desc (default desc) |
per_page | int | 1–100 |
Example
curl -H "Authorization: Bearer $TOKEN" \
"https://pulse.orangeclickmedia.com/api/v1/client/apps/acme-news/subscriptions?status=subscribed&country=US&per_page=50"
Response 200 OK
{
"data": [
{
"id": 10231,
"user_identity": "uuid-abc-123",
"status": "subscribed",
"device_type": "desktop",
"browser_name": "Chrome",
"browser_version": "134",
"os_name": "macOS",
"os_version": "15.3",
"country": "US",
"notifications_sent": 42,
"notifications_clicked": 6,
"click_rate": 14.28,
"tags": { "utm_source": "newsletter", "plan": "pro" },
"created_at": "2026-02-03T11:04:22+00:00",
"last_seen_at": "2026-04-14T08:12:07+00:00",
"last_clicked_at": "2026-04-12T19:40:00+00:00",
"unsubscribed_at": null
}
],
"links": { "...": "..." },
"meta": { "...": "..." }
}
Get subscription
GET /api/v1/client/apps/{identifier}/subscriptions/{subscription_id}
Ability: subscriptions:read
Delete subscription
DELETE /api/v1/client/apps/{identifier}/subscriptions/{subscription_id}
Ability: subscriptions:write
Returns 204 No Content.
Update subscription tags
POST /api/v1/client/apps/{identifier}/subscriptions/{subscription_id}/tags
Ability: subscriptions:write
Body
{
"tags": {
"plan": "pro",
"signup_source": "paywall",
"newsletter": "daily"
}
}
tagsis an object of up to 50 keys. Values are strings (≤ 255 chars).- Semantics are upsert: existing keys are overwritten, new keys added. Tags not listed are left untouched — to remove a tag, omit it from future writes (there is no dedicated delete endpoint).
Example
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://pulse.orangeclickmedia.com/api/v1/client/apps/acme-news/subscriptions/10231/tags" \
-d '{"tags":{"plan":"pro","newsletter":"daily"}}'
Returns the updated SubscriptionResource.
Deliveries
A delivery is a push notification send. Creating one queues it for the Go worker, which dispatches and reports back asynchronously.
List deliveries
GET /api/v1/client/apps/{identifier}/deliveries
Ability: deliveries:read
Query parameters
| Param | Type | Notes |
|---|---|---|
status | string | One of the delivery status enum |
name | string | Filter by name |
title | string | Filter by title |
category | string | Filter by category name |
created_after | date | — |
created_before | date | — |
per_page | int | 1–100 |
Example
curl -H "Authorization: Bearer $TOKEN" \
"https://pulse.orangeclickmedia.com/api/v1/client/apps/acme-news/deliveries?status=success&per_page=50"
Response (single resource shown):
{
"data": [
{
"id": 8821,
"identifier": "dlv_01HZ...",
"name": "Morning briefing",
"title": "Top 5 stories",
"body": "Everything you missed overnight",
"url": "https://news.acme.com/briefing",
"image_url": "https://cdn.acme.com/img/briefing.png",
"status": "success",
"source_type": "api",
"delivery_category_id": 3,
"segment_id": null,
"ignore_frequency_cap": false,
"action_buttons": [
{ "title": "Read", "url": "https://news.acme.com/briefing" }
],
"ttl_seconds": 3600,
"require_interaction": false,
"topic": null,
"scheduled_at": null,
"queued_at": "2026-04-15T06:00:00+00:00",
"started_at": "2026-04-15T06:00:04+00:00",
"completed_at": "2026-04-15T06:01:12+00:00",
"total_subscribers": 10210,
"capped_subscribers": 112,
"success_count": 10050,
"failed_count": 48,
"click_count": 843,
"click_rate": 8.39,
"error_message": null,
"created_at": "2026-04-15T05:59:50+00:00",
"updated_at": "2026-04-15T06:01:12+00:00"
}
],
"links": { "...": "..." },
"meta": { "...": "..." }
}
Get delivery
GET /api/v1/client/apps/{identifier}/deliveries/{delivery_id}
Ability: deliveries:read
Create delivery (send a notification)
POST /api/v1/client/apps/{identifier}/deliveries
Ability: deliveries:write
Body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | ✓ | Internal label, ≤ 255 chars |
title | string | ✓ | Push title shown to users, ≤ 255 chars |
body | string | ✓ | Push body text, ≤ 255 chars |
url | url | ✓ | Click-through URL |
image_url | url | file | – | Large image (PNG/JPG, ≤ 2 MB). URL is fetched and size-checked |
delivery_category_id | int | – | Must exist in delivery_categories |
segment_id | int | – | Target a specific segment; omit to send to everyone |
scheduled_at | datetime | – | ISO 8601, must be in the future. Omit to send immediately |
ignore_frequency_cap | bool | – | Default false |
action_buttons | array | – | ≤ 2 items, each { title (≤48 chars), url } |
ttl_seconds | int | – | 0–2 419 200 (28 days); falls back to app default |
require_interaction | bool | – | Persistent notifications (Chrome/Edge) |
topic | string | – | ≤ 32 chars, alpha_dash; used by the browser to replace older pushes in the same topic |
Example — send immediately
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://pulse.orangeclickmedia.com/api/v1/client/apps/acme-news/deliveries" \
-d '{
"name": "Breaking: market update",
"title": "Markets plunge on earnings",
"body": "S&P500 down 2.4% — follow live coverage.",
"url": "https://news.acme.com/markets/live",
"image_url": "https://cdn.acme.com/img/markets.png",
"action_buttons": [
{ "title": "Live coverage", "url": "https://news.acme.com/markets/live" },
{ "title": "Dismiss", "url": "https://news.acme.com/dismiss" }
],
"ttl_seconds": 1800,
"topic": "markets"
}'
Example — schedule for later, segment-targeted
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://pulse.orangeclickmedia.com/api/v1/client/apps/acme-news/deliveries" \
-d '{
"name": "Weekend digest",
"title": "Your Saturday digest",
"body": "Handpicked for subscribers.",
"url": "https://news.acme.com/digest",
"segment_id": 12,
"scheduled_at": "2026-04-18T13:00:00Z",
"delivery_category_id": 4
}'
Response 201 Created — the created DeliveryResource. The delivery will initially be in pending, scheduled, or queued status — poll the get endpoint to see it move through processing → success / partial / failed.
Cancel delivery
POST /api/v1/client/apps/{identifier}/deliveries/{delivery_id}/cancel
Ability: deliveries:write
Only cancellable while the delivery has not completed. If the delivery is already finished or cannot be cancelled, returns 422 Unprocessable Entity with a human message.
Example
curl -X POST \
-H "Authorization: Bearer $TOKEN" \
"https://pulse.orangeclickmedia.com/api/v1/client/apps/acme-news/deliveries/8821/cancel"
Returns 200 OK with the refreshed DeliveryResource (status becomes cancelled).
Analytics
All analytics endpoints are app-scoped, cached for ~5 minutes on the server, and gated behind analytics:read.
Overview
GET /api/v1/client/apps/{identifier}/analytics/overview
Response
{
"total_subscribers": 12483,
"active_subscribers": 10210,
"total_deliveries": 312,
"total_clicks": 48210,
"avg_click_rate": 7.84,
"subscribers_by_device": { "desktop": 7211, "mobile": 4800, "tablet": 472 },
"subscribers_by_country": [
{ "country": "US", "count": 4012 },
{ "country": "GB", "count": 1980 }
]
}
Subscriber growth
GET /api/v1/client/apps/{identifier}/analytics/subscribers?days=30
days is optional (default 30, max 90).
{
"period_days": 30,
"growth": [
{ "date": "2026-03-17", "new_subscribers": 112 },
{ "date": "2026-03-18", "new_subscribers": 87 }
],
"active_vs_inactive": { "subscribed": 10210, "unsubscribed": 2273 }
}
Delivery performance
GET /api/v1/client/apps/{identifier}/analytics/deliveries?days=30
{
"period_days": 30,
"daily_stats": [
{
"date": "2026-04-15",
"total": 4,
"successful": 4,
"notifications_sent": 40120,
"clicks": 3184
}
],
"status_breakdown": {
"success": 280,
"partial": 18,
"failed": 9,
"cancelled": 5
}
}
Engagement
GET /api/v1/client/apps/{identifier}/analytics/engagement?days=30
{
"period_days": 30,
"engagement_by_day": [
{
"date": "2026-04-15",
"avg_click_rate": 7.84,
"total_clicks": 3184,
"total_sent": 40120
}
],
"top_performing_deliveries": [
{
"id": 8821,
"identifier": "dlv_01HZ...",
"title": "Markets plunge on earnings",
"click_rate": 18.4,
"click_count": 1840,
"success_count": 10000,
"completed_at": "2026-04-15T06:01:12+00:00"
}
]
}
Public Endpoints
These are for the browser-side JS SDK — they're CORS-protected by the app's site_domain and authenticated by origin + X-App-Id, not by bearer token. You usually won't call them directly; they're documented here for completeness.
Base path: https://pulse.orangeclickmedia.com/api/v1/public
Required headers:
X-App-Id: <app.identifier>
Origin: <app.site_domain>
Content-Type: application/json
In production, the Origin header must exactly match the configured site_domain of the app or the request returns 403 Invalid origin domain.
Public: Subscribe
POST /api/v1/public/subscribe
Body
{
"appId": "acme-news",
"userId": "browser-uuid-abc-123",
"subscription": {
"endpoint": "https://fcm.googleapis.com/fcm/send/abcd...",
"expirationTime": null,
"keys": {
"p256dh": "BASE64_P256DH_KEY",
"auth": "BASE64_AUTH_SECRET"
}
},
"externalIds": {
"oid_cookie": "optional-id",
"lotame_id": "optional-id"
},
"categoryIds": [1, 3, 7]
}
Response 200 OK
{ "success": true }
On validation failure this endpoint intentionally returns a generic { "success": false, "message": "Invalid request." } with status 422 — no detailed error leak.
Public: Update Subscription
POST /api/v1/public/update_subscription
Used when the browser renews its push endpoint or reports new external IDs. Body is the same shape as subscribe, but subscription is optional and categoryIds is not accepted.
Public: Update Subscription Tags
POST /api/v1/public/subscription-tags
Body
{
"appId": "acme-news",
"userId": "browser-uuid-abc-123",
"tags": [
{ "key": "newsletter", "value": "daily" },
{ "key": "plan", "value": "pro" }
]
}
Upserts per (subscription, tag_key).
Internal Endpoints
These are used by OCM Pulse's own infrastructure (the Go delivery worker). Integrators normally don't need them.
Quick Start — End-to-end example
Send your first notification from the command line:
# 1. Mint a token in the dashboard with abilities:
# apps:read, deliveries:write, deliveries:read
export TOKEN="17|aBc1...XyZ"
export HOST="https://Pulse.example.com"
# 2. Find the identifier of the app you want to send to
curl -s -H "Authorization: Bearer $TOKEN" \
"$HOST/api/v1/client/apps" | jq '.data[].identifier'
# 3. Send a notification
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"$HOST/api/v1/client/apps/acme-news/deliveries" \
-d '{
"name": "Hello world",
"title": "Hello, subscribers!",
"body": "This is our first API-sent push.",
"url": "https://news.acme.com/welcome"
}' | jq
# 4. Poll status
curl -s -H "Authorization: Bearer $TOKEN" \
"$HOST/api/v1/client/apps/acme-news/deliveries/8821" | jq '.data.status'