Webhooks and log export

Telling your other systems what happens in your mail as it happens, and sending the security log to the SIEM your security team already watches.

Your organisation’s other systems can hear what happens in its mail without asking the API over and over: a webhook sends each event to an HTTPS address of yours as it happens. And the audit log, with sign-in events and alerts, can stream to your SIEM over syslog or HTTPS.

The examples use the vsx shell function from the Quick start.

Webhooks

On the console, open Settings › Webhooks and choose Add. Give the HTTPS address to send to and the events you want. The signing secret is shown once: keep it where your receiver can read it.

From the command line:

vsx admin webhooks add https://hooks.example.com/mail --events alert,quarantined --description 'Helpdesk tickets'
vsx admin webhooks list
vsx admin webhooks show 3

Events

EventCommand lineWhenIts fields
person.addedperson-addedA person’s account is made.account, address, name, by
person.removedperson-removedA person’s account is removed.account, address, by
alert.raisedalertAn alert goes off.alert, measure, threshold, detail, told
message.quarantinedquarantinedA message is held in quarantine.account, address, message, queueId, sender, size, stage, reason, authenticatedDomain
legal-hold.changedlegal-holdA legal hold is set, changed or lifted.account, domain, held, since, until, by
audit.lineauditA line is written to the audit log.line, who, verb, resource, outcome, why, before, after
sign-in.new-placenew-placeSomebody signs in from somewhere their account had not been seen from.account, address, protocol, from, method
sign-in.refusedrefusedThe right password, refused by the organisation’s access rules, from a new place.account, address, way, from, method
sign-in.lockedlockedAn account or a network is locked out for failed sign-ins.resource, account, network, until, through, from
message.delivereddeliveredThe receiving server took a message one of your people sent, for one recipient.queueId, sender, recipient, remote, reply
message.bouncedbouncedA message could not be delivered to one recipient: refused for good, or out of time. The sender is told as well.queueId, sender, recipient, remote, reply, expired

No event carries what a message says: only what the event is about. A legal hold’s reason stays in the audit log.

What is sent

Each event is a POST of JSON:

{
  "id": "1.message.quarantined.7f3c2a90e1b44d0c",
  "type": "message.quarantined",
  "at": 1790812800000,
  "tenant": 1,
  "data": { "address": "ada@example.com", "sender": "offers@spam.example", "reason": "malware", "…": "…" }
}

id is the same every time the event is sent, and at is in Unix milliseconds. The request carries these headers:

HeaderWhat it holds
Nixt Server-EventThe event’s type.
Nixt Server-Event-IdThe event’s id, the same on every retry, so a receiver can ignore one it has already had.
Versealx-TimestampWhen it was signed, in Unix seconds.
Nixt Server-Signaturev1= and the signature.

Verifying the signature

The signature is the HMAC-SHA256, in lower-case hexadecimal, of the timestamp, a full stop, and the body exactly as received, under the webhook’s secret. Check it before trusting the event, and refuse one whose timestamp is more than a few minutes from your clock, so an old request cannot be replayed:

import hashlib, hmac, time

def verified(secret: bytes, headers, body: bytes) -> bool:
    timestamp = headers["Versealx-Timestamp"]
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = "v1=" + hmac.new(secret, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, headers["Nixt Server-Signature"])

Answer with any 2xx status once the event is safely yours.

Deliveries, retries and pausing

Events go to each webhook in the order they happened. When a delivery fails, it is tried again after 30 seconds, then after twice as long each time, up to an hour apart, and the events behind it wait. A webhook that has failed for a whole day is paused, and the organisation’s administrators get the webhook-paused alert.

A webhook’s page lists its last 50 deliveries, with the status and answer each got, and Send again for any of them. Resume starts a paused webhook from where it stopped.

vsx admin webhooks deliveries 3
vsx admin webhooks resend 3 <delivery>
vsx admin webhooks resume 3

Up to 10,000 events wait for each webhook. When that many are waiting, newer events are not kept for it, and its page says how many were missed and between when. The audit lines among them can still be exported (see Exporting to a file).

Changing a webhook

vsx admin webhooks change 3 --events alert,quarantined,locked
vsx admin webhooks secret 3
vsx admin webhooks remove 3

secret makes a new signing secret, shown once, and the old one stops working at once.

Where a webhook may send

A webhook’s address must be HTTPS on a public host, and may not hold a user name or password. Addresses that resolve to private, loopback, link-local or other non-public addresses are refused, both when the webhook is saved and each time it connects, so a webhook cannot be used to reach machines inside your network. Redirects are not followed.

A server’s operator can allow private addresses for every organisation on the server, for a receiver on the same network:

[webhooks]
allow_private = true

