Reference
Notifications
Every project can send email, Slack, WhatsApp and in-app messages through one API, and keeps a durable record of each send. Email works with no configuration at all. The same layer sends the verification and password-reset emails described in Authentication.
Three entities carry it:
| Entity | What it holds |
|---|---|
notification_channels | How to deliver: the transport and sender identity |
notification_templates | What to say: a Mustache subject and body |
notifications | The send itself, and its delivery record |
Channels and templates are configuration you set once per project, with MCP
tools. notifications is data your app or a function writes at runtime over
ordinary REST.
The shortest path
Email needs nothing configured. The platform has its own verified sender and ships the auth templates in code:
POST /<your-slug>/<project>/notifications
Authorization: Bearer <token>
{
"template": "auth.password_changed",
"toUser": { "id": "USR5" },
"data": { "name": "Ada", "appName": "Acme" }
}
The response is the delivery record, with status: "sent" and a sentAt.
How a send resolves
A channel and a template are resolved separately, so a tenant can restyle a message while still sending through the project's identity.
channel tenant row -> project row -> platform email (email only)
template tenant row -> project row -> shipped in code (every channel)
The platform fallback covers email and nothing else. Aaly cannot own your Slack
workspace or your Meta phone number, so a Slack or WhatsApp send returns
NO_CHANNEL_CONFIGURED until you configure a channel, rather than falling back
to something that could not be right.
Channels
One channel per type, set with set_notification_channel.
provider | Channel | Credential |
|---|---|---|
ses | none. Sends as Aaly's own sender only | |
smtp | SMTP password | |
slack | Slack | bot token |
whatsapp_cloud | Meta access token | |
internal | in-app | none. The record is the delivery |
A channel stores the name of a secret, never the credential. The value is
entered by a human through the link set_secret returns, so it never passes
through your agent's context.
Sending from your own domain
Use smtp, not ses. The ses provider sends through Aaly's own account,
which can send only as aaly.io; pointing it at your address is recorded as a
failed send. With smtp, verification, password reset and the password-changed
notice all send from your address, because the auth flows use whatever channel
resolves.
The SMTP username is the login name, which is often not the sender address.
Leave it out and it defaults to fromAddress, which suits Google Workspace and
Microsoft 365. Most email providers need it set: SendGrid uses the literal
apikey, Postmark a server token, Mailgun postmaster@mg.yourdomain.com.
Set SPF and DKIM on your domain or mail from a shared IP lands in spam.
Templates
Mustache, rendered against the data on each send. {{value}} is
HTML-escaped and {{{value}}} is raw. A missing key renders empty rather than
failing the send. Templates are per key and channel type, so one key can carry
a full HTML email and a one-line Slack message.
Anything that must not be stored, such as a one-time code or an invite link,
goes in secretData instead of data. It renders the same way but is never
saved, never returned, and is redacted from request logs.
WhatsApp does not take a body you wrote. Meta only delivers pre-approved templates for a message a business starts, so a WhatsApp template names the approved template and its ordered placeholders instead.
Recipients
Pass exactly one of toUser or to. toUser reads the address from the user
record (email, phone, or the user itself for in-app). to takes a literal
address, for an invitation or a one-off alert. Slack uses to, usually a
channel such as #alerts.
In-app inbox
An in-app notification makes no external call, so the same entity backs an inbox:
GET /notifications?toUser=USR5&read=false # the unread list
GET /notifications?read=false&facets=read # the unread count
PATCH /notifications/NTF7 { "read": true } # mark read
read is the only field a client may change.
Delivery outcomes
A delivery failure is recorded, not raised. You get 201 with
status: "failed" and an error code, so check status rather than the HTTP
code. Review failures with ?status=failed. Pass an idempotencyKey to make a
send safe to retry: a second POST with the same key returns 409 instead of
delivering twice. A JSON array on POST /notifications sends up to 100 in one
request.
Not built yet
- No retry: a transient failure stays
faileduntil you re-send. - No bounce or complaint handling, and no suppression list.
- No opt-out or preference model. Anything marketing-shaped needs consent tracking of your own.
- No scheduling or digests, and no declarative triggers. "Notify when an order is created" is a decision for your app or a function.
- No Microsoft Teams, attachments, per-locale templates or test-send.
- No per-tenant rate limit.
See Limits for the full list.