Skip to main content

Notifications

Send an alert to your phone, chat, or inbox when pfSense stops answering or comes back, when sign-in is locked, or when an action fails.

Why Use Notifications?

  1. Checks pfSense every 5 minutes while a destination wants to hear about it, so you learn the firewall stopped answering before a script or shortcut fails.
  2. Reports locked sign-ins and secrets removed with a lost encryption key, so trouble with the operations console reaches you even when nobody has it open.
  3. Sends each alert to every destination that chose its event, from ntfy and Telegram to email and webhooks, so it reaches you wherever you look.
  4. Seals tokens, passwords, and webhook URLs with the encryption key and never shows them again, so the list of destinations doesn't expose them.

Open the Notifications Page

  • Wide screens — Select Notifications in the sidebar, below Connection.
  • Narrow screens — The tab bar keeps Overview, Activity, Access tokens, and Connection, so open the account menu from your initial at the top and select Notifications.
Notifications page, selected in the sidebar, listing the Firewall alerts, Network alerts, Ops chat, and My phone destinations with their services, event counts, and last sends, where the last attempt to Network alerts failed with HTTP 404: Unknown Webhook
The Notifications page with four destinations
pfSense Actions on a phone with the account menu open, listing Appearance, Notifications, Password and sign-in, and Sign out above a tab bar with Overview, Activity, Access tokens, and Connection
The account menu on a phone

Under Destinations, each row shows a destination's name and service, how many events it hears about, and the result of its last send. The newest destination is listed first.

Add a Destination

  1. Select Add notification.
  2. In Service, choose where alerts go. ntfy is selected at first.
  3. Enter a Name you'll recognize, such as the device or chat it goes to.
  4. Fill in the service's fields, as listed in Services.
  5. Under Events, keep the events this destination should hear about. The first four are checked at first, as listed in Events.
  6. Select Add notification in the dialog.
Add notification dialog with ntfy selected, the name Work laptop, a topic URL, an empty optional access token, and the first four of six events checked
Adding an ntfy destination
  • A name can have up to 80 characters, and each destination needs a different one, whatever the letter case.
  • The dialog's Add notification stays unavailable until the name is filled in and at least one event is checked.
  • A field with a problem shows the reason under it, such as Use the full topic URL, like https://ntfy.sh/my-alerts.

It's recommended to select Send test after adding a destination, as covered in Send a Test.

Services

Service offers these in this order, starting with ntfy.

ServiceRequired fieldsOptional fields
ntfyTopic URL, the full topic address, such as https://ntfy.sh/my-alertsAccess token, for a topic that needs one
Reverse Proxy for ntfyContext URL, the https:// address of a context whose interpreter is pfsense-actionsContext token, for a context that needs one
TelegramBot token from @BotFather, and the Chat ID of a user, group, or channel the bot can post toNone
DiscordWebhook URL from the channel's Integrations settings, starting with https://discord.com/api/webhooks/None
SlackWebhook URL of an incoming webhook in your Slack app, starting with https://hooks.slack.com/None
GotifyServer URL, and the App token of an application you made in GotifyNone
PushoverUser key, and the App token of an application you made in PushoverNone
MatrixHomeserver URL, a Room ID such as !room:example.com, and the Access token of an account already in the roomNone
EmailSMTP server, Port (587 at first), Security (STARTTLS, TLS, or None), From, and ToUsername and Password, when the server needs a sign-in
AppriseApprise URL, the notify URL of your Apprise API server with its config keyService URLs, separated by spaces, for a stateless Apprise server
WebhookURL that accepts a JSON POSTBearer token, for an endpoint that needs one
  • Secret fields — Every token, key, and password is a secret field, and so are the Discord, Slack, Apprise, and webhook URLs. The other fields are plain and show their saved values when you edit.
  • Addresses — The ntfy, Gotify, Apprise, and webhook URLs can start with http:// or https://, while the Reverse Proxy for ntfy and Matrix addresses need https://. Discord also accepts https://discordapp.com/api/webhooks/.
  • Matrix room IDs — Use the room ID that starts with !, such as !room:example.com. Room aliases that start with # aren't accepted, because sending needs the room ID.
  • Email — To takes several addresses separated by commas. Entering a Password also requires a Username; the form rejects a password on its own instead of silently sending without authentication.
  • Email security — STARTTLS fails the send when the server doesn't offer it, and TLS encrypts from the start, usually on port 465. None sends without encryption.
  • Reverse Proxy for ntfy — Alerts arrive as the JSON payload covered in Webhook Payload, which a Reverse Proxy for ntfy context with the pfsense-actions interpreter turns into a notification.
