Register (or re-bind) a Web Push subscription
const url = 'https://api.aerandil.com/v1/push-subscriptions';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"app_url":"https://example.com","declarative":false,"endpoint":"https://example.com","expiration_time":"example","keys":{"auth":"example","p256dh":"example"},"platform":"web","token":"example","user_agent":"example"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.aerandil.com/v1/push-subscriptions \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "app_url": "https://example.com", "declarative": false, "endpoint": "https://example.com", "expiration_time": "example", "keys": { "auth": "example", "p256dh": "example" }, "platform": "web", "token": "example", "user_agent": "example" }'Register the calling staff member’s push endpoint — the PushSubscription.toJSON() shape plus the handset’s origin (app_url) and whether it subscribed through window.pushManager (declarative, Declarative Web Push). The (tenant, endpoint) pair is the identity: registering an endpoint again re-binds it to the caller, which is how a shared handset follows whoever signs in. Bound to the token’s Principal::User; a service credential is refused with 403. Accepts Idempotency-Key. The endpoint must be HTTPS (loopback HTTP in dev only).
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”Opaque idempotency token (Stripe-shape). Repeated POSTs with the same key replay the original response within the cache TTL rather than creating a duplicate object.
Request Bodyrequired
Section titled “Request Bodyrequired”A browser’s Web Push subscription (platform web, the default: endpoint, keys and app_url), or the Ithaca store app’s device token (platform apns / fcm: token only). A native platform the deployment does not send to is 404 push_not_configured.
object
web: the handset’s origin, e.g. https://ithaca.example/; the sender composes the notification’s navigate target from it.
Subscribed through window.pushManager (Declarative Web Push, Safari 18.4+); such messages are sent as application/notification+json.
web: the push resource URL (RFC 8030).
Echoed by browsers; ignored.
web: the user agent’s RFC 8291 key material.
object
The 16-byte authentication secret, base64url.
The user agent’s P-256 public key, base64url (65-byte uncompressed point).
Where the push goes: a browser’s push service, Apple (iOS app) or Firebase Cloud Messaging (Android app).
apns: the APNs device token (hex). fcm: the FCM registration token.
Responses
Section titled “Responses”Registered (or re-bound).
object
web only.
The push resource URL (web) or the device token (apns / fcm).
Present and true when the response was replayed for a repeated Idempotency-Key.
The staff member the endpoint currently delivers to.
Example
{ "platform": "web"}Headers
Section titled “Headers”Always no-store.
/v1/push-subscriptions/{id}.
The bearer authenticated but does not hold the scope this operation requires.
Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.
object
object
Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).
Examplegenerated
{ "error": { "code": "example", "instance_id": "example", "message": "example", "parameters": "example" }}The request parsed but a validator refused it. The envelope’s code names which rule.
Structured error wrapper. Every non-2xx response from any Aerandil endpoint serialises to this shape; the HTTP status carries the failure class (4xx/5xx), the body adds a stable code for programmatic dispatch plus a human-readable message.
object
object
Stable, programmatic error code (e.g. unauthorized, forbidden, not_found, invalid_body, invalid_url, precondition_failed, no_secondary_key). Clients dispatch on code, not on the message (which may be localised or contextualised in future versions).
Request-id correlation handle echoed on the request-id response header — support teams correlate without scraping logs.
Human-readable explanation of the failure.
Per-error structured detail. The shape varies by code (e.g. forbidden_action carries {"action": "..."}; precondition_failed carries {"current_version": N}).
Examplegenerated
{ "error": { "code": "example", "instance_id": "example", "message": "example", "parameters": "example" }}