Everything uniqush-push does is driven over plain HTTP on the address in
[WebFrontend] addr (default localhost:9898, or a unix socket; see
Authentication). This document is the
reference for that API as of the current version; the README has worked
examples per backend and upgrading.md covers what changed
from 2.7.0.
Requests are form-encoded key/value pairs. POST with a body is the norm
(curl -d), and the query endpoints also accept GET with a query string.
Values must be URL-encoded; curl -d does not do that for you, so use
--data-urlencode for anything containing &, + or %.
Responses are JSON followed by \r\n, except /version (a plain string)
and /nrdp (a plain integer). Every JSON response carries a code, either
UNIQUSH_SUCCESS or one of the error codes listed at the end. HTTP status is
200 apart from /health and a 401; look at
code.
Names. A service groups providers and subscriptions; a subscriber is
your identifier for a user within a service. Both accept a-z, A-Z, 0-9,
-, _, @ and .. (A few other characters are still tolerated for
compatibility; do not rely on them.)
Push service types. pushservicetype is one of apns, fcm, gcm (an
alias for fcm, kept for existing subscriptions), adm, webpush or
unifiedpush (two names for one backend). The type is part of every
subscription's identity, so pick one name per service and keep it.
Off by default. Whoever can reach an API without it can register providers,
decide where a service's pushes go and read back subscriptions, so either keep
it on localhost or turn on one or both of these in [WebFrontend]:
token_file=/etc/uniqush/api-token requires every request to carry
Authorization: Bearer <token>, where the token is the file's contents with
leading and trailing whitespace removed. Generate one with
openssl rand -hex 32. It is read from a file, once at startup, so that it
never appears on a command line. uniqush refuses to start if the file is
missing or empty or the token contains whitespace or a control character,
and warns if the file is accessible to every user on the host. /health is
the one exception, so that a Docker healthcheck or load balancer can poll it
without the token; it then reports only whether redis answers (see
/health). /metrics needs it: give Prometheus the same token
with authorization: {credentials_file: ...} in the scrape config. The
Docker image runs as uid 65534, so a token file bind-mounted from the host
must be readable by that user; compose.yaml has a commented example that
gives it the token as a secret owned by 65534 with mode 0400.
curl -H "Authorization: Bearer $(cat /etc/uniqush/api-token)" http://localhost:9898/psps
A request without the right token is answered 401 Unauthorized with
WWW-Authenticate: Bearer and
{"code":"UNIQUSH_ERROR_UNAUTHORIZED","errorMsg":"This endpoint requires an Authorization: Bearer header with the token in [WebFrontend] token_file"}
and logged at warn with the path and remote address, never the token.
addr=unix:/run/uniqush/uniqush-push.sock listens on a unix socket instead
of TCP, so that file permissions decide who can connect. socket_mode
(octal, default 0660) sets the socket's mode, so run the application
server in uniqush's group or create the socket in a setgid directory of a
group both share. Put the socket in a directory no one else can write to,
such as one systemd creates with RuntimeDirectory=: anyone who can write
there can replace the socket with their own. A socket file left behind by
an unclean exit is replaced on startup, but uniqush refuses to start if
something is still listening on it or the path is not a socket. It is
removed on /stop or SIGTERM. Not supported on Windows.
curl --unix-socket /run/uniqush/uniqush-push.sock http://localhost/version
reaches it.
uniqush warns at startup when it listens on a non-loopback TCP address with no
token_file.
| Path | Purpose |
|---|---|
/addpsp | Add or update a push service provider (credentials for one backend) |
/rmpsp | Remove a push service provider |
/psps | List every provider |
/subscribe | Register a device (delivery point) for a subscriber |
/unsubscribe | Remove a device |
/subscriptions | List a subscriber's devices |
/health | Report whether this instance can serve, as an HTTP status code |
/metrics | Counters and gauges for Prometheus |
/nrdp | Count a subscriber's devices |
/stats | Count each service's subscribers and devices |
/push | Send a notification |
/removed | List the devices uniqush removed because their push service said they were gone |
/previewpush | Show the payload /push would send, without sending it |
/checkdb | Report database inconsistencies (read-only) |
/rebuildserviceset | One-time migration for databases created before 2.2.0 |
/rebuildsubscriberindex | Build the subscriber index; needed once after upgrading an existing database |
/version | Version string |
/stop | Shut down cleanly |
/addpspCreates a push service provider for a service, or updates the existing one of
that type. A provider's identity is a hash of its fixed fields (marked
below); the other fields can be changed freely by calling /addpsp again.
/addpsp replaces the provider wholesale rather than patching it, so an
optional field omitted from a later call is cleared, not kept.
If a provider of the same type already exists for the service and its fixed
fields differ, the call is rejected as a conflict — unless replace=true,
which supersedes the old provider and keeps every subscription. Use that for a
credential change such as moving APNs from a certificate to a .p8 key. Do
not /rmpsp and /addpsp to change credentials: in uniqush 2.7.0 and
earlier that deleted every subscription in the service.
Common parameters:
| Parameter | |
|---|---|
service | Required. Fixed. |
pushservicetype | Required. |
replace | Optional. true to supersede an existing provider whose fixed fields differ. |
apnsExactly one of the two credential forms:
| Parameter | |
|---|---|
cert, key | Paths to the PEM certificate and private key. Fixed. Validated as a key pair at /addpsp. |
authkey, keyid, teamid | Path to the .p8 signing key from the developer portal, its key ID, and your team ID. Not fixed, so the key can be rotated in place. The key must be P-256. |
and:
| Parameter | |
|---|---|
bundleid | The app's bundle identifier, sent as the apns-topic. Effectively required for HTTP/2; cleared when omitted. |
sandbox | true to use Apple's development environment. Otherwise production. Recorded as environment, which is what pushes are routed by when endpoint is unset. |
addr | The retired binary protocol's gateway address. Still accepted, so a registration script that has always sent it keeps selecting the same environment, but no longer stored: a host naming a sandbox gateway, or one of Apple's api.development. hosts, records environment=development, and anything else production. A provider registered before this and not re-registered still carries an addr and no environment, is still routed by that addr, and still shows it in /psps; re-running /addpsp for it is what swaps one field for the other. |
endpoint | Base URL HTTP/2 pushes go to (e.g. https://api.sandbox.push.apple.com), with no path, query or fragment. Omitted, the destination comes from environment. A host outside push.apple.com is refused unless allow_non_apple_endpoints=true is set in the [apns] section of the config. |
cacert | PEM bundle to verify endpoint against instead of the system roots. Read and validated at /addpsp. |
skipverify | true disables certificate verification for a non-Apple endpoint. Refused for Apple's hosts. |
Certificate example:
curl http://localhost:9898/addpsp -d service=myservice -d pushservicetype=apns \
-d cert=/etc/uniqush/apns.crt -d key=/etc/uniqush/apns.key -d bundleid=com.example.app
Token example, replacing the certificate provider above without losing its subscriptions:
curl http://localhost:9898/addpsp -d service=myservice -d pushservicetype=apns \
-d authkey=/etc/uniqush/AuthKey_ABCDE12345.p8 -d keyid=ABCDE12345 -d teamid=TEAM123456 \
-d bundleid=com.example.app -d replace=true
fcm and gcm| Parameter | |
|---|---|
projectid | The Firebase project ID. Required and fixed for gcm. Optional for fcm, where it defaults to the project_id in credentialsfile, and not fixed — this is what lets a provider of either name created by an older uniqush be updated in place. |
credentialsfile | Path to a Firebase service-account JSON file. Read and parsed at /addpsp. The client and project ID made from it are cached by path, so a change to the file at the same path, project_id included, takes effect only after a restart; to rotate it without one, /addpsp with credentialsfile set to a new path, which takes effect immediately. |
curl http://localhost:9898/addpsp -d service=myservice -d pushservicetype=fcm \
-d projectid=my-firebase-project -d credentialsfile=/etc/uniqush/service-account.json
The legacy apikey parameter is gone with the legacy API it authenticated to;
see upgrading.md.
adm| Parameter | |
|---|---|
clientid | From the Amazon developer console. Fixed. |
clientsecret | Likewise. Fixed. |
webpush and unifiedpush| Parameter | |
|---|---|
vapidpublickey | The VAPID public key, base64url. Fixed. |
vapidprivatekey | The matching private key, base64url. Not fixed. |
subscriber | The VAPID contact (RFC 8292): an email address (admin@example.org), a mailto: URI (mailto:admin@example.org) or an https: URL (https://example.org/contact). /addpsp refuses anything else; /rmpsp does not, so a provider added before uniqush checked stays removable. An address, either way it is given, is stored and shown by /psps bare and signed as mailto:admin@example.org, so the two forms are the same provider; a URL is stored, shown and signed as given. Fixed. |
uniqush-push -generate-vapid-keys prints a pair in exactly this form. The
README walks through the whole setup.
/rmpspRemoves a provider. Takes the same parameters as /addpsp for that type,
because the provider is identified by rebuilding it and hashing its fixed
fields: service, pushservicetype, and the fixed credential fields
(cert/key for a certificate APNs provider, projectid for gcm,
clientid/clientsecret for adm, and so on).
Subscriptions are not deleted. Devices whose provider is gone are skipped and
logged at push time, and pushes to them resume when a provider of that type is
added back. If what you want is to change credentials, use /addpsp with
replace=true instead.
/pspsNo parameters. Returns every stored provider, grouped by service, with its fixed and volatile fields merged into one object each:
{"services":{"myservice":[{"service":"myservice","bundleid":"com.example.app","cert":"/etc/uniqush/apns.crt", ...}]},"code":"UNIQUSH_SUCCESS"}
Intended for checking a setup. It returns credential file paths rather than
their contents, and reports every other field as [redacted] — a Web Push
provider's VAPID private key, an ADM provider's clientsecret and its issued
token, and anything a future backend stores that is not on the list of fields
this endpoint may report. A redacted field is still present in the response, so
you can see that a provider carries one. Databases created before uniqush 2.2.0 need
/rebuildserviceset once before this returns anything.
Redaction removes the worst of what a reader gains, not the reason to keep the
port closed or require a token:
/subscriptions returns the device tokens, registration IDs and Web Push
subscriptions of any subscriber whose name is guessed, and /push will send
notifications to them.
/subscribeRegisters a device under a subscriber. The provider it belongs to is derived
from the service and the device's pushservicetype, so the service must
already have a provider of that type. Calling it again with the same device
updates the optional fields.
| Parameter | |
|---|---|
service | Required. |
subscriber | Required. |
pushservicetype | Required. |
devid | Optional. Your identifier for the physical device, for spotting the same device subscribed twice (e.g. after a token change). Stored and returned; uniqush does not interpret it. |
old_devid | Optional. A previous devid, for the same purpose across an identifier change. |
subscribe_date | Optional. Unix timestamp (seconds) of this subscription, for clients that want to keep the newest. Validated as a number. |
app_version | Optional. Version of the app on the device. |
locale | Optional. Stored; not currently returned by /subscriptions. |
Plus the device identifier for its type:
| Type | Parameter | |
|---|---|---|
apns | devtoken | The device token, hex-encoded. An optional bundleid overrides the provider's for this device; see below. |
fcm, gcm | regid | The FCM registration token. An optional account is stored alongside it. |
adm | regid | The ADM registration ID. |
webpush, unifiedpush | endpoint, p256dh, auth | The push subscription: endpoint URL, client public key and auth secret, as produced by the browser or UnifiedPush connector. |
curl http://localhost:9898/subscribe -d service=myservice -d subscriber=alice \
-d pushservicetype=apns -d devtoken=0123456789abcdef...
The response's details names the deliveryPoint (the device's identifier,
<pushservicetype>:<hash>) and the pushServiceProvider it was bound to.
An APNs bundleid on /subscribe is for the case where one certificate serves
several bundle ids — an app and its enterprise or release-testing builds. The
device's own is sent as apns-topic, and the provider's is used for every
device that does not name one, so an existing setup needs no change. Sending an
empty bundleid clears it, putting the device back on the provider's. A push to
a device with no bundle id from either is refused, and only that device is.
/unsubscribeSame parameters as /subscribe (the optional fields are ignored). Removes the
device from the subscriber. The device is identified by rebuilding it from
pushservicetype and its token, so those are required; a delivery_point_id
from /subscriptions is not accepted here.
alldevices=1 instead removes every device the subscriber has in the service,
and then service and subscriber are the only parameters needed — no
pushservicetype, no token. It is meant for an account being deleted, where the
application knows the subscriber is finished and not which devices they had.
Removing nothing is a success, and the response reports how many devices went:
curl http://localhost:9898/unsubscribe -d service=myservice -d subscriber=alice -d alldevices=1
{"type":"UnsubscribeResponse","date":...,"status":0,"details":{"service":"myservice","subscriber":"alice","devicesRemoved":2,"code":"UNIQUSH_SUCCESS"}}
One service per call, and no wildcards in either name: alldevices=1 deletes
every device behind a name, so a pattern is refused rather than expanded. The
removal is not atomic, so a failure reports how many devices it had already
removed and the call can safely be retried.
/subscriptions| Parameter | |
|---|---|
subscriber | Required. |
services | Optional. Comma-separated list of services to look in; default is every service. |
include_delivery_point_ids | Optional. 1 to include each subscription's delivery_point_id, for use with /push and /unsubscribe. |
include_subscription_secrets | Optional. 1 to include the Web Push auth secret, which is withheld by default. See below. |
Returns a JSON array with one object per device, carrying service,
pushservicetype, the device identifier for its type (devtoken, regid,
or endpoint/p256dh), and whichever of devid, old_devid,
subscribe_date and app_version were set:
curl 'http://localhost:9898/subscriptions?subscriber=alice&include_delivery_point_ids=1'
[{"service":"myservice","pushservicetype":"apns","devtoken":"0123...","app_version":"1.2.3","delivery_point_id":"apns:5f2c..."}]
An empty array is returned when the subscriber has nothing, and also on a
database error (which is logged). Databases created before uniqush 2.2.0 need
/rebuildserviceset once.
A Web Push subscription's auth secret is withheld unless
include_subscription_secrets=1 is passed. A devtoken or a regid names a
device and is useless without the provider credentials uniqush holds, but
endpoint, p256dh and auth together are everything an application server
needs to encrypt a push for that browser — with nothing of uniqush's involved,
and no way for the subscriber to tell the difference. Ask for it when you need
to move a subscription to another push server or rebuild one after a restore;
p256dh and endpoint on their own cannot encrypt anything.
/healthNo parameters. Reports whether this instance can serve, as an HTTP status code and a JSON body:
curl -i http://localhost:9898/health
HTTP/1.1 200 OK
{"status":"ok","database":"ok","version":"uniqush-push 2.12.0","code":"UNIQUSH_SUCCESS"}
200 when redis answers, 503 when it does not, with the reason in
database. When a token is required, /health still
answers without one, but leaves out version and reports a failure as
"database":"unavailable", logging the reason instead. It is the only endpoint here whose status code carries the answer,
because that is what a load balancer reads.
Redis is the only thing checked. uniqush can do nothing without it — every push reads the devices to send to, and every subscription change writes one — and nothing else it depends on belongs in a health check: an endpoint that probed Apple or Google would report their outage as this instance being unhealthy, and a load balancer would then remove capacity in response to a failure that removing capacity cannot fix. What each provider is doing belongs in metrics.
Point a readiness probe at this. A liveness probe should not be pointed at anything that depends on another service: during a redis outage it would restart every uniqush repeatedly, which neither fixes redis nor helps the queue drain when it comes back.
/metricsNo parameters. Answers in the Prometheus text format, for a scraper:
| Metric | Labels | |
|---|---|---|
uniqush_push_results_total | service, push_service_type, result | Counter. One per result a push service reports: usually one per device per attempt, but once per push for a failure that applies to the whole provider. success, retry (including the attempt that is then abandoned), device_removed (the push service said the device is gone, and it was unsubscribed), updated (the push service changed a provider's stored credentials, as ADM does when it refreshes its access token) or error. |
uniqush_push_retries_abandoned_total | service, push_service_type | Counter. Notifications dropped for a device after retrying for as long as allowed. |
uniqush_http_requests_total | path | Counter. Requests to each endpoint. |
uniqush_database_up | 1 if redis answered a ping during the scrape, 0 if not. | |
uniqush_subscribers | service | The subscribers count from /stats. |
uniqush_delivery_points | service, push_service_type | The delivery_points counts from /stats. |
uniqush_build_info | version | Always 1. |
go_goroutines | Goroutines in the process. A count that only grows means connections are piling up. |
APNs is reported as success only when Apple accepts the notification. A device
Apple rejects as gone is counted as device_removed, not also as success.
Counters start from zero when uniqush starts. The two subscriber gauges are
read from redis on every scrape, costing what a /stats call does, and are
left out while redis is unreachable or the subscriber index is not built.
Redis gets two seconds: if it has not answered by then, uniqush_database_up
is 0 and the scrape returns without the gauges, so the counters are still
recorded while redis is struggling.
/nrdpservice and subscriber. Returns the number of devices as a bare integer.
/stats| Parameter | |
|---|---|
service | Optional. Comma-separated list of services; default is every service. |
since | Optional. A unix timestamp. Adds subscribers_since, a count of the subscribers whose most recent /subscribe was at or after it. |
Counts what each service holds, from the per-service index rather than by walking the database, so it is a handful of redis commands however large the database is.
curl 'http://localhost:9898/stats?service=myservice&since=1756900000'
{"services":{"myservice":{"subscribers":12034,"subscribers_since":8811,
"delivery_points":{"apns":9000,"fcm":4100}}},
"code":"UNIQUSH_SUCCESS"}
subscribers counts subscribers with at least one device. delivery_points
has an entry per push service type the service has a provider for, so a
configured type with no devices reads 0 rather than being absent.
subscribers_since is omitted when since is not given.
A subscriber's timestamp is the time of their last /subscribe, which is what
an application calls when it launches, so it is a usable last-seen rather than
a record of when they first appeared.
Until /rebuildsubscriberindex has been run on a
database that predates the index, this answers UNIQUSH_ERROR_INDEX_NOT_BUILT
rather than counts. It refuses instead of reporting what the partial index
holds, because an undercount is not distinguishable from a small service.
/pushSends one notification to every device of the named subscribers in a service. Delivery is asynchronous per backend, but the response waits for the first attempt at each device, so it reports what actually happened.
For Web Push and UnifiedPush, request_timeout in the backend's config section
bounds one device's first attempt, including endpoint DNS validation and the
HTTP request. It defaults to 30 seconds and accepts 1-300 seconds. Each backend
push runs at most 16 requests concurrently, so n slow endpoints can take about
ceil(n / 16) * request_timeout, plus processing overhead, before all first
attempts finish. This is not an overall deadline for /push; callers should
allow for the number of devices or split them across smaller requests.
Addressing:
| Parameter | |
|---|---|
service | Required. |
subscriber (or subscribers) | Required. One or more subscribers, comma-separated. * is a wildcard: alice.* matches every subscriber with that prefix, and * alone every subscriber in the service. A wildcard is matched against the service's subscriber index, so it costs the size of the service rather than the size of the database — unless /rebuildsubscriberindex has yet to be run, in which case it falls back to a keyspace scan and logs an error saying so. |
delivery_point_id | Optional. Comma-separated delivery_point_ids, to push to some of a subscriber's devices and not others. A device's id is the deliveryPoint in its /subscribe response, and /subscriptions lists them with include_delivery_point_ids=1. Ids are matched only within the named subscribers: another subscriber's device is never pushed to, and an id that none of the named subscribers has is reported in droppedDetails as UNIQUSH_NOT_SUBSCRIBED. |
Content — every parameter other than the addressing ones becomes part of the payload. These have meaning to uniqush or a backend:
| Parameter | Backends | |
|---|---|---|
msg | all | The message body. For APNs this is aps.alert.body; elsewhere it is a key in the data payload. |
ttl | apns, fcm, adm, webpush, unifiedpush | Seconds the push service may hold the notification for an offline device. 0 means deliver now or never, except on ADM, where it means Amazon's default. When omitted: one hour for APNs and FCM, 12 hours for Web Push and UnifiedPush, and Amazon's default (one week) for ADM. Web Push and UnifiedPush accept 0 to 2147483647 and reject anything else; push services cap it themselves, commonly at four weeks, and Microsoft's WNS rejects 0. The other backends ignore a value that is not a number. |
badge | apns | Badge count. Non-numeric values become 0. |
sound | apns | aps.sound. |
img | apns | aps.alert.launch-image. |
title, title-loc-key, title-loc-args, loc-key, loc-args, action-loc-key | apns | Placed under aps.alert. The two -args values are comma-separated lists (escape a literal comma with \). |
content-available, mutable-content | apns | aps.content-available and aps.mutable-content, as numbers: 1 wakes the app in the background, and 1 runs its notification service extension so it can rewrite the notification before it is shown. A value that is not a number is dropped, because iOS ignores the string "1" where it wants the number 1. |
category, thread-id, target-content-id, interruption-level | apns | Placed in aps as strings: the actions the notification offers, the conversation it groups under, the window it targets, and how far it may interrupt (passive, active, time-sensitive or critical). |
relevance-score | apns | aps.relevance-score, a number from 0 to 1, ranking the notification within its summary. |
msggroup | apns, fcm, adm, webpush, unifiedpush | The collapse key: a newer notification with the same group replaces an undelivered older one. It becomes APNs' apns-collapse-id, which may be at most 64 bytes with no control characters, and Web Push's Topic header, which may be at most 32 characters of A-Z, a-z, 0-9, - and _. Any other value is rejected. |
uniqush.priority | fcm | high or normal, becoming FCM's android.priority. A normal-priority message may be held until the device next leaves Doze; a high-priority one wakes it. Omitted by default, so FCM applies its own rule (high for a notification message, normal for a data-only one). Any other value is rejected. Not read by the other backends: APNs derives its default priority from uniqush.apns_push_type, and Web Push reads uniqush.urgency. |
uniqush.urgency | apns, webpush, unifiedpush | The RFC 8030 Urgency value: very-low, low, normal or high. For Web Push and UnifiedPush it is sent as the Urgency header, defaulting to normal; a push service may hold a less urgent message until the device is charging or on Wi-Fi. For APNs, very-low and low set apns-priority to 5, while normal and high set it to 10. If omitted, APNs uses the priority for uniqush.apns_push_type; background always requires priority 5, so an explicit normal or high urgency is rejected. Any other value is rejected. |
uniqush.apns_push_type | apns | The apns-push-type: alert (default), background, complication, controls, fileprovider, liveactivity, location, mdm, pushtotalk, voip or widgets. Sets the default priority Apple requires for that type; uniqush.urgency may override it except that background always requires priority 5. |
uniqush.apns_voip | apns | 1 is shorthand for uniqush.apns_push_type=voip, and allows the 5120-byte VoIP payload limit instead of 4096. |
uniqush.http2 | apns | Obsolete, and ignored. 0 used to select Apple's binary protocol, which Apple shut down in 2021 and uniqush no longer implements; the push goes over HTTP/2 and the response reports that the parameter does nothing. |
uniqush.payload.apns | apns | A complete APNs payload as JSON, sent verbatim instead of building one from the parameters above (ttl still applies). It must contain an aps dictionary holding either an alert or, for a background push, content-available set to the number 1. The string "1" is accepted too, for compatibility with callers who worked around that check once rejecting the number, but iOS ignores it and the payload is forwarded unchanged. |
uniqush.payload.fcm, uniqush.payload.gcm | fcm, gcm | A JSON object used as the FCM data payload instead of the other parameters. Every value must be a string; nested objects, numbers and booleans are rejected with a message naming the field. |
uniqush.notification.fcm, uniqush.notification.gcm | fcm, gcm | A JSON object sent as the FCM notification block, which the device displays itself, alongside (or instead of) the data payload. |
uniqush.payload.adm | adm | A JSON object used as the ADM data payload instead of the other parameters. |
uniqush.payload.webpush | webpush, unifiedpush | A raw body delivered verbatim (after encryption) instead of the JSON encoding of the other parameters. |
uniqush.perdp.<key> | all | Repeat the parameter to give a list of values; each provider group in the push gets the next value for <key>, in turn. Rarely needed. |
| anything else | all | A user-defined key, passed through to the device: as a key/value in the FCM/ADM data payload, a top-level key beside aps for APNs, or a field in the JSON body for Web Push. |
Names beginning with uniqush. are reserved. A push with no content at all is
rejected with UNIQUSH_ERROR_EMPTY_NOTIFICATION. A parameter with an invalid
value fails the push to that backend's devices without sending anything, and
the failureDetails say why in errorMsg, naming the parameter and value. APNs payloads are capped at
4096 bytes (5120 for VoIP); FCM's own limit is 4096 bytes of data. Such a failure is one entry per backend, not
per device: its subscriber is the request's subscriber list as given, and its deliveryPoint is the
delivery_point_id when exactly one was given, and Unknown otherwise.
curl http://localhost:9898/push -d service=myservice -d subscriber=alice,bob -d msg="Hello" -d badge=1
curl http://localhost:9898/push -d service=myservice -d subscriber=alice \
-d uniqush.notification.fcm='{"title":"Hi","body":"Hello"}' -d uniqush.payload.fcm='{"kind":"greeting"}'
The response counts and lists the outcome per device:
{"type":"Push","date":1756900000,"successCount":1,"failureCount":0,"droppedCount":1,
"successDetails":[{"requestId":"...","service":"myservice","subscriber":"alice","pushServiceProvider":"apns:...","deliveryPoint":"apns:...","messageId":"...","code":"UNIQUSH_SUCCESS"}],
"failureDetails":[],
"droppedDetails":[{"requestId":"...","service":"myservice","from":"127.0.0.1:52210","subscriber":"bob",
"deliveryPoint":"fcm:5f2b...","code":"UNIQUSH_UPDATE_UNSUBSCRIBE",
"removed":{"service":"myservice","subscriber":"bob","pushServiceType":"fcm",
"deliveryPoint":"fcm:5f2b...","devid":"bob-phone",
"regid":"registration-token","reason":"UNIQUSH_UPDATE_UNSUBSCRIBE",
"vendorReason":"SENDER_ID_MISMATCH"}}]}
droppedDetails are devices the push service reported gone, and requested
ids that are not subscribed. An entry with removed is a device uniqush
deleted; removed is the same object /removed lists, without
its id and time. An entry without removed carries an errorMsg and means
uniqush deleted nothing: the removal failed and the device is still subscribed,
or the device had already been removed, or it was re-subscribed after the push
read it. Only removed is a reason to delete a record.
A requested delivery_point_id that none of the named subscribers has is listed
once, as {"requestId":"...","service":"myservice","from":"...", "deliveryPoint":"webpush:5f2b...","code":"UNIQUSH_NOT_SUBSCRIBED"} — typically
a device removed while the caller was not listening. It means "not among the
subscribers this push named", so drop the record only if you named the right
subscriber. A device whose provider has been removed is still subscribed and
fails with UNIQUSH_ERROR_NO_PUSH_SERVICE_PROVIDER instead. If any named
subscriber's devices could not be read (UNIQUSH_ERROR_DATABASE in
failureDetails), no id is reported UNIQUSH_NOT_SUBSCRIBED, since the missing
ones may be that subscriber's. A subscriber with none of the requested ids also
gets UNIQUSH_ERROR_NO_DEVICE in failureDetails, as before.
modifiedDp: true on a success means the stored device was updated in passing
(an FCM canonical-token change).
failureDetails entries carry an errorMsg. A transient failure is retried
in the background with a backoff, honouring the delay the push service asked
for up to 30 minutes; the response reports the first attempt only, and a
retry that is finally abandoned is logged with UNIQUSH_ERROR_FAILED_RETRY.
Transient connection failures such as timeouts and connection resets use the
same retry path. DNS not-found errors, endpoint-policy rejections, and TLS
certificate verification failures remain permanent failures.
A retry after a timeout or dropped connection can deliver a notification
twice if the push service accepted the first attempt.
A device being retried is in failureDetails, and counted in failureCount,
with the code UNIQUSH_PUSH_RETRYING and the push service's answer in
errorMsg:
{"requestId":"...","service":"myservice","from":"127.0.0.1:52210","subscriber":"bob",
"pushServiceProvider":"webpush:...","deliveryPoint":"webpush:5f2b...",
"code":"UNIQUSH_PUSH_RETRYING","errorMsg":"push server returned HTTP 503"}
The retry's outcome is not reported to the caller, except that a device it finds gone is listed in
/removed.
For APNs, /push waits for Apple's answers, with each network attempt bounded by
the configured request_timeout. UNIQUSH_SUCCESS means Apple accepted the
notification, not that it reached the device. A device Apple rejects as gone
(Unregistered, BadDeviceToken, DeviceTokenNotForTopic or ExpiredToken)
is reported in droppedDetails, with removed when its subscription was deleted.
A failed or retrying device does not hide the results of other devices in the same push.
Not every removal can be in the response: a retry runs after it. /removed
has every removal, including the ones that are also in a response.
/removed| Parameter | |
|---|---|
service | Required. |
since | Optional. The next of a previous call. Without it, the list starts at the oldest removal kept. A malformed cursor, or one past the newest removal of the service (which this service's /removed cannot have returned), is refused with UNIQUSH_ERROR_GENERIC. |
limit | Optional. At most this many entries; default 1000, and larger values are capped at 1000. Anything but a positive integer is refused with UNIQUSH_ERROR_GENERIC. |
Lists, oldest first, the devices uniqush unsubscribed from a service because
their push service said they were gone, after the cursor since. The first
call has no since:
curl 'http://localhost:9898/removed?service=myservice'
{"code":"UNIQUSH_SUCCESS",
"removed":[{"id":"1756900012345-0","time":1756900012,"service":"myservice","subscriber":"bob",
"pushServiceType":"fcm","deliveryPoint":"fcm:5f2b...","devid":"bob-phone",
"regid":"registration-token","reason":"UNIQUSH_UPDATE_UNSUBSCRIBE",
"vendorReason":"SENDER_ID_MISMATCH"}],
"next":"1756900012345-0","truncated":false}
and each later one passes the next it got back:
curl 'http://localhost:9898/removed?service=myservice&since=1756900012345-0'
{"code":"UNIQUSH_SUCCESS","removed":[],"next":"1756900012345-0","truncated":false}
Each entry has:
| Field | |
|---|---|
id | This entry's cursor. Opaque; compare for equality only. |
time | Unix time the device was removed. |
service, subscriber | Whose device it was. |
pushServiceType | apns, fcm, gcm, adm, webpush or unifiedpush. |
deliveryPoint | The device's delivery_point_id in /subscriptions. |
devid | The devid given at /subscribe. Omitted if none was. |
endpoint | webpush and unifiedpush: the endpoint the push service rejected. |
devtoken | apns: the device token. |
regid | fcm, gcm and adm: the registration token. |
reason | The droppedDetails code: UNIQUSH_UPDATE_UNSUBSCRIBE or UNIQUSH_REMOVE_INVALID_REG. |
vendorReason | Optional push service code: an APNs reason such as BadDeviceToken or Unregistered, or an FCM code such as UNREGISTERED or SENDER_ID_MISMATCH. Omitted when no vendor code is known, including Web Push / UnifiedPush responses, APNs responses without a reason, and entries recorded by older versions. |
vendorReason adds diagnostic detail; it does not change which responses remove
a device or uniqush's reason code. In particular, SENDER_ID_MISMATCH means the
FCM token belongs to a different sender, not that the app was uninstalled; it
still removes the subscription as before. Free-form response messages, bodies
and headers are not copied into vendorReason.
endpoint, devtoken and regid are the address as it was given to
/subscribe, so a caller that keys devices by devid can tell the removed
registration from a newer one the same device has made since. The Web Push
auth secret and p256dh key are never included.
Pass next as since on the following call; when there is nothing new, next
is the since you sent. Each service keeps its newest 100000 removals, so poll
often enough that fewer than that happen in between. truncated: true means
removals after since have already been discarded: the entries returned are
still correct, but some before them are missing, so reconcile against
/subscriptions before carrying on from next. A first call without since
is truncated whenever anything has ever been discarded.
APNs removals are also reported in /push when they occur on the first attempt.
Those found by a later retry appear only here.
Each removal is listed once. A device is removed only if its stored record is still the one the push read, so a device that re-subscribed in the meantime is neither removed nor listed.
Needs Redis 5.0 or newer, as a single server or a primary with replicas; like the rest of uniqush, not twemproxy or Redis Cluster.
/previewpushTakes pushservicetype plus any of the content parameters of /push, and
returns the payload that would be sent, without sending it or touching the
database:
curl http://localhost:9898/previewpush -d pushservicetype=apns -d msg=Hello -d badge=3
{"code":"UNIQUSH_SUCCESS","payload":{"aps":{"alert":{"body":"Hello"},"badge":3}}}
For Web Push the preview is the plaintext before encryption.
/checkdbNo parameters. Scans the whole database and reports what does not add up,
changing nothing. It walks the keyspace with SCAN and takes no lock, so it
can be run against a live server. Run it before upgrading a database created
before uniqush 2.6.0; see delivery-point-rebinding.md.
{"services":3,"push_service_providers":4,"delivery_points":1200,"delivery_point_bindings":1200,
"subscribers":800,"counts":{"leaked_counter":2},
"problems":[{"kind":"leaked_counter","subject":"apns:5f2c...","detail":"..."}]}
counts is complete; problems holds at most 50 examples of each kind. The
kinds: duplicate_provider (a service with two providers of one type — the
one case where the provider for a device is ambiguous), dangling_provider
(a service's set names a provider that no longer exists), orphaned_provider
(a provider record no service refers to), stale_binding (a stored binding
pointing at a missing provider), binding_disagrees (a binding that differs
from the derived provider), orphaned_delivery_point (a subscriber's set names
a device with no record; heals on the next read), unreferenced_delivery_point
(a device record its own subscriber's set does not name, which an interrupted
/subscribe leaves behind), leaked_counter (a delivery.point.counter: key,
which nothing has written since subscribing became a redis script),
index_not_built (the subscriber index has never been rebuilt over this
database — run /rebuildsubscriberindex),
missing_index_entry (a subscriber or device the index does not know about)
and stale_index_entry (an index entry with nothing behind it, which makes
/stats overcount). A summary line is logged at warning level
whenever anything is found.
/rebuildservicesetNo parameters. Builds the index of service names that /subscriptions (with no
services) and /psps need. Only required once, on a database created before
uniqush 2.2.0. Returns {"code":"UNIQUSH_SUCCESS"} or an error.
/rebuildsubscriberindexNo parameters. Builds the per-service subscriber index that wildcard /push
and /stats read, from the subscriber sets, which are the source of
truth. Only required once, on a database that predates the index; a database
created by this release or later is marked as indexed when it is first opened.
Idempotent, and safe to run against a live server: each service's index is
built under a name of its own and renamed over the live one, so a concurrent
push sees the old index or the new one and never a partial one. It takes no
lock. Run /checkdb afterwards to confirm; a subscription made
during the run can, rarely, be missed, and /checkdb names it.
Returns {"code":"UNIQUSH_SUCCESS"} or an error. Until it has been run,
wildcard pushes still reach the same subscribers, over a keyspace scan that is
slow on a large database and logs an error on every use, and /stats refuses
to answer.
/versionReturns the version string, e.g. uniqush-push 2.7.0. The same as
uniqush-push -version.
/stopWaits for in-flight requests, flushes, and exits the process. Prefer this to a signal so the cache reaches the database.
Every response code is one of:
| Code | Meaning |
|---|---|
UNIQUSH_SUCCESS | Done. |
UNIQUSH_REMOVE_INVALID_REG | (push, dropped) The push service said the device is gone; it has been unsubscribed. |
UNIQUSH_NOT_SUBSCRIBED | (push, dropped) A delivery_point_id that none of the subscribers has. |
UNIQUSH_PUSH_RETRYING | (push, failure) The first attempt failed and is being retried in the background; errorMsg says why. |
UNIQUSH_UPDATE_UNSUBSCRIBE | (push, dropped) The push service said the device is gone (unregistered, or its token or endpoint expired); it has been unsubscribed. |
UNIQUSH_ERROR_GENERIC | Something failed; see errorMsg. Provider credential or configuration errors reported by the push service land here with a message naming what to check. |
UNIQUSH_ERROR_EMPTY_NOTIFICATION | /push with no content. |
UNIQUSH_ERROR_DATABASE | Redis error. |
UNIQUSH_ERROR_FAILED_RETRY | (logged, not returned) A retried push was abandoned. |
UNIQUSH_ERROR_BUILD_PUSH_SERVICE_PROVIDER | /addpsp or /rmpsp parameters were invalid; errorMsg says which. |
UNIQUSH_ERROR_BUILD_DELIVERY_POINT | /subscribe or /unsubscribe parameters were invalid. |
UNIQUSH_ERROR_BAD_DELIVERY_POINT | The push service rejected the device (bad token). |
UNIQUSH_ERROR_UPDATE_PUSH_SERVICE_PROVIDER, UNIQUSH_ERROR_UPDATE_DELIVERY_POINT | A database update after a push failed. |
UNIQUSH_ERROR_CANNOT_GET_SERVICE, UNIQUSH_ERROR_CANNOT_GET_SUBSCRIBER, UNIQUSH_ERROR_CANNOT_GET_DELIVERY_POINT_ID | A required addressing parameter was missing or malformed. |
UNIQUSH_ERROR_NO_SUBSCRIBER, UNIQUSH_ERROR_NO_DEVICE, UNIQUSH_ERROR_NO_DELIVERY_POINT, UNIQUSH_ERROR_NO_PUSH_SERVICE_PROVIDER | Nothing to push to: the subscriber, device or provider does not exist. |
UNIQUSH_ERROR_NO_PUSH_SERVICE_TYPE | /previewpush without a pushservicetype. |
UNIQUSH_ERROR_INDEX_NOT_BUILT | /stats on a database whose subscriber index has not been built. Run /rebuildsubscriberindex. |
UNIQUSH_ERROR_UNAUTHORIZED | A missing or wrong bearer token, with HTTP status 401. See Authentication. |
Simple responses (/addpsp, /rmpsp, /subscribe, /unsubscribe) wrap the
details with a numeric status, 0 for success and 1 for failure:
{"type":"Subscribe","date":1756900000,"status":0,"details":{"from":"127.0.0.1:52210","service":"myservice","subscriber":"alice","pushServiceProvider":"apns:...","deliveryPoint":"apns:...","code":"UNIQUSH_SUCCESS"}}