Multi-Channel Notifications¶
Editions: OSS, Cloud, Enterprise. Unless stated otherwise, everything on this page ships in OSS.
After reading this page you can route a workflow's approval requests to Slack, Mattermost, or a webhook, and set per-user email and mobile push preferences so approvers get pinged where they actually are.
Edition notes
All notification channels (email, mobile push, Slack, Mattermost, and webhooks) are available in the open-source edition. The mobile and watch apps are proprietary clients, but they work against self-hosted open-source servers.
Overview¶
When an approval is required, Preloop notifies through two kinds of channels:
- Per-user channels: email and mobile push, controlled by each approver's own preferences
- Per-workflow channels: Slack, Mattermost, and webhooks, configured on the approval workflow via
channel_configs
The web dashboard always shows pending requests in real time regardless of channel configuration.
Email and Push Interplay¶
Delivery is mobile-first:
- Users with push enabled get the push notification immediately; their email is delayed 60 seconds and skipped entirely if the request is resolved first (for example, approved from the phone).
- Users with email only get the email immediately.
Per-User Preferences¶
Each user configures three settings in Settings > Notifications:
| Setting | Values | Description |
|---|---|---|
preferred_channel |
email, mobile_push |
Which channel to prioritize |
enable_email |
on/off | Receive approval emails |
enable_mobile_push |
on/off | Receive push notifications on registered devices |
That is the full set: there are no quiet hours, digests, or per-tool notification overrides.
Email¶
Every approval email includes the tool name, formatted arguments, the agent's reasoning (if provided), and direct Approve / Decline links.
The links carry a secure token, so no login is required: click, decide, done. Sensitive argument values are redacted before they are included (see Redaction).
Subject: Approval Required: pay
Tool Call: pay
Arguments:
- recipient: alice@example.com
- amount: $1,000
[Approve] [Decline]
Mobile Push¶
Available on iPhone, iPad, Apple Watch, and Android.
- Approval request created
- Push sent to all registered devices immediately
- Tap to open the request, approve or decline from the app (or straight from the watch)
Setup¶
- Install the app: App Store or Google Play
- Self-hosted deployment? Enter your server URL before signing in
- Sign in and grant notification permissions
See Mobile Apps for the full setup flow.
Server configuration for iOS push¶
A self-hosted server sends iOS pushes either with its own APNs key or through the hosted push proxy.
With your own key, set APNS_TEAM_ID, APNS_KEY_ID, APNS_AUTH_KEY (or
APNS_AUTH_KEY_PATH) and, if you ship your own build, APNS_BUNDLE_ID.
One .p8 key works for both APNs environments.
Apple issues device tokens from two environments. App Store and TestFlight
builds get production tokens; builds installed from Xcode get sandbox
tokens. Each environment rejects the other's tokens with BadDeviceToken.
The iOS app reports apns_environment (sandbox or production) when it
registers, the server stores it with the token and sends each push to the
matching APNs host, so development and release builds both receive pushes
from the same server.
APNS_USE_SANDBOX=true only changes where tokens from older app versions,
which do not report an environment, are sent. Leave it unset (production)
unless every unlabeled device is a development build.
Without a key, set PUSH_PROXY_URL and PUSH_PROXY_API_KEY; the server
forwards the environment with each iOS push.
Slack and Mattermost¶
Slack and Mattermost notifications post to an incoming webhook configured per approval workflow. The message includes the tool name, redacted arguments, agent reasoning, and a Review button that opens the token-based approval page.
Configure via channel_configs on the workflow, in policy YAML:
approval_workflows:
- name: deploy-review
approvals_required: 1
approver_teams: [sre-team]
channel_configs:
slack:
webhook_url: "https://hooks.slack.com/services/T000/B000/XXXX"
Mattermost is identical: use the mattermost key with your Mattermost incoming webhook URL:
Because each workflow carries its own channel_configs, you can point different workflows at different channels (deployments to #deploys, payments to #finance) by creating a webhook per channel and one workflow per audience.
Webhooks¶
Send approval events to your own service. Configure the webhook key on the workflow:
A workflow has one webhook destination: when several of webhook, slack and mattermost are set, webhook wins, then slack. Custom request headers are not sent; authenticate deliveries by their signature instead.
Every delivery is signed with HMAC-SHA256. Check the X-Preloop-Signature header (t=<timestamp>,v1=<hex>, computed over "<timestamp>.<raw body>") as described in Verifying the signature. The workflow's signing secret is returned once, in webhook_secret, when you create the workflow through the API; later reads show only webhook_secret_hint. For a workflow created from policy YAML, or if you lost the secret, rotate it with POST /api/v1/approval-workflows/{id}/webhook-secret/rotate, which returns the new secret once.
Preloop POSTs a JSON payload:
{
"type": "approval_request",
"request_id": "6f1c0d2e-0000-4000-8000-000000000001",
"tool_name": "deploy",
"summary": "Deploy to production",
"tool_args": {"environment": "production"},
"agent_reasoning": "…",
"status": "pending",
"requested_at": "2026-07-17T20:00:00Z",
"expires_at": "2026-07-17T20:10:00Z",
"actions": {
"review": "https://preloop.example.com/console/approval/6f1c…?token=TOKEN",
"approve": "…same as review…",
"decline": "…same as review…",
"view": "…same as review…"
},
"decision": {
"method": "POST",
"approve_url": "https://preloop.example.com/approval/6f1c…/approve?token=TOKEN",
"decline_url": "https://preloop.example.com/approval/6f1c…/decline?token=TOKEN"
}
}
There are two kinds of URL in the payload:
actionsare pages for a person. Every key opens the same approval page in a browser (GET). They do not decide anything.approve,declineandvieware deprecated aliases ofreview, kept for receivers that read them.decisionis for a system.POSTtoapprove_urlordecline_urlto record the decision. The token in the query string is the only credential: send noAuthorizationheader. The body is optional; send{"comment": "..."}to record why.
summary is the one-line ask to show a person. It is never null: when no model summary is available it is built from the tool name and its redacted arguments.
When the request is decided, the timeline records the channel it came through: api (an API key or a service authenticated with one), cli (the preloop CLI signed in with a user session), token_url (the decision URLs above, or a review link opened without signing in), mobile (the mobile app) or console (a signed-in browser, including the page a Slack or Mattermost Review button opens). A CLI authenticated with an API key is recorded as api, because the credential decides the channel before the CLI's User-Agent is considered.
Sensitive argument values are redacted. The token in these URLs decides the request, so treat the payload as a secret.
Worked example: payload in, decision back¶
Your service receives the payload above, asks whoever must decide, and answers with one call:
# Approve, with a comment
curl -X POST "$(jq -r .decision.approve_url payload.json)" \
-H "Content-Type: application/json" \
-d '{"comment": "Confirmed by the account owner"}'
# Or decline (the body is optional)
curl -X POST "$(jq -r .decision.decline_url payload.json)"
A 200 returns the request with its new status (approved or declined). A 400 means it was already decided or expired; a 404 means the id or token is wrong.
If your service holds a Preloop API key instead, the authenticated routes take the same bodies:
curl -X POST "https://preloop.example.com/api/v1/approval-requests/$REQUEST_ID/approve" \
-H "Authorization: Bearer $PRELOOP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"comment": "Confirmed by the account owner"}'
/approve and /decline need no body. /decide takes {"approved": true|false, "comment": "..."} and requires approved. The CLI does the same with preloop approvals approve <id> --reason "..." and preloop approvals deny <id> --reason "...".
Web Dashboard¶
Always on. The notification bell shows pending requests; clicking one opens the full request with arguments, timeline, and Approve/Decline buttons. Updates arrive over WebSocket: new requests and status changes appear without a refresh.
Testing Your Setup¶
- Configure a test tool with approval required
- Call the tool from your MCP client
- Verify notifications arrive on each configured channel
- Approve from one channel
- Verify the others reflect the decision, and, if you have push enabled, that the delayed email was skipped
Related Pages¶
- Async Approvals: non-blocking approvals with polling
- Teams in Approval Workflows: routing to groups of approvers
- Per-Tool Justification: agent reasoning in notifications
- Native Tool Approvals: approvals for agents' shell and file operations, delivered over the same channels