Notifications API
z4j's notification surface has two halves. Project channels are operator-owned destinations a project sends to (Slack webhook for #ops, PagerDuty key, SMTP server for invitation emails). User channels and subscriptions are personal: each user picks the triggers they want notifications for and which of their personal channels deliver them, optionally bridged to project channels.
The catalog below maps every shipped route. Channel config is per-type JSON; see SMTP presets for email-specific shape.
Triggers
Section titled “Triggers”Every subscription and default binds to one of these trigger keys:
task.failed schedule.fire.failedtask.succeeded schedule.fire.succeededtask.retried schedule.task_failedagent.offline schedule.misfiredagent.online schedule.circuit_breaker.trippedThe list is enforced server-side; submitting a different value fails validation.
Channel types
Section titled “Channel types”webhook email slack telegram pagerduty discord teamsEach config shape differs. Examples:
| Type | Config example |
|---|---|
webhook |
{"url": "https://...", "headers": {"X-Custom": "v"}, "hmac_secret": "optional"} |
email |
{"smtp_host": "...", "smtp_port": 587, "smtp_user": "...", "smtp_pass": "...", "smtp_tls": true, "from_addr": "...", "to_addrs": [...]} |
slack |
{"webhook_url": "https://hooks.slack.com/services/..."} |
telegram |
{"bot_token": "...", "chat_id": "..."} |
pagerduty |
{"integration_key": "...", "severity_default": "error", "severity_map": {"agent.offline": "critical"}} |
discord |
{"webhook_url": "https://discord.com/api/webhooks/..."} |
teams |
{"webhook_url": "https://..."} |
config is JSON and capped at 16 KiB.
Project channels
Section titled “Project channels”Base path: /api/v1/projects/{slug}/notifications/channels. Any project member
(viewer or higher) can list channels. Creating, importing, updating, deleting,
or testing a channel requires admin on the project.
List responses mask smtp_pass, hmac_secret, bot_token, password, and
integration_key. Other configuration values, including webhook_url, are
returned as configured and are visible to every project member. Treat webhook
URLs as member-visible credentials when assigning project roles.
List channels
Section titled “List channels”GET /api/v1/projects/{slug}/notifications/channelsReturns ChannelPublic[]:
[ { "id": "...", "project_id": "...", "name": "ops slack", "type": "slack", "config": {"webhook_url": "https://hooks.slack.com/services/..."}, "is_active": true, "created_at": "...", "updated_at": "..." }]Create
Section titled “Create”POST /api/v1/projects/{slug}/notifications/channels{ "name": "ops slack", "type": "slack", "config": {"webhook_url": "https://hooks.slack.com/services/..."}, "is_active": true}CSRF-protected. The brain validates the config shape (SSRF guards on webhook URLs, port allow-list on SMTP, etc.) before persisting.
Import a personal channel into the project
Section titled “Import a personal channel into the project”POST /api/v1/projects/{slug}/notifications/channels/import_from_user{ "user_channel_id": "...", "name": "Copy of my Slack" // optional; defaults to "Copy of {original}"}Copies an admin's personal channel into the project so the secret never has to be re-pasted. The source channel must be owned by the calling admin; the brain refuses to import another user's channel.
Update / delete
Section titled “Update / delete”PATCH /api/v1/projects/{slug}/notifications/channels/{channel_id}DELETE /api/v1/projects/{slug}/notifications/channels/{channel_id}CSRF-protected. Patch body is a subset of the create body.
Test a saved channel
Section titled “Test a saved channel”POST /api/v1/projects/{slug}/notifications/channels/{channel_id}/testDispatches a single test payload through the saved channel and returns:
{ "success": true, "status_code": 200, "error": null, "response_body": null}Test unsaved config
Section titled “Test unsaved config”POST /api/v1/projects/{slug}/notifications/channels/testSame response shape; takes the full {type, config} body so admins can verify
credentials before persisting a channel. The channel configuration itself is
not saved, but the dispatch is not side-effect-free: the brain writes a
notification_deliveries row with trigger="test.dispatch" and a corresponding
audit row.
Project default subscriptions
Section titled “Project default subscriptions”Project admins can set defaults so newly-joining members start with sensible subscriptions instead of empty inboxes.
Base path: /api/v1/projects/{slug}/notifications/defaults. All routes require admin.
GET /api/v1/projects/{slug}/notifications/defaultsPOST /api/v1/projects/{slug}/notifications/defaultsPATCH /api/v1/projects/{slug}/notifications/defaults/{default_id}DELETE /api/v1/projects/{slug}/notifications/defaults/{default_id}Default body:
{ "trigger": "task.failed", "filters": {"priority": ["critical", "high"]}, "in_app": true, "project_channel_ids": ["..."]}filters.priority is constrained to critical / high / normal / low.
task_name is a substring match capped at 500 chars. Use
task_name_pattern for an fnmatch glob; it is capped at 200 chars and at five
*/? wildcards and three character classes.
Project delivery log
Section titled “Project delivery log”GET /api/v1/projects/{slug}/notifications/deliveriesDELETE /api/v1/projects/{slug}/notifications/deliveriesRole: admin. Returns the project's recent delivery attempts (per-trigger /
per-channel / per-status). DELETE permanently removes matching delivery rows;
an optional before timestamp limits the deletion. The deletion and a
notifications.deliveries.clear audit row (actor, count, and cutoff) commit
atomically, so clearing deliveries does leave an audit-log record.
User channels
Section titled “User channels”Personal channels owned by the calling user. Same shape as project channels; admin role not required.
Base path: /api/v1/user/channels.
GET /api/v1/user/channelsPOST /api/v1/user/channelsPATCH /api/v1/user/channels/{channel_id}DELETE /api/v1/user/channels/{channel_id}POST /api/v1/user/channels/testPOST /api/v1/user/channels/{channel_id}/testPOST /api/v1/user/channels/import_from_project copies a project channel,
including its secret configuration, into the caller's personal channels. It
therefore requires project admin; viewers and operators may reference a
project channel in subscriptions but may not clone its credentials.
User subscriptions
Section titled “User subscriptions”A subscription binds (trigger, filters) -> (in-app yes/no, user channel ids, project channel ids) for one user.
Base path: /api/v1/user/subscriptions.
GET /api/v1/user/subscriptionsPOST /api/v1/user/subscriptionsPATCH /api/v1/user/subscriptions/{sub_id}DELETE /api/v1/user/subscriptions/{sub_id}Create body:
{ "trigger": "task.failed", "filters": {"priority": ["critical"], "task_name": "billing.*"}, "in_app": true, "user_channel_ids": ["..."], "project_channel_ids": ["..."], "cooldown_seconds": 60}cooldown_seconds (default zero) suppresses every subsequent fire of that
subscription within the window. Cooldown is claimed per subscription, not per
deduplication key, so two different tasks or events can suppress each other.
User delivery log
Section titled “User delivery log”GET /api/v1/user/deliveriesRecent deliveries to the calling user's channels.
In-app inbox
Section titled “In-app inbox”In-app notifications surface in the dashboard's bell icon. Each subscription with in_app=true produces an inbox row when its trigger fires.
GET /api/v1/user/notificationsGET /api/v1/user/notifications/unread-countPOST /api/v1/user/notifications/{notification_id}/readPOST /api/v1/user/notifications/read-allunread-count is what the bell badge polls; mark-as-read flips a single row, read-all flips every row.
Webhook signing
Section titled “Webhook signing”Webhook channels with hmac_secret set send X-Z4J-Timestamp and
X-Z4J-Signature headers. The signature is sha256=<hex>, where the hex value
is HMAC-SHA256 over the UTF-8 bytes of
<X-Z4J-Timestamp>.<exact request body>. A receiver must verify the timestamp
is within its replay window and compare the signature in constant time. This
authenticates the payload but does not encrypt it or replace TLS.
HTTP-only webhooks
Section titled “HTTP-only webhooks”Outbound webhook URLs must be https:// by default. To allow plaintext for an intranet receiver, set Z4J_NOTIFICATIONS_WEBHOOK_ALLOW_HTTP=true. The check fires at both config-validation and dispatch time so an existing http:// URL stops working immediately if the flag is unset later. See production hardening.