Querying the audit trail and deciding pending approvals
Every sensitive action in an Organization leaves a trace, and some actions do not take effect until a second person signs off. This tutorial covers reading and exporting the audit trail, then walking through the approval decision itself.
Both areas require a session for a Member holding the right permission: reading the audit trail needs audit:read, and deciding approvals needs the role-management permission.
Filtering the audit trail
GET /v1/audit-events returns your Organization's events newest first. You never pass an organization id: the Organization is always the one bound to your session. Filter by any combination of event type, subject, and an occurred-at window.
const orgId = "018f2f3a-6000-7000-9c3a-1f2b3c4d5e6f";
const token = process.env.ROLLFUSE_SESSION_TOKEN;
const params = new URLSearchParams({
event_type: "role.granted",
subject_type: "member",
occurred_after: "2026-08-01T00:00:00Z",
occurred_before: "2026-09-01T00:00:00Z",
limit: "50",
});
const auditRes = await fetch(
`https://api.rollfuse.com/v1/audit-events?${params}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const { audit_events, limit, offset } = await auditRes.json();
audit_events.forEach((event) =>
console.log(event.event_type, event.subject_id, event.actor)
);
The response echoes back the pagination it actually applied, so if you omit limit you can read the default from the response rather than guessing it. Filtering by an inverted window (an occurred_after later than occurred_before) or a malformed timestamp is rejected outright, rather than silently returning nothing.
Exporting the trail
When you need the full trail for a compliance review rather than a paginated slice, export it. The endpoint honors the same filters as the listing call but skips pagination entirely, returning one document.
const exportRes = await fetch(
`https://api.rollfuse.com/v1/audit-events/export?format=csv&occurred_after=2026-08-01T00:00:00Z`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const csv = await exportRes.text();
format defaults to csv; pass format=json if you would rather parse it directly. In the CSV, each row's payload column holds the event's JSON document as a quoted field, so downstream tooling still needs to parse that column as JSON even though the outer file is CSV.
Deciding a pending approval
Some actions, like granting the admin role to a Member, do not apply immediately: they raise an ApprovalRequest instead. List the ones waiting on a decision:
const pendingRes = await fetch(
"https://api.rollfuse.com/v1/approval-requests",
{ headers: { Authorization: `Bearer ${token}` } }
);
const pending = await pendingRes.json();
Each entry carries requested_by_member_id and the target_member_id the change would apply to. Approve or reject by id, using a session that belongs to a Member other than the one who requested it:
const decisionRes = await fetch(
`https://api.rollfuse.com/v1/approval-requests/${pending[0].id}/approve`,
{ method: "POST", headers: { Authorization: `Bearer ${token}` } }
);
const decided = await decisionRes.json();
console.log(decided.status); // "approved"
Swap the path segment for reject to discard the request instead; rejecting never creates the role assignment the request was raised for. Two safeguards are enforced server-side: the deciding Member cannot be the one who requested the change, and a request that was already decided cannot be decided again. Both come back as a conflict rather than silently succeeding twice.