GET
/
api
/
v1
/
admin
/
audit-log
Admin audit-log search
curl --request GET \
  --url https://api.example.com/api/v1/admin/audit-log
import requests

url = "https://api.example.com/api/v1/admin/audit-log"

response = requests.get(url)

print(response.text)
const options = {method: 'GET'};

fetch('https://api.example.com/api/v1/admin/audit-log', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/admin/audit-log",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
package main

import (
"fmt"
"net/http"
"io"
)

func main() {

url := "https://api.example.com/api/v1/admin/audit-log"

req, _ := http.NewRequest("GET", url, nil)

res, _ := http.DefaultClient.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.get("https://api.example.com/api/v1/admin/audit-log")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.example.com/api/v1/admin/audit-log")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)

response = http.request(request)
puts response.read_body

Overview

The audit_logs collection is the append-only record of every consequential action performed by an admin, an automated system actor, or (for high-stakes user actions) a user. This endpoint exposes a paginated, filterable read of that trail. Authorization is role-scoped at the service layer, not the middleware. Any admin role (SUPER, MODERATOR, FINANCE) passes the identity gate, but the query itself must be anchored unless the caller is SUPER:
RoleAllowed queries
SUPERAny query, including no filters at all (full feed).
MODERATORMust be anchored to actorId === self OR include a targetId.
FINANCESame as MODERATOR — actorId === self OR a targetId.
A non-SUPER admin issuing a broad, unanchored query receives 403 FORBIDDEN. This prevents a moderator from trawling the trail; they can ask “what did I do” or “what happened to this user/listing” but not “what did everyone do today.”
Reads are themselves audited. Every call to this endpoint writes an audit-log.read row whose payload.filters echoes the query that was issued. Audit-log queries leave a trail.

Authentication

Bearer <accessToken> with scope: 'admin' required (requireAdmin). The role-scoping above is enforced by the service after identity is established.

Path parameters

None.

Query parameters

All filters are optional except where the role-scoping rule kicks in (see overview). Multiple filters are combined with AND.
NameTypeRequiredNotesExample
actorIdstringconditionalFilter by acting principal id (admin id, user id, or null-string for SYSTEM). 1..64 chars. Non-SUPER admins typically set this to their own admin id.66400a8f1c2b4d5e6f7a8b01
actionstringnoNamespaced action key, e.g. user.banned, tenancy.approved, admin.login.mfa-complete. 1..128 chars, must match ^[a-z][a-z0-9._-]+$.user.banned
targetTypestringnoThe kind of target (user, listing, tenancy, conversation, audit-log, …). 1..64 chars.user
targetIdstringconditionalThe target’s id. 1..64 chars. Non-SUPER admins can use this to scope by “what happened to X”.66400a8f1c2b4d5e6f7a8b02
fromstringnoInclusive lower bound on createdAt, ISO 8601 with timezone offset (Z or +00:00).2026-05-20T00:00:00Z
tostringnoInclusive upper bound on createdAt, ISO 8601 with timezone offset.2026-05-23T23:59:59Z
cursorstringnoOpaque base64url pagination cursor returned in nextCursor on the previous page. 1..512 chars.eyJjcmVhdGVkQXQ…
limitintegernoPage size, 1..100. Default 25. Accepts a string that parses as an integer ("50").50

Allowed query examples

// SUPER — any query is allowed.
{
  "action": "user.banned",
  "from": "2026-05-20T00:00:00Z"
}

// MODERATOR — anchored to self via actorId.
{
  "actorId": "66400a8f1c2b4d5e6f7a8b01",
  "limit": 50
}

// MODERATOR — anchored to a target.
{
  "targetType": "user",
  "targetId": "66400a8f1c2b4d5e6f7a8b02"
}

// MODERATOR — REJECTED (will 403): no actorId=self and no targetId.
{
  "action": "user.banned"
}

Request body

None.

Response — 200 OK

FieldTypeNotes
itemsarrayAudit-log entries (below), most-recent first by createdAt.
nextCursorstring | nullCursor for the next page, or null on the last page.

Audit-log entry

FieldTypeNotesExample
idstringThe audit_logs document _id, as a string.66400a8f1c2b4d5e6f7af000
actorTypeenumUSER | ADMIN | SYSTEM. The kind of principal that did the action.ADMIN
actorIdstring | nullThe acting principal’s id, or null for SYSTEM actors.66400a8f1c2b4d5e6f7a8b01
actionstringNamespaced action key. Stable across releases.user.banned
targetTypestring | nullThe kind of target acted on (user, listing, tenancy, audit-log, …), or null if not applicable.user
targetIdstring | nullThe target’s id, or null if not applicable.66400a8f1c2b4d5e6f7a8b02
payloadobjectFree-form, action-specific context. Always an object (never null). Examples: { reason: "spam" } for bans, { filters: { ... } } for audit-log.read.{ "reason": "Repeated harassment reports" }
ipstring | nullBest-effort source IP.203.0.113.42
userAgentstring | nullBest-effort UA.Swappr-Admin/1.0
createdAtstringISO 8601 UTC of when the row was appended.2026-05-22T15:08:11.412Z
{
  "items": [
    {
      "id": "66400a8f1c2b4d5e6f7af001",
      "actorType": "ADMIN",
      "actorId": "66400a8f1c2b4d5e6f7a8b01",
      "action": "user.banned",
      "targetType": "user",
      "targetId": "66400a8f1c2b4d5e6f7a8b02",
      "payload": { "reason": "Repeated harassment reports" },
      "ip": "203.0.113.42",
      "userAgent": "Swappr-Admin/1.0",
      "createdAt": "2026-05-22T15:08:11.412Z"
    },
    {
      "id": "66400a8f1c2b4d5e6f7af000",
      "actorType": "SYSTEM",
      "actorId": null,
      "action": "tenancy.approved",
      "targetType": "tenancy",
      "targetId": "66400a8f1c2b4d5e6f7a9b01",
      "payload": {},
      "ip": null,
      "userAgent": null,
      "createdAt": "2026-05-22T03:01:04.117Z"
    }
  ],
  "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA1LTIyVDAzOjAxOjA0LjExN1oifQ"
}
Pagination follows the cursor convention. The cursor is opaque — never decode it client-side. Items within a page are ordered most-recent-first; a row appended mid-pagination will surface on subsequent pages without disrupting the cursor.

Error responses

StatusCodeMeaning
400VALIDATION_FAILEDA field failed schema validation (e.g. limit out of 1..100, action does not match the ^[a-z][a-z0-9._-]+$ pattern, from/to not ISO 8601 with offset).
401UNAUTHENTICATEDMissing, malformed, expired, or non-admin-scope token.
403FORBIDDENNon-SUPER admin issued an unscoped query — neither actorId === self nor targetId was provided.

Example error — 403 FORBIDDEN

{
  "type": "https://api.swappr.co.uk/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "code": "FORBIDDEN",
  "detail": "Non-SUPER admins must scope an audit-log query by actorId=self or by targetId",
  "instance": "/api/v1/admin/audit-log",
  "requestId": "01HZQ7K3M4N5P6Q7R8S9T0V1W2"
}

Side effects

  • Meta-audit row written. Each successful read appends an audit-log.read row with the requesting admin as actorId, targetType: 'audit-log', and the requested filters echoed into payload.filters. The read is loggable retroactively.

See also

curl

# SUPER — full feed (last 25 most-recent rows)
curl "https://api.swappr.co.uk/api/v1/admin/audit-log" \
  -H "Authorization: Bearer $ADMIN_ACCESS_TOKEN"

# MODERATOR — what happened to this user
curl "https://api.swappr.co.uk/api/v1/admin/audit-log?targetType=user&targetId=66400a8f1c2b4d5e6f7a8b02&limit=50" \
  -H "Authorization: Bearer $ADMIN_ACCESS_TOKEN"

# MODERATOR — what did I do
curl "https://api.swappr.co.uk/api/v1/admin/audit-log?actorId=66400a8f1c2b4d5e6f7a8b01&action=user.banned" \
  -H "Authorization: Bearer $ADMIN_ACCESS_TOKEN"

Postman

See docs/postman/swappr.postman_collection.jsonAdmin → Audit log → Search.