Notifications API
All endpoints verified working via curl. Document updated only after curl confirmation.
acruxcore sends seven kinds of notification email: two budget alerts (80% used, and exhausted), evaluation run results, three membership emails (joined, removed, role changed), and the weekly usage digest. They are grouped into four coarse categories, and each category can be turned off independently.
A user who belongs to two teams has a separate preference set in each — someone may care about one team's spend and not the other's. Both endpoints below act on your active team, the one the dashboard's team switcher currently shows.
| Category | Covers |
|---|---|
budget_alerts | The 80% warning and the exhausted notice. Owners/admins only. |
connection_health | A provider connection the gateway cannot call at all, even when a fallback answered. Owners/admins only. |
eval_runs | A run you started finishing or failing. |
eval_rules | A live-traffic rule flagging a low score, or being disabled. Owners/admins only. |
membership | Someone joining, being removed, or having their role changed. |
weekly_digest | The Monday usage summary. Owners/admins only. |
No row means enabled. Nothing is written when you sign up, so a category you have never touched is on. A new category ships on for everyone until somebody turns it off.
One exception: the member removed email ignores preferences entirely. Losing access to a team is a security-relevant change, so it is always sent.
GET /api/v1/notifications/preferences
Your effective preferences in the active team. Always returns every category, so a client never has to fill in a missing key.
curl http://localhost:3001/api/v1/notifications/preferences \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"
# Response (status 200)
{
"preferences": {
"budget_alerts": true,
"connection_health": true,
"eval_runs": true,
"eval_rules": true,
"membership": true,
"weekly_digest": true
}
}
Team-scoped API keys are rejected: preferences belong to a person, and a team key has no user identity.
PATCH /api/v1/notifications/preferences
Turns one category on or off. Returns the full effective map, so the response can
replace client state without a second GET.
curl -X PATCH http://localhost:3001/api/v1/notifications/preferences \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"category":"connection_health","enabled":false}'
# Response (status 200)
{
"preferences": {
"budget_alerts": true,
"connection_health": false,
"eval_runs": true,
"eval_rules": true,
"membership": true,
"weekly_digest": true
}
}
Calling it twice for the same category updates the existing row rather than creating a second one.
An unknown category is a 400:
curl -X PATCH http://localhost:3001/api/v1/notifications/preferences \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"category":"nope","enabled":false}'
# Response (status 400)
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid enum value. Expected 'budget_alerts' | 'connection_health' | 'eval_runs' | 'eval_rules' | 'membership' | 'weekly_digest', received 'nope'"
}
}
One-click unsubscribe
Every notification email carries an unsubscribe link, and the weekly digest also sends the RFC 8058 headers Gmail and Yahoo require of bulk senders:
List-Unsubscribe: <https://acruxcore.com/api/v1/email/unsubscribe?token=…>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Both endpoints below are unauthenticated by design — a mail client POSTs with no session, and a login prompt would defeat the point. The signed token in the query string is the entire credential, and it authorizes exactly one action: turning one category off for one user in one team. It never expires, because an unsubscribe link in a months-old email must still work.
A valid token, a tampered token, and a token for a user who has since left the
team all return the same status and body. Distinguishing them would let anyone
enumerate which users belong to which teams. A mail client's POST also always
returns 204 because those clients show the recipient an error on any non-2xx —
and a token we mis-signed is not something they can fix.
The GET renders a confirmation page and writes nothing. A link in an email is
fetched by things that are not the recipient — security scanners, mail gateways,
clients that prefetch — and none of them clicked. The page's button POSTs the
same token, and that is what applies the opt-out. This is the reason RFC 8058
specifies POST for one-click in the first place.
POST /api/v1/email/unsubscribe
The one-click target, and the only endpoint here that writes. Idempotent — posting twice leaves one preference row.
The response depends on who is asking, which the Accept header tells apart: a
mail client (no Accept, or */*) gets 204; a browser submitting the
confirmation form (Accept: text/html) gets the "Unsubscribed" page, because a
204 would leave the page unchanged and look like the button did nothing.
curl -X POST "http://localhost:3001/api/v1/email/unsubscribe?token=Yzc4ZWZhMzgtMjM4Ny00YWE4LWE3Y2ItMjljM2FmZGU0Yjc1OmY1NzY0ZmIxLWU5NDYtNGVhNC05MGRiLTljNmYyNzlkZWNmNjpidWRnZXRfYWxlcnRz.EXAMPLE_SIGNATURE"
# Response (status 204, no body)
A following GET /api/v1/notifications/preferences shows the effect:
{
"preferences": {
"budget_alerts": false,
"connection_health": true,
"eval_runs": true,
"eval_rules": true,
"membership": true,
"weekly_digest": false
}
}
GET /api/v1/email/unsubscribe
For the many clients that render List-Unsubscribe as a plain link. Returns a
minimal, self-contained page — no assets, no tracking, noindex — naming the
category and offering one button. It writes nothing; the button POSTs the
same token back.
curl "http://localhost:3001/api/v1/email/unsubscribe?token=<same token>"
<!-- Response (status 200) -->
<!doctype html><html><head><meta charset="utf-8" />…<title>Unsubscribe · acruxcore</title></head>
<body …><div …><h1 …>Unsubscribe</h1>
<p …>Confirm that you no longer want to receive budget alerts for this team.</p>
<form method="post" action="/api/v1/email/unsubscribe?token=<same token>">
<button type="submit" …>Unsubscribe</button></form>
<p …>Nothing has changed yet. Other notification types are unaffected.</p>
</div></body></html>
A GET with a token that is not ours renders the same page with the generic
wording "these emails", so the endpoint is still not an oracle.
The token is scoped to one category in one team, deliberately. Unsubscribing from digests does not silently stop your budget alerts, and there is no account-wide kill switch here — that belongs on the preferences page, where you are signed in and can see what you are turning off.