Push Notifications
Send push notifications to your users' devices with a single server-to-server API call.
The notifications endpoint dispatches push notifications to devices registered through the device SDK. You target one or more persons, provide the message once, and Voidhash fans it out to every registered device — routing each token through the right provider (FCM or APNs) for you.
This is a privileged, server-to-server surface: it authenticates with a project secret key only. Publishable keys and user API keys are rejected, so clients can never push to arbitrary persons.
Feature flag required
Push notifications are gated by an internal feature flag. If the flag is not enabled for your
organization, sends fail with 409 Api/PushSendNotEnabledError. Contact us to get it enabled
for your project.
Devices become targetable once your app registers them via the SDK push-device endpoints — see push devices on the SDK page.
Send a notification
POST /api/v1/notifications/sendAuthenticate with your project secret key:
curl https://api.voidhash.com/api/v1/notifications/send \
-X POST \
-H "x-secret-key: vh_sk_..." \
-H "Content-Type: application/json" \
-d '{
"distinctIds": ["user-123"],
"title": "Your trial ends tomorrow",
"body": "Upgrade now to keep your perks.",
"data": { "screen": "paywall", "locationSlug": "onboarding" },
"priority": "high",
"idempotencyKey": "trial-reminder-user-123"
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
personIds | string[] | See below | Target persons by canonical person ID |
distinctIds | string[] | See below | Target persons by distinct ID |
title | string | Yes | Notification title (min length 1) |
body | string | Yes | Notification body (min length 1) |
data | object | No | Arbitrary key-value payload delivered to the app |
sound | string | No | Sound to play on delivery |
badge | number | No | Badge count to set on the app icon |
priority | "default" | "high" | No | Delivery priority |
ttl | number | No | Time-to-live in seconds |
channelId | string | No | Android notification channel |
collapseId | string | No | Collapse key — newer sends replace undelivered ones with the same ID |
idempotencyKey | string | No | Collapses retried sends into a single dispatch (per project) |
Targeting
At least one of personIds or distinctIds must be non-empty — the server rejects requests that target nobody. You can mix both in one request.
Targeting is forgiving by design: a distinctId that does not map to a known person is recorded in the response's unresolvedDistinctIds, never treated as a fatal error. The rest of the send proceeds normally.
Retries are safe with an idempotency key
Pass an idempotencyKey when your caller might retry (queue workers, cron jobs, webhook
handlers). Retried sends with the same key collapse into a single dispatch instead of
notifying users twice.
Response
The send is enqueued asynchronously — the response confirms acceptance and returns a tracking ID plus up-front counts, not final delivery results.
{
"pushNotificationSendId": "push_send_...",
"deviceCount": 3,
"status": "pending",
"unresolvedDistinctIds": []
}| Field | Type | Description |
|---|---|---|
pushNotificationSendId | string | Tracking ID for this send (push_send_*) |
deviceCount | number | Number of registered devices resolved for the targeted persons |
status | string | Send status — see below |
unresolvedDistinctIds | string[] | Distinct IDs that did not resolve to a known person |
status is one of:
| Status | Meaning |
|---|---|
pending | Accepted and queued for dispatch |
in_progress | Dispatch to providers is underway |
succeeded | All device dispatches succeeded |
partial_failed | Some device dispatches failed |
failed | All device dispatches failed |
no_recipients | The targeted persons have no registered devices |
A send that resolves zero devices returns immediately with status: "no_recipients" and deviceCount: 0 — check unresolvedDistinctIds to see whether your targeting was off or the persons simply have no devices registered.
Errors
| Status | _tag | Meaning |
|---|---|---|
400 | Api/PushDeviceValidationError | Invalid request payload (e.g. no targets, empty title or body) |
401 | Api/NotAuthenticatedError | Missing or invalid credentials |
403 | Api/ActionForbiddenError | Credential is not a project secret key, or lacks permission |
409 | Api/PushSendNotEnabledError | Push is not enabled for this project — no enabled push provider, or the feature flag is off |
500 | Api/PushSendServiceError | Downstream dispatch infrastructure failed |
See API Overview for the general error shape and status mapping.
Registering devices
This endpoint only delivers to devices your app has registered. Registration, token refresh, and unregistration happen on the device-facing SDK surface (POST /api/v1/sdk/push-devices/register, /refresh, /unregister), authenticated with a publishable key. See push devices on the SDK page for the full contract.