Notifications, task reminders and webhooks
People are told of what concerns them in the app (the bell in the top bar and the Notifications page) and, if they choose, by e-mail. Other systems are told of events by webhooks. The design is ADR-0090.
Notifications
GET /v1/me/notifications ?unread&limit
POST /v1/me/notifications/read {ids: [...]} or {all: true}
GET /v1/me/notification-settings
PUT /v1/me/notification-settings {email_kinds: [...]}
| Kind | Who is told | When |
|---|---|---|
task_assigned |
the person a task is given to | someone else logs a task for them |
task_due |
the task's person | on the task's due day (its company's day) |
task_overdue |
the task's person | once, when the due day passed and the task is still open |
deal_assigned |
a deal's owner | someone else creates the deal for them or makes them its owner |
deal_won, deal_lost |
a deal's owner | someone else moves their deal to won or lost |
approval_decided |
who asked for an approval | a second person approves or rejects it |
webhook_disabled |
every administrator | a webhook kept failing for a day and was stopped |
A person is never told of what they did themselves. Each notification carries its kind, the values
the screen words it with (params) and a link into the web UI; the screen words it in the person's
language. approvals_waiting in the list counts the approval requests the person may decide now.
Reminders are written when a person opens their notifications, every 15 minutes by the mailbox
sync job (when configured) and by the daily close job: one
task_due and one task_overdue per task at most; a task whose day passed more than 7 days ago
reminds nobody (its list shows it overdue).
E-mail is chosen per kind (none by default). Everything new goes out together in one message to the person's sign-in address, in their language, within about a minute. Nothing is sent from a test copy, without a mail server or to a disabled person.
An API key or an agent has no notifications of its own. One of a person (an AI assistant, an app
connected through the MCP) reads that person's list, with the approvals that person may decide,
so it can answer "what waits for me"; it never marks them read (409 notifications_person_only,
as for the settings). A key the system issued gets an empty list.
Webhooks
Settings > Webhooks (admin scope; people only, never an agent):
GET /v1/webhooks/event-types
GET /v1/webhooks
POST /v1/webhooks {url, description, event_types}
GET /v1/webhooks/{id}
PATCH /v1/webhooks/{id} {url, description, event_types, status}
DELETE /v1/webhooks/{id}
POST /v1/webhooks/{id}/rotate-secret
POST /v1/webhooks/{id}/test
GET /v1/webhooks/{id}/deliveries ?status&limit
POST /v1/webhook-deliveries/{id}/send-again
Events: deal.created, deal.won, deal.lost, lead.created, lead.converted,
quote.accepted, quote.declined, task.created, task.completed, invoice.posted,
credit_note.posted, bill.posted, vendor_credit.posted, payment.received, payment.sent,
subscription.activated, subscription.cancelled, case.created, case.closed;
event_types: ["*"] subscribes to every event, also those added later. A test sends
webhook.test.
The message is a JSON object:
{
"id": "0b4f…",
"type": "invoice.posted",
"timestamp": "2026-10-23T10:12:03.418+00:00",
"organization": "5d1c…",
"entity": "CH01",
"test_copy": false,
"data": {
"id": "…", "kind": "invoice", "number": "INV-2026-0042", "external_number": null,
"party": {"id": "…", "code": "C1001", "name": "Alpen Logistik AG"},
"document_date": "2026-10-23", "due_date": "2026-11-22", "currency": "EUR",
"net_total": "1000.00", "tax_total": "81.00", "total": "1081.00", "journal_entry_id": "…"
}
}
Amounts are strings with the currency's decimals.
Signing follows Standard Webhooks: the headers
webhook-id (the event's id, the same on every attempt), webhook-timestamp (Unix seconds) and
webhook-signature (v1, and the base64 HMAC-SHA256 of id.timestamp.body, keyed with the bytes
of the secret after whsec_). The secret is in the answer of create and rotate only. Any Standard
Webhooks library verifies it, for example in Python:
from standardwebhooks.webhooks import Webhook
Webhook(secret).verify(body, headers)
Delivery: a 2xx within 10 seconds delivers it. Otherwise it is tried again after 1, 5 and 30
minutes, then 2, 5, 10 and 24 hours; after the eighth attempt it is failed and waits in the log to
be sent again. Redirects are not followed. A receiver may get an event twice or out of order, and
keeps the ids it has seen. An endpoint that failed 20 times in a row without a success for a day is
stopped (disabled_reason: failing) and the administrators are notified; what waits for it goes
out when it is started again. Deleting an endpoint deletes its log.
Addresses: https://, no user name or password, no #fragment, and a public address: never
localhost, a private, link-local or internal address, also not through a name that resolves to one.
Test copies start without webhooks and never send those of the organization they copy.
Audit: creating, changing, stopping, starting, rotating, deleting and sending again are audited, never the secret. Endpoints and deliveries are not in the organization export (the secret must not leave, and the log is operational).