Notifications and Reminders
This guide explains how the SpendOne Platform, the AI-based platform for indirect corporate spend, notifies users about events that need their attention, and how it nudges them with reminders and escalations when items go stale. The system covers three categories: event-driven notifications (something happened), buyer-targeted reminders (you still owe an action), and leader escalations (your team is blocked). All three share the same configuration surface, so admins tune cadence, channels and on/off state in one place.
For admins. Notification configs are managed under Administration → Notifications. Each row maps a notification_type (for example notification-approval-request, reminder-pr-approval-requested, escalation-pr-approval-requested) to a category (notification, reminder, escalation), an is_enabled flag, a list of delivery_channels, and, for reminder and escalation rows, a cadence (initial_wait_period_seconds, reminders_count, reminder_interval_seconds, escalation_count, escalation_interval_seconds). Channels are a subset of email and in_app. WebSocket is the transport that delivers the in_app feed live, not a separately configurable channel: putting websocket in delivery_channels does nothing. A few types (notification-group-email, notification-group-in-app, notification-welcome) have a fixed channel that is not user-configurable. Configs cascade: an organizational-unit override (ou_code set) takes precedence over the org-wide default (ou_code null). Removing an OE override falls back to the default, never to silence. A separate scope settings entry (/notification-scope-settings) toggles all escalations on or off for a scope and sets a digest send-time (HH:MM) used by digest-mode notifications. digest_mode on a config is a legacy Novu-era flag, off by default and superseded by the named digests at /notification-digests and /me/notification-digests. End users do not currently have per-user channel preferences; channel choice is admin-controlled per type and per OE.
For integrators. Two endpoint groups govern configuration. All require ADMIN role.
| Method | Endpoint | Purpose |
|---|---|---|
GET | /notification-configs?ou_code=... | List effective configs (org-wide or OE-resolved) |
GET | /notification-configs/{type}?ou_code=... | Read effective config for one type |
PUT | /notification-configs/{type} | Create or update a config row |
DELETE | /notification-configs/{type}/override?ou_code=... | Remove an OE override; revert to org default |
GET | /notification-scope-settings?ou_code=... | Read escalation/digest scope settings |
PUT | /notification-scope-settings | Update scope settings |
A typical update payload for a reminder-type config:
PUT /notification-configs/reminder-pr-approval-requested
Content-Type: application/json
{
"notification_type": "reminder-pr-approval-requested",
"notification_category": "reminder",
"ou_code": null,
"is_enabled": true,
"delivery_channels": ["email", "in_app"],
"initial_wait_period_seconds": 86400,
"reminders_count": 3,
"reminder_interval_seconds": 86400,
"escalation_count": 1,
"escalation_interval_seconds": 259200
}
Reading a config returns the same shape plus code, created_at, updated_at. See the API reference for full schemas including UpsertConfigRequest and NotificationScopeSettings.
Channels
Three channels are first-class:
- email: transactional mail via the platform mailer, with template selection driven by
notification_type. - in_app: persisted notification feed surfaced in the dashboard. Reads are tracked per user.
The
in_appchannel is pushed live to connected dashboard clients overGET /ws. Disconnected clients miss live frames and missed frames are not replayed; on reconnect a client re-reads the persisted feed (GET /notificationsplus the unread count), which is authoritative.
A single notification fans out to every channel listed in delivery_channels for the resolved config. Disabled types and disabled scopes (escalations_enabled=false) suppress the notification entirely: no row is written, no message is sent.
Event types
Categories and the types they cover:
notification:notification-approval-request,notification-request-approved,notification-request-rejected,notification-invoice-attached,notification-new-comment,notification-comment-mention,notification-accountant-task-assigned,notification-group-email,notification-group-in-app,notification-vacation-delegation-scheduled,notification-vacation-delegation-activated,notification-vacation-delegation-ended,notification-welcome.reminder:reminder-draft,reminder-pr-approval-requested,reminder-po-ready-to-order,reminder-po-no-invoice-no-delivery,reminder-po-no-invoice-no-delivery-card,reminder-po-no-invoice-has-delivery,reminder-po-invoice-received-no-delivery,reminder-po-invoice-received-process-not-finished,reminder-at-not-assigned,reminder-at-assigned-not-resolved.escalation: oneescalation-*key for each reminder above. Escalations target the original recipient's manager (or the configured escalation audience) instead of the buyer.
Subscriptions, invoices and other events trigger these through an internal job queue. The reminders consumer handles entity and action pairs (for example purchase_orders.created, approval_plans.pending, accountant_tasks.assigned, invoices.assigned_to_po) and writes scheduled reminder rows. When the buyer or assignee acts, the same consumer cancels the active reminder.
Worked example: approval reminder escalating to manager after 3 days
Configure reminder-pr-approval-requested and escalation-pr-approval-requested org-wide:
{
"notification_type": "reminder-pr-approval-requested",
"notification_category": "reminder",
"is_enabled": true,
"delivery_channels": ["in_app", "email"],
"initial_wait_period_seconds": 86400,
"reminders_count": 2,
"reminder_interval_seconds": 86400,
"escalation_count": 2,
"escalation_interval_seconds": 86400
}
Timeline for a single approval task:
- t = 0: Approval plan transitions to
pending. The reminders consumer schedules areminder-pr-approval-requestedrow for the assigned approver, due in 24 h. Anotification-approval-requestis delivered immediately (separate type, separate config). - t = 24 h: First reminder fires. In-app feed entry is written, email is sent. A second reminder is scheduled for t = 48 h.
- t = 48 h: Second reminder fires. With
reminders_count=2reached, the consumer transitions to escalation phase and schedules anescalation-pr-approval-requestedrow at t = 72 h. - t = 72 h: First escalation fires. The payload builder resolves the approver's manager (or escalation audience) and notifies them via the channels configured for the escalation type. A second escalation is scheduled for t = 96 h.
- t = 96 h: Second escalation fires. With
escalation_count=2reached, the chain ends. No more reminders are sent for this task unless it transitions back to a triggering state. - Approver acts (any time): on
approvals.approvedorapprovals.rejected, the consumer cancels every scheduled and pending row keyed to that task, in both the reminder and the escalation phase, and the chain stops.
If escalations_enabled=false on /notification-scope-settings for the relevant scope, step 4 onwards is skipped: reminders still fire, but the escalation phase is suppressed. If the whole reminder-pr-approval-requested config is disabled, step 2 onwards is skipped: only the immediate notification-approval-request is sent.
Recovery and at-least-once delivery
Reminders are persisted before they fire, so a worker restart does not lose schedules. WebSocket pushes are best-effort and are never replayed: there is no event catch-up endpoint, so integrators must re-read GET /notifications on reconnect rather than expecting a backfill of missed frames. Email and persisted in-app entries are at-least-once, so clients should de-duplicate on the notification id.
For full payload shapes, status enums and pagination, refer to the API reference.