Skip to main content

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.

MethodEndpointPurpose
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-settingsUpdate 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_app channel is pushed live to connected dashboard clients over GET /ws. Disconnected clients miss live frames and missed frames are not replayed; on reconnect a client re-reads the persisted feed (GET /notifications plus 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: one escalation-* 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:

  1. t = 0: Approval plan transitions to pending. The reminders consumer schedules a reminder-pr-approval-requested row for the assigned approver, due in 24 h. A notification-approval-request is delivered immediately (separate type, separate config).
  2. t = 24 h: First reminder fires. In-app feed entry is written, email is sent. A second reminder is scheduled for t = 48 h.
  3. t = 48 h: Second reminder fires. With reminders_count=2 reached, the consumer transitions to escalation phase and schedules an escalation-pr-approval-requested row at t = 72 h.
  4. 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.
  5. t = 96 h: Second escalation fires. With escalation_count=2 reached, the chain ends. No more reminders are sent for this task unless it transitions back to a triggering state.
  6. Approver acts (any time): on approvals.approved or approvals.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.