Contents

REST API - Plugixa Activity Log

Everything the admin screens do, they do through this API. There is no private back channel, so anything the app can read, a script can read with the same permissions.

No route creates, edits or deletes an event. That is the design, not an omission. Events are written inside WordPress by the monitors; the API cannot forge one and cannot erase one. The only writes here change configuration, which affects what is recorded from now on and rewrites no history.

Namespace

/wp-json/plugixa-activity-log/v1/

Every successful response has the same envelope:

{ "success": true, "data": [], "meta": {} }

meta is present only when there is something to say, such as paging. Errors are standard WordPress REST errors with a plugixa_activity_log_* code, for example plugixa_activity_log_forbidden or plugixa_activity_log_not_found, and the HTTP status to match.

Authentication

Two ways, and both end at the same permission checks.

Cookie and nonce, from inside wp-admin. Send the signed-in user’s cookies and an X-WP-Nonce header holding a wp_rest nonce. This is what the admin app does.

Application Passwords, from anywhere else. Create one on the account’s profile under Application Passwords. WordPress requires HTTPS for them, except on a local site. Then use HTTP Basic authentication:

curl -u 'alex:abcd efgh ijkl mnop qrst uvwx' \
  'https://example.com/wp-json/plugixa-activity-log/v1/events?min_severity=high&per_page=50'

The account needs access to the log: an administrator, or a role listed under Settings -> Access. The tables below say which level each route needs:

Needs Who has it
view Administrators, and the roles allowed to see the log
settings Administrators. On multisite, network administrators
manage Anybody with view or settings
export PRO Administrators, and the roles allowed to export

See Access.

GET /events

A page of events, newest first.

GET /wp-json/plugixa-activity-log/v1/events?group=authentication&action=failed
{
  "success": true,
  "data": [
    {
      "id": 48211,
      "occurred_at": "2026-10-08T09:14:02Z",
      "event_code": 1003,
      "severity": "medium",
      "group": "authentication",
      "group_label": "Sign-ins",
      "site_id": 1,
      "user": { "id": 5, "login": "sam", "name": "Sam Lee", "exists": true },
      "user_role": "editor",
      "ip": "203.0.113.9",
      "object": { "type": "user", "id": 5, "label": "sam", "url": "" },
      "action": "failed",
      "message": "Failed sign-in attempt for \"sam\": the password was wrong."
    }
  ],
  "meta": { "has_more": true, "next_before_id": 48211, "newest_id": 48211, "gap": false }
}

occurred_at is UTC. user.login is the login recorded at the time; user.exists says whether that account is still there.

Paging parameters

Parameter Meaning
per_page 1 to 200. Defaults to the Events per page setting, itself 50 by default
before_id Only events older than this id. Pass meta.next_before_id to fetch the next page
after_id Only events newer than this id: the live tail
with_total Also count the matches, into meta.total

These four are enforced: a per_page of 500 is a 400 error, because a wrong paging value is a bug in the caller.

Keyset paging

Pages are fetched by cursor, not by page number. There is no page and no offset. A numbered page 4,000 would make the database walk past every earlier row first; a cursor jumps straight to the place, so the last page of a ten-million-row log costs what the first did.

To read everything, follow meta.next_before_id until meta.has_more is false:

GET /events?per_page=200
GET /events?per_page=200&before_id=48012
GET /events?per_page=200&before_id=47812

To read only what is new since your last call, remember the highest id you have seen (meta.newest_id) and pass it as after_id next time:

GET /events?after_id=48211

meta.gap is true when more new events arrived than fit in one page. A poller that sees it should reload from the top instead of assuming it has everything.

meta key Meaning
has_more There is another page in the direction you are reading
next_before_id The id of the last event on this page, or null for an empty page
newest_id The id of the first (newest) event on this page, or null
gap An after_id poll came back full

with_total

Totals are computed only when asked for, because a poll every few seconds must not pay for a count.

meta key Meaning
total The number of matching events
total_capped True when a filtered count stopped at 10,000: read it as “10,000 or more”
total_approximate True when nothing is filtered and the log is larger than 10,000: the total is the database’s own estimate of the table

Counting is exact up to 10,000. Beyond that, an exact figure would mean scanning the table, which is the one thing this endpoint never does.

Filter parameters

Parameter Matches
severity Exact severities, comma separated or repeated: informational, low, medium, high, critical
min_severity At least this severe
event_code Event codes, comma separated or repeated
group An event group key, such as authentication, users or content
module The id of the module that owns the events, such as content
user_id The WordPress user id of the person who acted
user_login The login recorded at the time. Also matches a failed sign-in by the name that was typed
user_role The role the person had at the time
ip An exact client address
object_type The kind of object: user, post, media, comment, term, plugin, theme, option and others
object_id The id of the object
action The verb: created, modified, deleted, login, failed, activated and others
search Free text over the message, the object and the user. At most 100 characters. A term shaped like an address matches the IP by prefix
date_from Start: Y-m-d or ISO 8601, read in the site’s time zone
date_to End, inclusive: Y-m-d or ISO 8601, read in the site’s time zone
site_id A network site id. Only network administrators see other sites

