Skip to content

Event catalogue ​

Every Event the panel raises, with the scope a listener needs to hear it and the fields it carries. Webhook subscribers, addons, Telegram chats and email addresses all choose from this list. To set up a subscriber, see Webhooks and events; for how events are queued and delivered, see The event bus.

The same catalogue is served by the panel itself at GET /api/v1/events, which every API token may read.

The envelope ​

Each event reaches a webhook as a POST with a JSON body:

json
{
  "v": 1,
  "id": 4182,
  "event": "user.quota_warning",
  "time": 1760180000,
  "data": {
    "userId": 57,
    "name": "alice",
    "adminId": 3,
    "usedPercent": 81,
    "threshold": 80,
    "used": 86973087744,
    "volume": 107374182400
  },
  "recipients": [3]
}
Field
vthe envelope version, 1
idthe event's id: the same for every subscriber and on every retry
eventthe event name
timewhen it was raised, in Unix seconds
datathe payload: the fields listed for the event below
recipientsthe admin ids the event is about, when it is about one; absent otherwise

Times in payloads are Unix seconds, and traffic figures are bytes. A payload never carries a password, key or subscription link; read those from the API with a token if you need them.

Headers and signature ​

Header
Content-Typeapplication/json
User-Agentnexora-panel
X-Nexora-Eventthe event name
X-Nexora-Deliverythe delivery's id: the same on every retry and on Send again
X-Nexora-Signaturet=<unix seconds>,v1=<hex>, present when the subscriber has a secret

v1 is the hex HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the subscriber's signing secret:

text
v1 = hex( HMAC-SHA256( secret, "<t>" + "." + <raw body bytes> ) )

A receiver should:

  1. Read the raw body before parsing it; a re-serialised body does not match.
  2. Recompute v1 with its secret and compare in constant time.
  3. Refuse a request whose t is more than five minutes from its own clock, so a captured delivery cannot be replayed later.
  4. Answer 2xx quickly. Anything else, or no answer within ten seconds, is a failure and is retried.

In Python:

python
import hashlib, hmac, time

def verify(secret: bytes, header: str, body: bytes, max_skew: int = 300) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1 or abs(time.time() - int(t)) > max_skew:
        return False
    mac = hmac.new(secret, t.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, v1)

To process each event once, keep the id values you have handled. A restore from backup brings back the backup's ids, so a receiver that remembers ids across a restore should start over when it receives panel.restore_applied.

Who hears what ​

Scope. Each event names the scope a listener needs. An addon may ask for an event only if its token holds that scope too, and the consent screen shows both (a :write scope includes the matching :read). A Telegram chat or email address hears an event only if its account's role holds the scope. A webhook the main admin adds by hand hears every event its filter names.

Families. The part before the dot is the family, and it decides routing:

FamilyAboutNames an account
user.*a customer accountyes: adminId is the reseller that owns it (0 for the panel)
node.*a nodeno
admin.*an operator accountyes: adminId is that account
panel.*the panel itselfno

A subscriber set to Only events about one account, and every reseller's chat or address, receives only the events that name that account. node.* and panel.* events therefore never reach a reseller.

Bulk work raises one event. A bulk operation on the users page raises a single user.bulk, and generating a batch of accounts a single user.generated, however many accounts they touched.

user.* ​

Every user.* event carries these three fields, plus the ones in its row:

FieldType
userIdintegerthe account's id
namestringthe account's name
adminIdintegerthe reseller that owns it; 0 for the panel
EventScopeRaised whenMore fields
user.createdusers:readAn account was created.
user.deletedusers:readAn account was deleted.
user.enabledusers:readThe panel switched an account back on, for example after a renewal or a usage reset.
user.disabledusers:readThe panel switched an account off. reason is volume, expiry, resale-volume or resale-expiry.reason
user.quota_reachedusers:readThe traffic case of user.disabled: the account used what it paid for.reason
user.expiredusers:readThe date case of user.disabled: the account's time ran out.reason
user.quota_warningusers:readThe account crossed the Traffic used threshold (default 80 %). Once per cap and threshold.usedPercent, threshold, used, volume
user.expiringusers:readThe account entered one of the Days before expiry windows (default 7 and 1). Once per window.expiry, daysLeft, threshold
user.renewedusers:readThe account was renewed. Carries its new date and traffic.expiry, volume
user.activatedusers:readA plan that starts at the first connection started; the expiry is now fixed.expiry
user.first_fetchusers:readA client took the account's configurations for the first time. client names the app.client
user.device_limit_reachedusers:readA device was refused the subscription because the account's device limit is full. At most once per device per hour.hwid, limit

Two user.* events carry their own fields instead of the three above:

EventScopeRaised whenFields
user.bulkusers:readOne bulk operation finished. op is the operation; ids are the accounts it reached; adminId is the reseller when one did it.op, matched, affected, blocked, failed, actor, adminId, ids
user.generatedusers:readA batch of accounts was generated from a name pattern.pattern, count, skipped, planId, actor, adminId

