Push notifications
Put a notification on a member's lock screen. Connect holds the VAPID keys, the devices and the member's own preferences - your application supplies a title, a body and a path.
Put a notification on a member's lock screen, without holding a push credential of your own.
What Connect owns, and what you supply
You supply a title, a body, a path and the members to reach. Connect owns everything else: the VAPID keys it signs with, the devices each member has registered across every application, that member's own notification preferences, and the bookkeeping when a browser goes away.
So your application never sees a push endpoint, never stores one, and cannot reach anybody outside the workspace it names — membership is re-checked on every call rather than trusted from the ids you pass.
Two halves, and only one of them is this API
Registering a device happens in the browser, through the SDK's bridge — connect.push.subscribe() in @cosmoops/connect-sdk/client. It has to: a push subscription is produced by pushManager.subscribe() against the member's own browser, and no server can create one on their behalf. The bridge resolves who they are from their session, so your application never handles the subscription either.
Sending is this endpoint, from your backend, with your API key.
It is a best-effort signal, never a channel of record
A send reports how many devices it reached, and that number is legitimately zero: the member may have registered no device, silenced notifications from your apps, or simply denied the browser prompt. Treat a notification as a nudge toward something that is already true in your own data — never as delivery of the fact itself.
The same follows for failure. A send that throws must not fail the thing that triggered it; the reminder is worth less than the record it is about.
Preferences are the member's, not yours
Everything you send goes out under one scenario the member can silence — "Notifications from your apps" — alongside Connect's own. That is deliberate: a notification a person cannot turn off is one they turn off by revoking the device, which costs you every other notification too.
Endpoints
POST /api/v1/application/push
Put a notification on a member's lock screen
Scope push:send · Rate limit 60 requests / min per API key
Notify members of a subscribing workspace. You supply a title, a body, a URL and the ids to reach; Connect owns the VAPID keys it signs with, the devices each member has registered across every application on the platform, that member's own notification preferences, and the bookkeeping when a browser goes away.
So you never hold a push credential, never store an endpoint, and cannot reach anybody who is not an active member of the workspace you name - membership is re-checked on every send rather than trusted from the ids you pass.
delivered: 0 is a success
The member may have registered no device, silenced notifications from their apps, or denied the browser prompt - all ordinary. The response reports the fan-out honestly rather than collapsing it to a boolean, precisely so this is distinguishable from a failure: a caller that retries on zero retries forever against somebody who has opted out.
Treat every send as a nudge toward something already true in your own data, and never let a failure here fail the thing that triggered it - the reminder is worth less than the record it is about.
The URL is checked, not trusted
url must be absolute and on an origin this application registered as an SSO redirect URI. Anything else is a 400: a URL Connect did not check is an open redirect delivered to a lock screen, wearing the notification styling of an app the person trusts. Origin rather than exact match, so a notification can deep-link to a record page that is not a registered callback.
An application with no OAuth client has no registered origin, so every URL is refused rather than every URL allowed.
This is a server-to-server call only
There is no session-token form of it. A push a browser can trigger is a push an attacker can trigger from a compromised page, so the session path is refused outright - see gateWorkspaceScopedRequest.
Registering a device is the other half of push and is not this scope: a subscription is made by pushManager.subscribe() in the member's own browser, so it arrives on a session-authenticated endpoint the SDK bridge calls. Use useConnectPush() for it.
Errors
validation_error (400 - more than 50 recipients, an over-long title or body, or a url outside your registered origins), forbidden (403 - key unknown, missing push:send, or that workspace does not subscribe to your application).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
workspaceId | string | yes | The subscribing workspace whose members you are notifying |
userIds | array | yes | Connect user ids, at most 50 per call. Anyone who is not an active member of workspaceId is dropped silently rather than failing the call - a partner legitimately holds stale ids for somebody who has left |
title | string | yes | |
body | string | yes | |
url | uri | yes | Absolute, and on an origin this application registered as an SSO redirect URI. A path would send the member into Connect's portal to hunt for a record that lives in yours |
Responses
| Status | Meaning |
|---|---|
200 | What the fan-out did |
400 | validation_error |
403 | forbidden |
429 | rate_limited |
Response body
| Field | Type | Required | Notes |
|---|---|---|---|
recipients | integer | yes | Members reached after preference filtering |
silenced | integer | yes | Members dropped because they silenced notifications from their apps |
delivered | integer | yes | Devices the notification actually reached, across every recipient |
failed | integer | yes |
Send your application's own email - a support reply, a sign-in code - on behalf of a subscribing workspace, through Connect's sending identity and its bounce and complaint suppression list.
Task queues
Background jobs delivered back to your own endpoint by Cloud Tasks, at a rate you declare - for work too long for one request, like sending a campaign page by page.