Filters combine with AND.

A filter value the server does not understand is dropped, never an error. That is deliberate: a hand-edited or stale link then opens a wider view instead of failing. It also means a typo in a filter returns more than you expected, not nothing, so check the result when a filter seems to have been ignored.

On multisite, a site administrator is confined to their own site by the server, whatever site_id says.

The same parameter list, with descriptions, is in the REST index: GET /wp-json/plugixa-activity-log/v1.

The other routes

Route Method Needs Purpose
/events/{id} GET view One event in full, with its decoded details, a link to the object when it still exists, and its definition
/events/catalogue GET view Every event the plugin can record, and whether each is on
/events/catalogue PATCH, POST settings Switch events on or off
/stats GET view Totals and chart data for a window: days (1 to 365, default 30), plus the same filters as /events
/monitors GET view The monitors and whether each runs
/monitors/{id} PUT, PATCH, POST settings Switch one monitor: { "enabled": true }
/settings GET settings The settings, and the choices each control offers
/settings POST, PUT settings Save settings. Send only the keys you are changing
/users GET view A picker of users: search, at most 20 results
/views GET, POST view Saved views you can see, yours and shared ones; create one
/views/{id} PUT, PATCH, DELETE view Change or delete a view. Other people’s shared views need settings
/health GET settings The whole Health report
/notifications/test POST settings Send a test email to the saved recipients
/import/wsal GET settings The status of the WP Activity Log import
/import/wsal/start POST settings Start or resume it: date_from, skip_unmapped
/import/wsal/cancel POST settings Stop it, keeping its position
/bootstrap GET manage The admin app’s start-up payload

An event on another site of the network answers 404 to a site administrator, not 403, so the id’s existence is not revealed.

Switching events

PATCH /events/catalogue
{ "overrides": { "2001": false, "2012": true } }

An unknown code is a 422 and nothing is saved. Only differences from an event’s default are stored, so setting an event back to its default removes the override.

Saving settings

POST /settings
{ "retention_days": 30, "privacy_ip_mode": "truncate" }

The response is the full settings as saved. A key the plugin does not recognise is discarded, and every value goes through the same checks as the Settings screen, so read the response to see what was stored. The change is recorded in the log as event 9000.

Saved views

A view is a name plus a set of log filters. Anyone who can read the log can save views for themselves, at most 50. Only somebody with settings access can share a view with every reader, or change and delete other people’s shared views. is_default marks the view the log opens with.

POST /views
{ "name": "Failed sign-ins", "filters": { "group": "authentication", "action": "failed" }, "is_default": true }

See Saved views.

Pro routes PRO

These exist only when Plugixa Activity Log Pro is installed. On the free build they answer rest_no_route, because the code is not there: no licence check refuses them.

Route Method Needs Purpose
/export PRO GET export The events matching the same filters as /events, as a CSV file, at most 50,000 rows
/reports/run PRO POST export Build a report and download it
/reports/schedules PRO GET, POST settings List scheduled reports; create one
/reports/schedules/{id} PRO PUT, PATCH, DELETE settings Change or delete a schedule
/reports/schedules/{id}/run-now PRO POST settings Send a scheduled report now
/rules PRO GET, POST settings List alert rules; create one
/rules/{id} PRO GET, PUT, PATCH, DELETE settings Read, change or delete a rule
/rules/{id}/test PRO POST settings Send a sample to every channel of the rule
/sessions PRO GET signed in Your own sessions. With settings access, everybody’s: user_id, role, search
/sessions/end-others PRO POST signed in End your other sessions
/sessions/{user_id}/end-all PRO POST own, or settings End all sessions of a user
/sessions/{user_id}/{verifier} PRO DELETE own, or settings End one session
/geolocation/status PRO GET settings The IP location database and its last update
/geolocation/update PRO POST settings Download the database now
/files/status PRO GET settings The file scan’s status
/files/scan PRO POST settings Start a scan
/archive/events PRO GET view A page of archived events. Same parameters as /events
/archive/events/{id} PRO GET view One archived event
/archive/status PRO GET settings What the archive holds
/archive/settings PRO GET, POST, PUT settings Read or save the archive settings
/archive/test-connection PRO POST settings Try the external database connection
/archive/run-now PRO POST settings Archive now
/mirrors PRO GET, POST settings List mirrors; create one
/mirrors/{id} PRO GET, PUT, PATCH, DELETE settings Read, change or delete a mirror
/mirrors/{id}/test PRO POST settings Send a test event to a mirror

Even here, nothing edits an event. The deletes in this table remove a rule, a schedule, a mirror or a sign-in session, never a log entry. The archive moves events; it does not change them. Every CSV download is itself recorded, as event 9003.

Request bodies PRO

The routes that create something take JSON. A change (PUT or PATCH) takes the same fields, and a field you leave out keeps its stored value. A request the plugin cannot accept answers 422 with a sentence saying why.