Add notification dialog with Email selected, showing the SMTP server, Port 587, STARTTLS security, Username, Password, From, and To fields filled in
The email fields

Events

EventAt firstSent when
Saved secrets were removedCheckedpfSense Actions started without its encryption key and removed the secrets the old key sealed. It's sent while pfSense Actions starts.
Sign-in lockedCheckedTen failed sign-ins from one address within 15 minutes paused sign-in from it. Each address is reported once per 15 minutes.
pfSense can't be reachedCheckedTwo scheduled checks in a row couldn't sign in to pfSense over SSH or run uname -mrs. It's sent once per outage.
pfSense reachable againCheckedA scheduled check succeeded after pfSense can't be reached was sent.
Action failedNot checkedAn API request for an action answered 502 or 500, for example because the SSH sign-in or the pfSense command failed.
Wake-on-LAN sentNot checkedpfSense sent a wake packet for a POST /api/v1/wol request.
  • Off at first — Action failed and Wake-on-LAN sent can come with every request, so they stay off until you check them.
  • Sign-in attempts — Wrong passwords, authenticator and recovery codes, rejected passkeys, and wrong passwords on Password and sign-in all count, while wrong setup codes don't.
  • Pings — A ping request sends Action failed when the SSH sign-in or ping command fails, or something unexpected happens. A target that simply never meets the reachability condition doesn't send it.
  • Not counted — Invalid requests, token and rate limit errors, a missing pfSense connection, and Test connection in the operations console don't send Action failed.
  • Saved secrets were removed — Only destinations without secret fields can get it, such as ntfy without an access token or Email without a password. The others are paused until their secrets are entered again.

The Scheduled Check

pfSense Actions checks pfSense on its own, so you hear about an outage even when no script is calling it.

  • Every 5 minutes — While a destination that isn't paused hears about pfSense can't be reached or pfSense reachable again, pfSense Actions signs in to pfSense over SSH and runs uname -mrs every 5 minutes. The first scheduled check runs 5 minutes after pfSense Actions starts.
  • One at a time — Each SSH command in the check has a 30-second deadline. If a check is still running when another interval arrives, the new interval is skipped instead of opening another session.
  • Two in a row — A scheduled check fails when the sign-in or the command fails, and the second failure in a row sends pfSense can't be reached. An outage is therefore reported about 5 to 10 minutes after pfSense stops answering.
  • Back again — The first scheduled check that succeeds after that sends pfSense reachable again.
  • Skipped — There's no scheduled check while no destination wants these events, while no pfSense connection is saved, or while its password or private key is missing.
  • Separate from the API — The scheduled checks don't use a token, don't count toward the rate limits, and aren't recorded in the activity log.
  • After a restart — The count starts over, so a firewall that's still down is reported again after two more failed scheduled checks. A firewall that came back while pfSense Actions was restarting isn't reported as back.

tip

When pfSense is also the router for the Docker host, an outage can cut off the internet as well, so a cloud service may never get pfSense can't be reached. A destination on the same network, such as a self-hosted ntfy or Gotify server, can still get it.

How Alerts Look

  • Title — Every alert starts with pfSense Actions: and the event, such as pfSense Actions: pfSense can't be reached, and email uses it as the subject.
  • High priority — Saved secrets were removed, Sign-in locked, and pfSense can't be reached use priority 4 on ntfy, 8 on Gotify, and 1 on Pushover. Discord shows them in red, and Apprise sends them as warnings.
  • Low priority — Wake-on-LAN sent uses priority 2 on ntfy and Gotify, and -1 on Pushover.
  • Everything else — pfSense reachable again, Action failed, and the test use priority 3 on ntfy, 5 on Gotify, and 0 on Pushover.
  • Webhooks — Webhook and Reverse Proxy for ntfy destinations get JSON instead, as covered in Webhook Payload.
