HelloJohn / docs
Audit Logs

Querying Audit Logs

Filter, search, and paginate audit logs via the HelloJohn API to power security dashboards and compliance reports.

Querying Audit Logs

The audit log API provides filtering, pagination, and aggregation to support security dashboards, compliance workflows, and incident investigations.

List audit logs

GET /v1/audit-logs
Authorization: Bearer <admin_api_key>

Query parameters

ParameterTypeDescription
tenant_idstringFilter to a specific tenant
actor_idstringFilter by actor (user, admin, or API key ID)
actor_typestringuser, admin, api_key, system
typestringEvent type, e.g. user.login. Supports wildcards: user.*
resultstringsuccess or failure
ip_addressstringFilter by exact IP
countrystringISO 3166-1 alpha-2 country code
fromISO 8601Start of time range
toISO 8601End of time range
resource_idstringFilter by affected resource ID
resource_typestringuser, session, tenant, org, etc.
limitintegerMax results per page (default: 50, max: 200)
cursorstringPagination cursor from previous response

Response

{
  "data": [
    {
      "id": "evt_audit_01HXXXX",
      "type": "user.login",
      "actor": {
        "id": "usr_01HXXXX",
        "email": "alice@example.com",
        "type": "user"
      },
      "resource": {
        "id": "usr_01HXXXX",
        "type": "user"
      },
      "tenant_id": "tnt_01HXXXX",
      "organization_id": null,
      "ip_address": "203.0.113.42",
      "user_agent": "Mozilla/5.0...",
      "country": "US",
      "result": "success",
      "metadata": {},
      "created_at": "2024-01-15T14:23:05.123Z"
    }
  ],
  "meta": {
    "total": 1423,
    "limit": 50,
    "cursor": "eyJpZCI6ImV2dF9hdWRpdF8..."
  }
}

Common queries

Failed login attempts in the last hour

FROM=$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || \
       date -u -v-1H +%Y-%m-%dT%H:%M:%SZ)

curl "https://auth.yourdomain.com/v1/audit-logs\
?type=user.login_failed\
&from=${FROM}" \
  -H "Authorization: Bearer <admin_api_key>"

All actions by a specific user

curl "https://auth.yourdomain.com/v1/audit-logs?actor_id=usr_01HXXXX&limit=200" \
  -H "Authorization: Bearer <admin_api_key>"

All actions from a suspicious IP

curl "https://auth.yourdomain.com/v1/audit-logs?ip_address=203.0.113.42" \
  -H "Authorization: Bearer <admin_api_key>"
curl "https://auth.yourdomain.com/v1/audit-logs?type=mfa.*&tenant_id=tnt_01HXXXX" \
  -H "Authorization: Bearer <admin_api_key>"

Admin actions in the last 30 days

curl "https://auth.yourdomain.com/v1/audit-logs\
?actor_type=admin\
&from=2024-01-01T00:00:00Z" \
  -H "Authorization: Bearer <admin_api_key>"

Pagination

The audit log uses cursor-based pagination, which is stable even as new events are inserted.

async function getAllAuditLogs(filters: Record<string, string>) {
  const client = createHelloJohnAdminClient({ secretKey: process.env.HJ_SECRET_KEY! });
  const results = [];
  let cursor: string | undefined;

  do {
    const page = await client.auditLogs.list({
      ...filters,
      limit: 200,
      cursor,
    });

    results.push(...page.data);
    cursor = page.meta.cursor;
  } while (cursor);

  return results;
}

Get a single audit log entry

GET /v1/audit-logs/{id}
Authorization: Bearer <admin_api_key>
curl "https://auth.yourdomain.com/v1/audit-logs/evt_audit_01HXXXX" \
  -H "Authorization: Bearer <admin_api_key>"

Aggregations

Aggregate queries for dashboards:

GET /v1/audit-logs/aggregate
Authorization: Bearer <admin_api_key>
ParameterTypeDescription
group_bystringtype, result, country, actor_id, hour, day
typestringFilter to a specific event type
fromISO 8601Start of time range
toISO 8601End of time range
tenant_idstringFilter to tenant
# Login failures by country in the last 7 days
curl "https://auth.yourdomain.com/v1/audit-logs/aggregate\
?type=user.login_failed\
&group_by=country\
&from=2024-01-08T00:00:00Z" \
  -H "Authorization: Bearer <admin_api_key>"

Response:

{
  "data": [
    { "country": "CN", "count": 412 },
    { "country": "RU", "count": 287 },
    { "country": "US", "count": 143 }
  ]
}

SDK usage

Node.js

import { createHelloJohnAdminClient } from '@hellojohn/node';

const client = createHelloJohnAdminClient({
  secretKey: process.env.HJ_SECRET_KEY!,
});

// Get recent failed logins
const logs = await client.auditLogs.list({
  type: 'user.login_failed',
  result: 'failure',
  from: new Date(Date.now() - 60 * 60 * 1000).toISOString(), // 1 hour ago
  limit: 50,
});

for (const entry of logs.data) {
  console.log(`${entry.created_at} — ${entry.actor.email} from ${entry.ip_address}`);
}

Go

logs, err := client.AuditLogs.List(ctx, &hellojohn.AuditLogListParams{
    Type:   "user.login_failed",
    Result: "failure",
    From:   time.Now().Add(-time.Hour),
    Limit:  50,
})

Querying the database directly (self-hosted)

On self-hosted deployments, audit logs are stored in the audit_logs table in PostgreSQL:

-- Failed logins by IP in the last 24 hours
SELECT
  ip_address,
  COUNT(*) AS attempts,
  MAX(created_at) AS last_attempt
FROM audit_logs
WHERE
  type = 'user.login_failed'
  AND created_at > NOW() - INTERVAL '24 hours'
GROUP BY ip_address
ORDER BY attempts DESC
LIMIT 20;

-- Most active users today
SELECT
  actor->>'email' AS email,
  COUNT(*) AS actions
FROM audit_logs
WHERE
  actor->>'type' = 'user'
  AND created_at > NOW() - INTERVAL '1 day'
GROUP BY actor->>'email'
ORDER BY actions DESC
LIMIT 10;

Useful indexes (automatically created by migrations):

CREATE INDEX idx_audit_logs_created_at ON audit_logs (created_at DESC);
CREATE INDEX idx_audit_logs_actor_id ON audit_logs ((actor->>'id'));
CREATE INDEX idx_audit_logs_type ON audit_logs (type);
CREATE INDEX idx_audit_logs_tenant_id ON audit_logs (tenant_id);
CREATE INDEX idx_audit_logs_ip ON audit_logs (ip_address);

On this page