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:

EntityWhat it holds
notification_channelsHow to deliver: the transport and sender identity
notification_templatesWhat to say: a Mustache subject and body
notificationsThe 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.

providerChannelCredential
sesemailnone. Sends as Aaly's own sender only
smtpemailSMTP password
slackSlackbot token
whatsapp_cloudWhatsAppMeta access token
internalin-appnone. 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 failed until 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.