Example alerts
text
pfSense Actions: pfSense can't be reached
Couldn't sign in to pfSense at 192.0.2.1:22 over SSH in two checks in a row. Actions fail until it answers again.

pfSense Actions: pfSense reachable again
pfSense at 192.0.2.1:22 answered the scheduled check again.

pfSense Actions: Action failed
Wake-on-LAN failed: The pfSense command failed.

pfSense Actions: Sign-in locked
Ten failed sign-ins from 198.51.100.7. Sign-in from it is paused for 15 minutes.

Send a Test

  1. Select Send test on a destination, which reads Sending… while it works.
  2. Check that Test notification arrives with the message This is a test from pfSense Actions. Alerts for the events you chose will look like this.
  • A notification in the corner then shows Test sent to and the destination's name, or The test to … couldn't be sent: with the reason, such as HTTP 401: Unauthorized or No answer within 10 seconds.
  • A test goes out whichever events the destination chose, and it becomes the row's last send.
Destinations list where the My phone row shows last sent just now, with a Test sent to My phone. notification in the corner
A destination after Send test

Edit or Remove a Destination

  • Edit — Opens the same dialog with the note Leave a saved secret blank to keep it. Change the name, the events, or the fields, then select Save.
  • Service — It can't be changed, so add a new destination to switch services.
  • Saved secrets — A saved secret field shows Saved instead of its value. Leaving it blank keeps the saved value, and typing a new one replaces it.
  • Optional secrets — Select Remove saved value below an optional secret, such as an ntfy Access token, to delete it when you save, or Keep saved value to undo that. Required secrets can't be removed this way.
  • Plain fields — They show their saved values, and clearing an optional one removes it when you save. Email is the exception when a password is saved: its Username stays required so the password can't be silently ignored.
  • Remove — Opens a dialog that asks you to confirm with Alerts stop going to this destination. You can add it again later., then stops its alerts right away.
Edit Firewall alerts dialog with the service fixed to Reverse Proxy for ntfy, the context URL, the context token showing Saved with Remove saved value below it, and two events checked
Editing a destination with a saved secret
Remove Network alerts? dialog saying alerts stop going to this destination and that it can be added again later, with Cancel and Remove buttons
Confirming a removal

Secrets

  • Sealed — Secret fields are encrypted with the encryption key before they're saved, and pfSense Actions opens them only to send an alert or to keep them when you edit.
  • Never shown — The operations console and the API show which secret fields are saved, never their values.
  • Backups — It's recommended to back up keys/installation.key together with the database, as covered in Backup and Recovery.

After a Lost Encryption Key

When pfSense Actions starts without its encryption key, it makes a new one and removes what the old key sealed, as covered in Lost Encryption Key.

  • Secrets removed — Every destination's secret fields are removed. The destination stays with its name, events, and plain fields.
  • Paused — A destination missing a secret gets no alerts, and its Send test is unavailable. Its row says which fields to enter, such as Enter the bot token again. Alerts are paused until you do.
  • Notice — The Saved secrets were removed notice at the top of every page has a link for each one, such as Set up the Ops chat notification again, which opens Notifications.
  • Optional secrets too — For an optional secret that a lost key removed, such as an ntfy Access token, either enter it again or select Continue without this value before you save, and the destination resumes. Require this value undoes that choice.
  • Others keep working — Destinations without secret fields keep getting alerts, and the ones that chose Saved secrets were removed get that alert while pfSense Actions starts.
Notifications page under the Saved secrets were removed notice, which lists the pfSense private key, the authenticator app, and the Firewall alerts, Network alerts, and Ops chat notifications, where the paused Firewall alerts, Network alerts, and Ops chat rows ask for their context token, webhook URL, and bot token again with Send test unavailable, and My phone isn't paused
Destinations paused after a lost key

To set a destination up again:

  1. Select Edit on it.
  2. Enter each required field marked A lost key removed it. Enter it again. For an optional one, enter it again or select Continue without this value.
  3. Select Save.