An alert rule. name and at least one channel are required.

POST /rules
{
  "name": "Administrator sign-ins from outside the office",
  "enabled": true,
  "conditions": {
    "groups": ["authentication"],
    "min_severity": "medium",
    "user_roles": ["administrator"],
    "ips": ["203.0.113.0/24"]
  },
  "channels": [
    { "type": "email", "target": "security@example.com" },
    { "type": "webhook", "target": "https://hooks.example.com/activity", "secret": "a-signing-secret" }
  ],
  "throttle_minutes": 15
}
Field Values
conditions.codes Event codes. Codes of 9000 and above are dropped
conditions.groups Area slugs, as the group filter of /events uses them
conditions.min_severity informational, low, medium, high or critical
conditions.severities An exact list of the same severity names
conditions.user_roles Role slugs
conditions.user_ids User IDs
conditions.ips Single addresses or CIDR ranges
conditions.object_types Object type slugs
conditions.search Text to find, at most 100 characters
channels[].type email, slack, discord, teams or webhook. At most 10 channels
channels[].target An email address, or an https:// address
channels[].secret Webhook only: the signing secret
throttle_minutes 0 to 1440

Every condition that is filled in must hold, and an empty one does not narrow. severities and user_ids have no field in the rule editor. A stored signing secret is returned masked; send the masked value back to keep it. See Alert Rules.

A report, built and downloaded now.

POST /reports/run
{
  "title": "Sign-ins, last 7 days",
  "format": "html",
  "days": 7,
  "sections": ["summary", "by_user", "events"],
  "filters": { "group": "authentication" }
}
Field Values
format csv (default) or html
days 1 to 365, default 30. Or give date_from and date_to instead
sections Any of summary, by_severity, by_group, by_user, by_ip, events. All of them when left out
filters severity, min_severity, event_code, group, module, user_id, user_login, user_role, ip, object_type, object_id, action, search, site_id

A scheduled report. name is required.

POST /reports/schedules
{
  "name": "Weekly security summary",
  "frequency": "weekly",
  "weekday": 1,
  "hour": 7,
  "format": "html",
  "sections": ["summary", "by_severity", "events"],
  "filters": { "min_severity": "high" },
  "recipients": ["owner@example.com"]
}
Field Values
frequency daily, weekly (default) or monthly
weekday Weekly only: 0 (Sunday) to 6. Default 1
monthday Monthly only: 1 to 28. Default 1
hour 0 to 23, on the site’s clock. Default 7
format html (default) or csv
sections, filters As for /reports/run
recipients Email addresses, at most 20
enabled true (default) or false

See Reports.

A mirror. name and type are required, a site can have at most 20, and the type cannot be changed afterwards.

POST /mirrors
{
  "name": "Central syslog",
  "type": "syslog",
  "config": { "host": "logs.example.com", "port": 6514, "transport": "tls" },
  "filters": { "min_severity": "low" }
}
type config fields
syslog host, port (default 514), transport: udp (default), tcp or tls
http url (https:// only), token
file keep_days: 1 to 365, default 14

filters takes min_severity and groups. A stored token is returned masked: send the masked value back to keep it, or an empty one to clear it. See Mirrors.

An export takes no body: GET /export reads the same filter parameters as GET /events.

Recipes

Failed sign-ins today, most recent first:

curl -u 'logreader:xxxx xxxx xxxx xxxx xxxx xxxx' \
  'https://example.com/wp-json/plugixa-activity-log/v1/events?group=authentication&action=failed&date_from=2026-10-08'

Everything one person did, with a count:

curl -u 'logreader:xxxx xxxx xxxx xxxx xxxx xxxx' \
  'https://example.com/wp-json/plugixa-activity-log/v1/events?user_login=alex&with_total=1'

Poll for new high-severity events from a monitoring tool:

curl -u 'logreader:xxxx xxxx xxxx xxxx xxxx xxxx' \
  'https://example.com/wp-json/plugixa-activity-log/v1/events?min_severity=high&after_id=48211'

Troubleshooting

Symptom Usual cause
401 or 403, plugixa_activity_log_forbidden The account has no access, or the route needs a higher level than view.
Refused although you are signed in to wp-admin Cookie requests need the X-WP-Nonce header. Without it WordPress treats the request as signed out.
Application Passwords are not offered on the profile The site is not on HTTPS.
rest_no_route A Pro route on a free build, or the module behind the route is switched off.
400 on per_page It must be between 1 and 200.
A filter returned everything Its value was not understood, and was dropped.
total stops at 10,000 total_capped is true. The count is “10,000 or more”.
Times look wrong occurred_at is UTC. date_from and date_to are read in the site’s time zone.
A site administrator sees fewer events than the network administrator They are confined to their own site.

What to do next

  • The same data from a terminal: WP-CLI.
  • React to events in PHP instead of polling: Hooks.
  • What each event code means: Events.

Quick Links