HTTP API
All routes are tenant-scoped via the four CPET headers (X-Customer, X-Product, X-Env, X-Tenant) and are rate-limited per class (see Configuration).
Auth: send routes accept a service API key or a Bearer token (API key is tried first, falling through to JWT). In-app, list, and template routes require a user Bearer token.
:type is the channel — email, sms, push, inapp, webhook, whatsapp, …
| Method | Path | Purpose |
|---|---|---|
| POST | /notify/:type/sync | Deliver inline, return the provider result |
| POST | /notify/:type/async | Queue and return an outbox id |
| POST | /notify/multi | Resolve a user’s preferred channel via IAM, then send |
| POST | /notify/list | Fan out to a stored notification list |
Request body (all send routes):
{ "templatename": "password_reset", "idempotency_key": "optional-caller-supplied-key", "sendat": "2026-07-24T09:00:00Z", "user": "realm:user-ulid", "list": "list-ulid"}- Template reference — exactly one of
templatename,templatemap(inline definition), ortemplatepath(loaded through meta). None → template-not-found error. sendatapplies to async only.userapplies to/notify/multi;listto/notify/list.idempotency_key— omit and one is generated. Supplying your own makes retries safe.
provider_context recipient shapes (email shown; other channels analogous): a single {name, email} map, an array of such maps, a bare string, or an array of strings. cc, bcc, and reply_to attach to the first recipient.
Responses:
// sync → 200{ "message": "notification sent", "status": "sent", "channel": "email", "provider": "sendgrid", "receipt": "…" }
// async → 200{ "message": "notification queued", "outbox_id": "01K…" }
// multi → 200{ "message": "notification dispatched", "status": "sent", "channel": "email", "provider": "sendgrid" }
// list → 200{ "message": "list notifications dispatched", "dispatched": 42, "failed": 3 }status is one of sent, failed, suppressed, rate_limited, deferred, dead.
Deprecated aliases POST /notification/:type/sync and POST /notification/:type/async still resolve — prefer /notify/….
In-app inbox
Section titled “In-app inbox”Scoped to the caller’s own user id from the JWT — one user cannot read another’s inbox.
| Method | Path | Purpose |
|---|---|---|
| PATCH | /inapp/user | Paginated unseen list. Query: page (default 1), pagesize (default 10). Body may carry filters. Ordered newest-first |
| GET | /inapp/user/new | Unseen count → {"data":{"inappnotification":{"new": 3}}} |
| PUT | /inapp/user | Mark seen — body {"id": ["ulid", …]} |
Notification lists
Section titled “Notification lists”| Method | Path | Purpose |
|---|---|---|
| POST | /notificationlist | Create |
| GET | /notificationlist | List all, or one by ?id= / ?name= |
| PUT | /notificationlist | Update |
| DELETE | /notificationlist/:id | Delete |
Response envelope: {"data": {"notification_list": …}}.
Templates (read-only)
Section titled “Templates (read-only)”| Method | Path | Purpose |
|---|---|---|
| GET | /template?name=<n> | One template → {"data":{"template": {…}}} |
| GET | /template | The whole cached template map |
Templates are pushed from the product repo through meta — there is no create/update API.
Platform
Section titled “Platform”| Method | Path | Purpose |
|---|---|---|
| GET | /health · GET /ready | Liveness / readiness (unauthenticated) |
| — | /v2/admin/data/*, /v2/superadmin/tenant | Data-admin plane (plans, seeds, retention, locks). apply, retention, and locks require superadmin |
There is no generic /rest entity surface on this service.
Error codes
Section titled “Error codes”| Code | Meaning |
|---|---|
notify-template-not-found | No templatename / templatemap / templatepath, or the name doesn’t resolve |
notify-route-all-providers-failed | Every provider for the channel failed; error data carries providers and last_error |
notify-recipient-unresolved | The user has no primary channel preference, or no contact value for it |
notify-channel-unknown | The requested channel isn’t mapped to a provider |