Edit Firewall alerts dialog with an empty Context token field whose hint says a lost key removed it, Continue without this value below it, and two events checked
Entering a removed secret again

The destination then leaves the notice and gets alerts again.

Delivery

  • Right away — Each alert goes to every destination that chose its event as soon as it happens, without holding up the request that caused it.
  • 10-second limit — Each send gives up after 10 seconds without an answer, and a slow service doesn't hold up the others.
  • No retries — A failed alert isn't sent again, and the row shows Last attempt failed in red with the reason.
  • From the controller — Alerts leave from pfSense Actions itself, so it needs to reach each service, such as api.telegram.org, api.pushover.net, or your SMTP server.

Webhook Payload

Webhook and Reverse Proxy for ntfy destinations receive the same JSON payload in a POST. A saved token is sent as Authorization: Bearer <token>.

Webhook body
json
{
  "app": "pfsense-actions",
  "details": {
    "address": "192.0.2.1",
    "port": 22
  },
  "event": "pfsense-unreachable",
  "message": "Couldn't sign in to pfSense at 192.0.2.1:22 over SSH in two checks in a row. Actions fail until it answers again.",
  "occurredAt": "2026-10-26T12:10:00.000Z",
  "priority": 4,
  "tags": ["warning"],
  "title": "pfSense can't be reached",
  "version": 1
}
FieldDescription
appAlways pfsense-actions.
detailsExtra data for the event, listed in the next table.
eventThe event's key, or test for Send test.
messageThe alert's text.
occurredAtWhen it happened, in UTC with milliseconds.
priority4 for high-priority events, 2 for Wake-on-LAN sent, and 3 for the rest, on ntfy's 1-to-5 scale.
tagsntfy tag names that show as emoji.
titleThe event's name, without pfSense Actions:.
versionThe payload format, 1.
KeyEventPriorityTagsdetails
secrets-removedSaved secrets were removed4key, warningitems, the removed secrets in the log's words
sign-in-lockedSign-in locked4lockaddress, the client address that was locked
pfsense-unreachablepfSense can't be reached4warningaddress and port of the saved connection
pfsense-reachablepfSense reachable again3white_check_markaddress and port of the saved connection
action-failedAction failed3xaction and reason
wol-sentWake-on-LAN sent2zapmacAddress and broadcastAddress
testSend test3bellEmpty
  • details.action names the request the way the activity log does: connectivity-check, system-information, reachability-check, or wake-on-lan.
  • details.reason is the short reason, such as The pfSense command failed.

Manage Notifications from Scripts

The Notifications page uses these routes, which scripts can call too.

RouteWhat it does
GET /api/v1/notificationsLists every destination, newest first, with secrets naming its saved secret fields but never their values.
GET /api/v1/notifications/eventsLists the events in the dialog's order, with their labels, descriptions, and defaultEnabled for the ones checked at first.
POST /api/v1/notificationsAdds a destination from name, service, events, and settings, and answers 201.
PUT /api/v1/notifications/{notificationId}Changes a destination's name, events, and settings, keeping saved secret fields that are left out or blank. clearSecrets lists optional secrets to clear, saved or removed by a lost key, each once and never with a new value for the same field.
DELETE /api/v1/notifications/{notificationId}Removes a destination and answers 204.
POST /api/v1/notifications/{notificationId}/testSends the test and answers {"delivered":true}, or false with a message.

notificationId is the destination's id, such as ntf-2a4c6e8a0b2d4f60.

warning

These routes accept only a signed-in browser session. A request with an Authorization header, such as a personal access token, gets 403 with the code session_required, whatever its scopes.

Sign In from a Script

A script signs in the way the operations console does and keeps the session cookies.

bash
read -r -s PFSENSE_ACTIONS_PASSWORD

# Sign in and save the session cookies.
printf '{"username":"admin","password":"%s"}' "$PFSENSE_ACTIONS_PASSWORD" \
  | curl --silent --show-error --fail-with-body --cookie-jar session.txt \
      --header 'Content-Type: application/json' --data @- \
      https://actions.example.net/api/v1/auth/login

