# Notifications The notification feature sends transactional email through [Brevo](https://www.brevo.com/) (formerly Sendinblue). It supports two delivery models on top of the same Brevo client: - **List-based notifications** -- visitors opt in (double opt-in) to a Brevo contact list attached to a content object. Sending a notification mails everyone on that list. - **Direct notifications** -- an editor picks specific users and groups from the site and mails exactly those people, resolving their email addresses from their Plone user properties. No opt-in list is involved. Both models share the same registry configuration, the same email template, and the same after-commit delivery mechanism. ## Configuration Notification delivery is configured through four registry records. All default to empty and must be filled in before notifications can be sent. | Registry record | Purpose | |---|---| | `wcs.backend.notification.brevo.apikey` | Brevo API key used for all API calls. | | `wcs.backend.notification.brevo.folderid` | Brevo folder id under which new contact lists are created. | | `wcs.backend.notification.brevo.double_optin_template_id` | Brevo template id for the double opt-in confirmation email. | | `wcs.backend.notification.brevo.notification_template_id` | Brevo template id for the notification email itself. | The notification template receives three parameters that you can reference in the Brevo template: `title`, `text`, and `url`. ### Enabling Notifications on Content Notifications are enabled per content object through one of two behaviors: - **Brevo Push notifications** (`wcs.backend.notification.push.brevo`) -- the list-based model. It stores an `enabled` flag and the Brevo `list_id` for that object. When enabled, visitors can sign up and the object owns its own Brevo contact list (created lazily on first use). - **Brevo Direct Push notifications** (`wcs.backend.notification.direct.push.brevo`) -- the direct model. It stores only an `enabled` flag (enabled by default). A content object must provide one of these behaviors for the notification endpoints to be available on it. ## Recipient Resolution How recipients are resolved depends on the delivery model. ### List-based recipients For list-based notifications, recipients are the contacts in the object's Brevo list. They are fetched from Brevo in pages of 500 and reduced to an `{email, name}` pair each, where the name is built from the contact's `FIRSTNAME` and `LASTNAME` attributes (falling back to the email if no name is stored). Visitors are added to the list through the sign-up endpoint, which by default triggers Brevo's **double opt-in** flow: Brevo sends the confirmation email (using the double opt-in template) and only adds confirmed contacts to the list. ### Direct recipients For direct notifications, the editor selects users and groups. Group selections are expanded to their members (the `AuthenticatedUsers` group expands to all site users). Each resulting user id is resolved to an `{email, name}` pair from the Plone user's `email` and `fullname` properties. Users without a matching account or without an email property are **dropped** and logged. The send routine logs how many recipients were resolved versus how many user ids were selected, and which addresses were ultimately targeted, so dropped recipients can be diagnosed from the logs. ## Delivery (After-Commit Hook) Sending a notification does **not** dispatch email immediately. Recipients are resolved synchronously (so they reflect the state at request time), but the actual Brevo API calls are deferred to a **transaction after-commit hook**. Email is only sent if the transaction commits successfully -- if the request fails and the transaction is aborted, no email goes out. During delivery, recipients are sent as BCC entries in batches (up to 50 per batch) against Brevo's transactional email endpoint, with a short pause between batches to stay within rate limits. The current user's email address (when available) is attached as the `replyTo` of the message, so recipients can reply directly to the person who triggered the notification. Batch failures are logged and do not abort the remaining batches. ## REST API Both endpoints require the content object to provide one of the notification behaviors. ### POST @sign-up Signs a visitor up for the object's notification list. Requires `firstname`, `lastname`, and `email`; any missing field returns `400 Bad Request`. By default this starts Brevo's double opt-in flow, so the visitor receives a confirmation email before being added to the list. ```http POST /Plone/news/@sign-up HTTP/1.1 Host: localhost:8080 Accept: application/json Content-Type: application/json { "firstname": "Jane", "lastname": "Doe", "email": "jane@example.com" } ``` Response: ```json { "status": "success", "message": "User signed up for notifications." } ``` ### POST @send-notification Sends a notification for the current context. Optional `title` (defaults to the object's title) and `message` may be supplied in the body. The object's URL is used as the `url` template parameter. ```http POST /Plone/news/@send-notification HTTP/1.1 Host: localhost:8080 Accept: application/json Content-Type: application/json { "title": "New article published", "message": "

Read our latest update.

" } ``` Response: ```json { "status": "success", "message": "Notification sent to users." } ``` The success response confirms the notification was queued for delivery via the after-commit hook; the actual emails are sent once the request transaction commits. #### Consuming the endpoints **JavaScript -- sign up a visitor:** ```javascript async function signUp(objectUrl, firstname, lastname, email) { const response = await fetch(`${objectUrl}/@sign-up`, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ firstname, lastname, email }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || `Sign-up failed: ${response.status}`); } return response.json(); } // Usage await signUp( 'http://localhost:8080/Plone/news', 'Jane', 'Doe', 'jane@example.com' ); ``` **JavaScript -- send a notification:** ```javascript async function sendNotification(objectUrl, title, message) { const response = await fetch(`${objectUrl}/@send-notification`, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ title, message }) }); return response.json(); } // Usage await sendNotification( 'http://localhost:8080/Plone/news', 'New article published', '

Read our latest update.

' ); ``` **Python -- send a notification:** ```python import requests response = requests.post( 'http://localhost:8080/Plone/news/@send-notification', auth=('editor', 'password'), headers={ 'Accept': 'application/json', 'Content-Type': 'application/json', }, json={ 'title': 'New article published', 'message': '

Read our latest update.

', }, ) response.raise_for_status() print(response.json()['status']) ```