This lets any organisation’s administrators make the server connect to machines on its network. The server warns about it when it starts, and the console shows the warning on the webhook pages. Leave it off unless every organisation on the server is yours.

A webhook, and a log export over HTTPS, goes to port 443 on a public host. For a receiver on another port, such as a Splunk HTTP Event Collector on 8088, the operator allows other ports:

[webhooks]
any_port = true

Private addresses, once allowed, may use any port. A log export over syslog always uses the port you give it.

Security log export

The log export sends every line of the audit log, sign-ins from new places, refused sign-ins, lockouts and alerts to your SIEM as they happen.

On the console, open Settings › Security log export. From the command line, choose syslog or HTTPS:

vsx admin log-export set syslog siem.example.com:6514
vsx admin log-export set https https://splunk.example.com:8088/services/collector/event --format splunk-hec
vsx admin log-export show
vsx admin log-export off
DestinationWhat is sent
syslogSyslog over TLS (RFC 5425), each event an RFC 5424 message from the app versealx, with the event’s type as its message id and the event’s JSON as its message. Audit lines use facility log audit, sign-ins security/authorization and alerts log alert. The collector’s certificate is checked against the public certificate authorities.
https with --format jsonBatches of events, one JSON event a line (application/x-ndjson), signed as a webhook is, with the token you give as the secret.
https with --format splunk-hecSplunk’s HTTP Event Collector form, sent with Authorization: Splunk <token>, with the source versealx and the sourcetype versealx:<type>.
https with --format elasticElasticsearch’s bulk API form, each event a document with an @timestamp, sent with Authorization: ApiKey <key>. A batch counts as taken only when Elasticsearch refused none of it.

Pinning the collector’s certificate

A syslog collector can be pinned, so the export talks to it and nothing else, even if somebody obtains a certificate for its name. Pin the SHA-256 of its certificate, or of its public key, which keeps working when the certificate is renewed on the same key. Either is accepted as openssl prints it:

openssl s_client -connect siem.example.com:6514 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256
vsx admin log-export set syslog siem.example.com:6514 --pin-certificate 5E:2B:…:9A
vsx admin log-export set syslog siem.example.com:6514 --pin-public-key 7c41…e0

With a pin, the collector must present the pinned certificate and still pass the usual checks of its name and authority. For a collector whose certificate no public authority signed, add --pin-only, so the pin is the only check. That is weaker, since nothing else is checked, including expiry; the console, log-export show and doctor warn about it. A collector that presents another certificate is refused, and the export’s status says the pin did not match. doctor connects to each pinned collector without sending anything and warns when its certificate no longer matches the pin.

A client certificate for the collector

A collector that asks its senders for a certificate (mutual TLS) is given one that the server’s operator names, as two files on the server:

versealx-server admin log-export set syslog siem.example.com:6514 --tenant 3 --client-certificate /etc/versealx-server/siem-client.pem --client-key /etc/versealx-server/siem-client.key

Only the operator sets or removes it, since it is a file on the server; the organisation sees which certificate is presented, never the key. --no-client-certificate removes it.

For HTTPS, the collector’s token is asked for at the terminal, or read from standard input with --secret-stdin. It is kept encrypted and never shown again.

The export keeps its own place in the log. A collector that is down catches up from where it stopped once it is back, even across a restart of the server. Up to 100,000 events wait. The export is paused after a day of failing, like a webhook, and log-export resume starts it again.

The same rule on addresses applies as for webhooks.

Exporting to a file

To send a stretch of the log once, or to fill a gap, write it to a file in the same JSON form, one event a line:

vsx admin audit export --since 2026-10-01 --until 2026-10-08 --format jsonl --to audit.jsonl

On the console, the Audit log page has Export as JSON lines….

Who can do what

Webhooks and the log export are for the organisation’s administrators. Every change to them is in the audit log.

Over the API

RouteWhat it does
GET · POST /api/v1/tenants/{tenant}/webhooksThe webhooks, and a new one. Its secret is in the answer to POST only.
GET · PATCH · DELETE /api/v1/tenants/{tenant}/webhooks/{id}One webhook.
POST /api/v1/tenants/{tenant}/webhooks/{id}/secretA new signing secret, shown once.
GET /api/v1/tenants/{tenant}/webhooks/{id}/deliveriesIts last 50 deliveries.
POST /api/v1/tenants/{tenant}/webhooks/{id}/deliveries/{delivery}/resendSends one again.
POST /api/v1/tenants/{tenant}/webhooks/{id}/resumeStarts a paused webhook again.
GET · PUT · DELETE /api/v1/tenants/{tenant}/log-exportThe log export’s destination.
POST /api/v1/tenants/{tenant}/log-export/resumeStarts a paused export again.

Something unclear or out of date on this page? Tell us.