unset PFSENSE_ACTIONS_PASSWORD

# Read the CSRF value that changes need from the pfa_csrf cookie.
csrf_token="$(awk '$6 == "pfa_csrf" { print $7 }' session.txt)"

# List the destinations.
curl --silent --show-error --fail-with-body --cookie session.txt \
  https://actions.example.net/api/v1/notifications

# Send a test to one destination.
curl --silent --show-error --fail-with-body --cookie session.txt \
  --request POST --header "X-CSRF-Token: $csrf_token" \
  https://actions.example.net/api/v1/notifications/ntf-2a4c6e8a0b2d4f60/test

# Sign out, which ends the session, then delete the cookies.
curl --silent --show-error --cookie session.txt \
  --request POST --header "X-CSRF-Token: $csrf_token" \
  https://actions.example.net/api/v1/auth/logout

rm session.txt
  • Username — Replace admin with your username. printf doesn't escape JSON, so a password with " or \ needs those characters escaped first.
  • Changes — POST, PUT, and DELETE need the X-CSRF-Token header with the session's CSRF token, which the sign-in answer also returns as csrfToken.
  • Authenticator app — When it's on, the sign-in answers with a challenge instead, which you send with a code to POST /api/v1/auth/login/second-factor.
  • Failed sign-ins — They count toward the sign-in limit, and ten from one address send Sign-in locked to the destinations that chose it.
  • Session file — session.txt holds a working session until it expires or you sign out, so it's worth keeping it private.

Request Body

POST /api/v1/notifications
json
{
  "name": "My phone",
  "service": "ntfy",
  "events": ["sign-in-locked", "pfsense-unreachable", "pfsense-reachable"],
  "settings": {
    "url": "https://ntfy.sh/pfsense-alerts",
    "token": "tk_example"
  }
}
servicesettings keys
ntfyurl, token
ntfy-reverse-proxyurl, token
telegrambotToken, chatId
discordwebhookUrl
slackwebhookUrl
gotifyurl, token
pushoveruserKey, appToken
matrixhomeserverUrl, roomId, accessToken
emailhost, port, security (starttls, tls, or none), username, password, from, to
appriseurl, urls
webhookurl, token
  • events takes the keys from Webhook Payload, except test, and each key only once.
  • Every settings value is a string, including port, and the required keys match the required fields in Services.
  • A key that isn't listed for the service is refused with This field isn't accepted.
  • PUT takes the same body without service, or with the same one. A plain field that's left out is cleared, so it's worth sending all of them again.

Response Body

One item from GET /api/v1/notifications
json
{
  "createdAt": "2026-10-26T12:00:00.000Z",
  "events": ["sign-in-locked", "pfsense-unreachable", "pfsense-reachable"],
  "id": "ntf-2a4c6e8a0b2d4f60",
  "lastAttemptAt": "2026-10-26T12:05:00.000Z",
  "lastError": null,
  "name": "My phone",
  "secrets": { "token": true },
  "secretsMissing": [],
  "service": "ntfy",
  "settings": { "url": "https://ntfy.sh/pfsense-alerts" },
  "updatedAt": "2026-10-26T12:00:00.000Z"
}
  • secrets marks each saved secret field with true, and secretsMissing lists the ones a lost encryption key removed.
  • lastAttemptAt and lastError describe the last send, and lastError is null when it worked or nothing was sent yet.
  • POST answers 201 with the new destination, and PUT answers 200 with the changed one.
  • The test route answers 200 even when the send fails, with delivered set to false and the reason in message.

Errors

StatusCodeWhen
401authentication_requiredNo signed-in browser session, or it expired.
403session_requiredThe request had an Authorization header, such as a personal access token.
403csrf_invalidA change came without a matching X-CSRF-Token header.
404notification_not_foundThe destination doesn't exist or was removed.
409notification_existsAnother destination uses the name, whatever the letter case.
422invalid_requestA field failed its check, and issues names each one, such as settings.url or events.2.
429rate_limitedThe address sent more than 120 requests under /api/v1 in a minute.

Malformed JSON and bodies over 64 KB get 400 and 413, as described in Request Basics.