StabisoDocs

Webhooks and notifications

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).

Stabiso, the Enterprise System of Record · generated from the code and its guides · release 20b7dd5b0c4d