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
| Parameter | Type | Description |
|---|---|---|
tenant_id | string | Filter to a specific tenant |
actor_id | string | Filter by actor (user, admin, or API key ID) |
actor_type | string | user, admin, api_key, system |
type | string | Event type, e.g. user.login. Supports wildcards: user.* |
result | string | success or failure |
ip_address | string | Filter by exact IP |
country | string | ISO 3166-1 alpha-2 country code |
from | ISO 8601 | Start of time range |
to | ISO 8601 | End of time range |
resource_id | string | Filter by affected resource ID |
resource_type | string | user, session, tenant, org, etc. |
limit | integer | Max results per page (default: 50, max: 200) |
cursor | string | Pagination 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>"MFA-related events for a tenant
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>| Parameter | Type | Description |
|---|---|---|
group_by | string | type, result, country, actor_id, hour, day |
type | string | Filter to a specific event type |
from | ISO 8601 | Start of time range |
to | ISO 8601 | End of time range |
tenant_id | string | Filter 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);