The thresholds are set on the Webhooks and events page.

node.* ​

Most node.* events carry the node's id and name:

EventScopeRaised whenFields
node.connectednodes:readA node started answering the panel. warnings lists anything it is not running.nodeId, name, status, message, warnings
node.disconnectednodes:readA node stopped answering. message is the error.nodeId, name, status, message, warnings
node.limit_reachednodes:readA node reached its periodic traffic limit and was switched off. period is the cycle.nodeId, name, used, limit, period
node.disk_highnodes:readThe node's root filesystem crossed Disk usage (default 90 %). Once per crossing.host fields
node.disk_recoverednodes:readIt fell back under the threshold by at least five points.host fields
node.memory_highnodes:readThe node's memory crossed Memory usage (default 90 %). Once per crossing.host fields
node.memory_recoverednodes:readIt fell back under the threshold by at least five points.host fields
node.rejections_highnodes:readThe node refused more connections through one block outbound inside an hour than Rejected connections per hour (default 100). outbound is that outbound's tag; window is 1h. Once per crossing.nodeId, name, outbound, count, threshold, window

The host fields are nodeId, name, resource (disk or memory), usedPercent, threshold, used and total (bytes). Disk and memory are read every five minutes.

admin.* ​

EventScopeRaised whenFields
admin.loginadmins:writeAn operator logged in.adminId, username, ip
admin.login_failedadmins:writeA login was refused. username is what was typed; stage is mfa for a wrong two-factor code.username, ip, stage
admin.login_new_ipadmins:writeAn operator logged in from an address other than its previous login's.adminId, username, ip, previousIp
admin.2fa_changedadmins:writeAn operator turned two-factor authentication on or off.adminId, username, enabled
admin.owner_changedadmins:writeThe panel was handed to another main admin. previous names the account that gave it up.adminId, username, previous
admin.resale_cap_reachedadmins:writeA reseller spent its allowance or its account expired; its users went offline with it. reason is volume or expiry.adminId, username, reason, used, volume

panel.* ​

EventScopeRaised whenFields
panel.startedstats:readThe panel came up. restored says a restore was applied on the way.version, listen, restored
panel.stoppingstats:readThe panel is shutting down cleanly, a restart included. A panel.started with no panel.stopping before it means the panel stopped unexpectedly.version
panel.crashedstats:readAn internal error was caught while answering a request; the panel is still running. At most once per route and message every five minutes.method, path, error, version
panel.setup_completedsettings:readThe setup wizard finished and the main admin exists.username, restart
panel.settings_changedsettings:readA panel-wide setting (address, TLS, base path and the like) was changed. The value is never carried.setting, actor
panel.restore_appliedusers:readA backup was restored over this panel.superseded, tables, rows
panel.backup_donebackup:readThe scheduled backup wrote an archive.name, size
panel.backup_failedbackup:readThe scheduled backup failed; it is tried again within the hour.error
panel.backup_upload_failedbackup:readAn archive did not reach a backup destination. It is still on the panel's server.destination, name, error
panel.cert_renewedcertificates:readA certificate the panel manages was renewed.certificate fields
panel.cert_renew_failedcertificates:readA renewal failed; it is tried again every six hours.certificate fields, error
panel.cert_expiringcertificates:readA certificate you uploaded is close to its end; the panel cannot renew it. Once per certificate.certificate fields
panel.ruleset_refresh_failedpool:readThe hourly refresh of the rule sets failed.error
panel.update_availablestats:readA newer panel release exists. version is the new one, current the running one. Once per release.version, current, url
panel.license_changedlicense:writeThe licence state changed: licensed, expired or free, or new limits.licensed, reason, previousReason, licenseId, issuedTo, expiresAt, limits
panel.license_expiringlicense:writeThe licence ends in 30, 14, 7 or 1 days. Once per threshold. There is no grace period after the date.licenseId, issuedTo, expiresAt, daysLeft, threshold
panel.license_unverifiedlicense:writeThe panel has not been able to confirm its licence for some days. deadline is when it falls back to the free tier if that goes on.licenseId, verifiedAt, deadline, daysLeft
panel.ip_bannedsecurity:readThe login protection banned an address.ip, until, reason
panel.addon_registeredtokens:writeAn addon was registered.addon fields
panel.addon_removedtokens:writeAn addon was removed with its token and webhook. The addon itself is sent this first, whatever its filter.addon fields
panel.addon_unhealthytokens:writeAn addon's health path failed twice in a row. Once per crossing.addon fields, error
panel.addon_recoveredtokens:writeAn unhealthy addon answered its health path again.addon fields
panel.addon_update_waitingtokens:writeA newer version of a signed addon asks for more than it holds and waits for approval.addon fields, version
panel.pinganyThe test event Send test event sends to one subscriber.subscriberId, name, by

The certificate fields are kind (node or panel), certificateId, nodeId, name and notAfter. The addon fields are addonId, slug, name and signed.

For what the licence events mean for your panel, see Licence; for addons, see Addons page.

Text and images under CC BY 4.0.