The audit trail.
Workspace, member, role, policy and PAT operations write append-only audit rows. Read them for support, export them for SOC2, query them for incident response.
1
What gets captured
The workspace audit log records:
- Member lifecycle — invites created / accepted / revoked (
workspace.invite.*), members joined / removed / role-changed (workspace.member.*). - Role + policy mutations — custom role and managed-policy create / update / delete (
platform.role.*,platform.policy.*). - PAT lifecycle — mints, revokes, renames, policy-binding changes (
platform.apikey.*). - Workspace mutations — created / updated (display name, slug) / deleted (
workspace.*). - Sensitive reads — member, invite, settings and audit-log reads write rows too (e.g.
workspace.audit.read), so the log shows who looked, not just who changed.
What's NOT in workspace audit: per- service activity lives in the service's own log. Auth app changes → GET api.auth.productcraft.co/v1/apps/:app_id/audit-logs. Mail sends → Mail's message log. Social moderation → Social's moderation audit. A unified /v1/workspaces/:slug/audit-feed endpoint exists with a stable shape (see Section 3); today it serves the workspace rows, with the per-service sources arriving as a follow-up.
2
Read it
Requires workspace.audit.read. Newest first, cursor-paginated (limit up to 100 per page).
curl 'https://api.platform-auth.productcraft.co/v1/workspaces/acme/audit-logs?limit=50' \
-H 'authorization: Bearer <token-or-pak>'{
"data": [
{
"id": "...",
"workspace_id": "<workspace-uuid>",
"action": "workspace.member.role_changed",
"actor_id": "<owner-account>",
"actor_type": "platform_user",
"resource": "workspace_membership",
"resource_id": "<member-account>",
"metadata": { "role_id": "<role-uuid>", "role_name": "admin" },
"ip": "203.0.113.4",
"created_at": "2026-05-11T..."
},
{
"id": "...",
"workspace_id": "<workspace-uuid>",
"action": "platform.apikey.created",
"actor_id": "<owner-account>",
"actor_type": "platform_user",
"resource": "platform_api_key",
"resource_id": "<pak-id>",
"metadata": { "name": "github-actions-staging-deploy",
"requested_policy_ids": ["..."], "resolved_policy_ids": ["..."] },
"ip": "192.0.2.1",
"created_at": "2026-05-11T..."
}
],
"pagination": { "next_cursor": "...", "has_more": true }
}3
Query patterns
/audit-logs takes only limit + cursor — unknown query params are silently ignored, so don't rely on server-side filters that aren't documented here. Two patterns cover the common asks:
- Time-bounded reads — use the unified feed, which accepts
since(ISO-8601) andlimitup to 500:GET /v1/workspaces/:slug/audit-feed?since=2026-04-01T00:00:00Z. - Everything else — filter client-side. Page through and select with
jq, e.g..data[] | select(.action == "workspace.member.role_changed")for every role change, orselect(.resource == "platform_api_key")for all PAT lifecycle rows.
4
Export for compliance
For SOC2 / ISO27001 / annual review-style asks, paginate through the log and store it in your own object storage. The feed endpoint is the better export surface: it takes since and up to 500 rows per page.
#!/usr/bin/env bash
set -euo pipefail
WS=acme
SINCE="2026-01-01T00:00:00Z"
PAT="${PRODUCTCRAFT_PAT}"
BASE="https://api.platform-auth.productcraft.co/v1/workspaces/$WS/audit-feed"
CURSOR=""
PAGE=0
> audit-export.jsonl
while :; do
RESP=$(curl -fsS "$BASE?since=$SINCE&limit=500&cursor=$CURSOR" \
-H "authorization: Bearer $PAT")
echo "$RESP" | jq -c '.data[]' >> audit-export.jsonl
HAS_MORE=$(echo "$RESP" | jq -r '.pagination.has_more')
if [[ "$HAS_MORE" != "true" ]]; then break; fi
CURSOR=$(echo "$RESP" | jq -r '.pagination.next_cursor')
PAGE=$((PAGE+1))
echo "page $PAGE complete"
sleep 0.5
done
echo "Done. $(wc -l < audit-export.jsonl) rows."
# Need an upper bound too? Filter the export afterwards:
# jq -c 'select(.timestamp < "2026-07-01T00:00:00Z")' audit-export.jsonl5
Retention + workspace deletion
Audit rows are append-only — there is no delete or edit endpoint.
Deleting a workspace deletes its audit log with it. The rows live inside the workspace, so workspace deletion (Chapter 1) removes them immediately. If you need the trail for compliance, export it (Section 4) before deleting.
6
Common compliance asks + how to answer them
- “Who has access to our workspace?” — list members + PATs, plus their roles + policies. The console splits this across two pages: Workspace → Members (people, each with a role selector) and Workspace → API keys (PATs, each showing its bound-policy count and last-used date). For the policy statements behind a binding, use the API:
GET .../members+GET .../api-keys. - “Show me every action taken by the user we just offboarded.” — export, then
jq 'select(.actor_id == "<account>")'. Theirplatform.apikey.createdrows show which PATs they minted — revoke those too. - “What changed in the last 30 days?” —
/audit-feed?since=<30d-ago>. Group byactionfor the executive view, byactor_idfor the per-person view. - “Prove we revoked the leaked PAT.” — the
platform.apikey.revokedrow withresource_id = <pat-id>carries timestamp + actor + the IP that did the revoke.
What's next
You've finished the series
From here:
- Full API reference for every endpoint with schemas
- Auth guides for the end-user identity layer
- Mail guides for sender configuration + deliverability