Reference
Audit Logs
The workspace ledger is append-only and hash-chained. Every control-plane write appends one row with the actor, the action, a document without secrets, the previous row's hash and its own. Two owner/admin routes read it and export it; nothing writes to it except the Paycux API itself. An ingestion route for your own application's events is planned and not part of this reference.
The event row
Rows are written in the same database transaction as the change they describe, so a write that fails leaves no row and a row that exists describes a write that happened. The fields below are what both routes return for each row.
| Field | Type | Description |
|---|---|---|
| id | string | UUID of the row, assigned on write. |
| scopeType | string | workspace, project or environment. The hash chain is kept per scope. |
| projectId | string | null | Set for the project and environment scopes; null in the workspace scope. |
| environmentId | string | null | Set for the environment scope; null otherwise. |
| sequence | integer | Position in the scope's chain. Starts at 1 and never skips. |
| actorType | string | user, system or agent. |
| actorId | string | null | The workspace member for console writes, the end-user identity for organisation writes, null for system writes. |
| action | string | What happened, such as catalog.plan.created or workspace_member.invited. Lower-case letters, digits, dots, underscores and dashes. |
| payload | object | The canonical event document that was hashed: action, actorId, actorType, environmentId, payload (the event's own data: ids, keys, roles, outcomes), projectId, scopeKey, scopeType, sequence and workspaceId, keys sorted. Provider keys, invite tokens and raw e-mail addresses are never written. |
| previousHash | string | SHA-256 hex of the previous row in the same scope; 64 zeros for the first row. |
| eventHash | string | SHA-256 hex over previousHash, a newline and the canonical JSON of payload. |
| createdAt | timestamp | When the row was written, RFC 3339. |
How the hash is computed
payload is serialised as JSON with its keys sorted recursively, and eventHash is SHA-256 over previousHash, a newline and that serialisation. The first row of a scope uses 64 zeros as previousHash. Rows are inserted under a per-scope lock, so two concurrent writers cannot produce two rows with the same sequence number.
Scopes
Each row belongs to exactly one scope, and the chain is kept per scope. A workspace with two environments therefore has at least three chains, each verified on its own. The scopeKey inside payload names the scope: workspace:<id>, project:<id> or environment:<id>.
| Scope | What lands there | Examples |
|---|---|---|
| workspace | Members, invites, workspace lifecycle, workspace billing, workspace switching, ledger exports | workspace_member.invited, workspace_member.role_changed, workspace.exported, audit.exported |
| project | Project creation | project.created |
| environment | Catalog, provider connections, organisation invites, application setup, customer billing actions | catalog.plan.created, provider_connection.created, organization_invite.revoked |
Actions
Action names are stable identifiers of the form entity.verb. The groups below list the actions the control plane writes today; the action filter of the read route takes any of them, or any prefix of them, verbatim.
| Group | Actions |
|---|---|
| Catalog | catalog.product.created, catalog.plan.created, catalog.price.created, catalog.feature.created, catalog.meter.created, catalog.price_mapping.created, catalog.plan_entitlement.created, catalog.plan_entitlement.updated, catalog.<entity>.archived, catalog.<entity>.restored |
| Workspace members | workspace_member.invited, workspace_member_invite.accepted, workspace_member_invite.revoked, workspace_member.role_changed, workspace_member.removed |
| Organisations | billing_account.created, organization_invite.created, organization_member.joined, organization_invite.revoked |
| Provider connections | provider_connection.created, provider_connection.disabled, provider_connection.enabled, provider_connection.secret_rotated, provider_connection.webhook_secret_attached |
| Workspace lifecycle | workspace.created, workspace.exported, workspace.deletion_requested, workspace.deletion_cancelled, dashboard.workspace_switched, audit.exported |
| Workspace billing | workspace_billing.checkout_created, workspace_billing.portal_opened, workspace_billing.plan_change_requested, workspace_billing.cancel_requested |
| Applications and agents | project.created, environment.created, application_setup.saved, agent.connection.mint_requested, agent.connection.revoke_requested |
The export document
The export is one JSON document: a summary per scope with its verification result, and the rows themselves. It is bounded so that a very large ledger is cut rather than streamed; the truncated flags say when that happened.
| Field | Type | Description |
|---|---|---|
| workspaceId | string | The workspace of the session; never a query value. |
| exportedAt | timestamp | When the export was built, RFC 3339. |
| truncated | boolean | The row bound (10,000 rows) was reached. Later scopes may be missing from scopes entirely. |
| scopes[] | array | One summary per scope: scopeKey, scopeType, projectId, environmentId, events (rows of the scope present in events), chainVerified and truncated. |
| scopes[].chainVerified | boolean | True only when every row of the scope is present and the hash chain recomputes. A truncated scope is always false. |
| events[] | array | The rows themselves, in the event row shape above, ascending by scopeKey and then sequence. |
Endpoints
Paths are relative to https://api.paycux.com. Both routes are dashboard member routes: they take the __Host-paycux_session cookie, require the owner or admin role, answer with cache-control: no-store and stay reachable while a workspace deletion request is open, when the console itself shows only the frozen-workspace view. The implemented API page covers the identities and how the session is issued.
| Method | Path | What it does |
|---|---|---|
| GET | /v1/workspace/audit-events | Page through the workspace ledger, newest first, with optional filters. |
| GET | /v1/workspace/audit-events/export | Download the bounded workspace ledger as a JSON attachment with chain verification per scope. |
List events
Query parameters: limit (1 to 100, default 50), cursor (the nextCursor of the previous page), scopeType (workspace, project or environment), projectId, environmentId and action. The action filter is a prefix match, so catalog. returns every catalog action and catalog.plan.created exactly one. Only owners and admins may call it; every other role receives 403 FORBIDDEN. The response is newest first, so walk it with the cursor rather than with sequence numbers.
1curl "https://api.paycux.com/v1/workspace/audit-events?scopeType=workspace&action=workspace_member.&limit=2" \2 --cookie "__Host-paycux_session=<session cookie>"
1HTTP 2002{3 "events": [4 {5 "id": "0b1c2d3e-4f50-4a6b-8c7d-8e9f0a1b2c3d",6 "scopeType": "workspace",7 "projectId": null,8 "environmentId": null,9 "sequence": 42,10 "actorType": "user",11 "actorId": "6d1f0b2e-3c4a-4d5e-8f60-7a8b9c0d1e2f",12 "action": "workspace_member.invited",13 "payload": {14 "action": "workspace_member.invited",15 "actorId": "6d1f0b2e-3c4a-4d5e-8f60-7a8b9c0d1e2f",16 "actorType": "user",17 "environmentId": null,18 "payload": {19 "inviteId": "e4d3c2b1-a098-4f7e-8d6c-5b4a39281706",20 "role": "developer",21 "expiresAt": "2026-10-19T09:14:03.220Z"22 },23 "projectId": null,24 "scopeKey": "workspace:3f9d2c1e-5b7a-4c8d-9e0f-1a2b3c4d5e6f",25 "scopeType": "workspace",26 "sequence": 42,27 "workspaceId": "3f9d2c1e-5b7a-4c8d-9e0f-1a2b3c4d5e6f"28 },29 "previousHash": "b9dfd30b27583211844dc26b5ff783142b869a733309c74682e7e6b3756bb13d",30 "eventHash": "677d9cde1f8189ef82bb1cea2d29457b6d0b2afe9a24bbdf279c97598971998e",31 "createdAt": "2026-10-12T09:14:03.220Z"32 },33 {34 "id": "2d3e4f50-6a7b-4c8d-9e0f-0a1b2c3d4e5f",35 "scopeType": "workspace",36 "projectId": null,37 "environmentId": null,38 "sequence": 41,39 "actorType": "user",40 "actorId": "6d1f0b2e-3c4a-4d5e-8f60-7a8b9c0d1e2f",41 "action": "workspace_member.role_changed",42 "payload": {43 "action": "workspace_member.role_changed",44 "actorId": "6d1f0b2e-3c4a-4d5e-8f60-7a8b9c0d1e2f",45 "actorType": "user",46 "environmentId": null,47 "payload": {48 "memberId": "9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",49 "previousRole": "viewer",50 "role": "developer"51 },52 "projectId": null,53 "scopeKey": "workspace:3f9d2c1e-5b7a-4c8d-9e0f-1a2b3c4d5e6f",54 "scopeType": "workspace",55 "sequence": 41,56 "workspaceId": "3f9d2c1e-5b7a-4c8d-9e0f-1a2b3c4d5e6f"57 },58 "previousHash": "5f11bab172b8450f9dc9dbb405a3853797612d1cc964c64097a1fe62707ef5da",59 "eventHash": "b9dfd30b27583211844dc26b5ff783142b869a733309c74682e7e6b3756bb13d",60 "createdAt": "2026-10-12T09:11:48.902Z"61 }62 ],63 "nextCursor": "<opaque cursor>"64}
Export the ledger
The export reads every scope of the workspace ascending by scopeKey and sequence, up to 10,000 rows, recomputes each hash and reports chainVerified per scope. true means the rows are complete and unaltered; false means a row was changed, removed or inserted out of order, or the bound cut the scope (truncated: true). Keep a failing file and contact support. The attachment is named paycux-audit-<workspace id>.json, the export is recorded in the workspace scope as audit.exported, and repeated exports are damped per member (429 RATE_LIMITED).
1curl "https://api.paycux.com/v1/workspace/audit-events/export" \2 --cookie "__Host-paycux_session=<session cookie>" \3 -o paycux-audit.json
1HTTP 2002cache-control: no-store3content-disposition: attachment; filename="paycux-audit-3f9d2c1e-5b7a-4c8d-9e0f-1a2b3c4d5e6f.json"45{6 "workspaceId": "3f9d2c1e-5b7a-4c8d-9e0f-1a2b3c4d5e6f",7 "exportedAt": "2026-10-12T11:30:00.000Z",8 "truncated": false,9 "scopes": [10 {11 "scopeKey": "workspace:3f9d2c1e-5b7a-4c8d-9e0f-1a2b3c4d5e6f",12 "scopeType": "workspace",13 "projectId": null,14 "environmentId": null,15 "events": 42,16 "chainVerified": true,17 "truncated": false18 },19 {20 "scopeKey": "environment:a1000000-0000-4000-8000-000000000003",21 "scopeType": "environment",22 "projectId": "a1000000-0000-4000-8000-000000000002",23 "environmentId": "a1000000-0000-4000-8000-000000000003",24 "events": 18,25 "chainVerified": true,26 "truncated": false27 }28 ],29 "events": [ "...60 rows in the event row shape, ascending by scopeKey and sequence..." ]30}
Errors
A missing or revoked session answers 401 with a DASHBOARD_SESSION error code. A developer, billing or viewer member answers 403 FORBIDDEN. A limit outside 1 to 100, a malformed cursor, a malformed id or an action filter with other characters answers 400 INVALID_INPUT. Repeated exports by the same member answer 429 RATE_LIMITED with a Retry-After header. Every response is JSON.
Pagination
The list route is keyset paginated: pass limit (1 to 100, default 50) and the nextCursor of the previous page as cursor until nextCursor is null. Cursors are opaque base64url strings; do not construct one. To verify a chain from the list route, collect one scope's rows and sort them by sequence ascending before recomputing. The export already returns them in that order.
1let cursor: string | null = null;2const events = [];34do {5 const url = new URL("https://api.paycux.com/v1/workspace/audit-events");6 url.searchParams.set("scopeType", "workspace");7 url.searchParams.set("limit", "100");8 if (cursor) url.searchParams.set("cursor", cursor);910 const page = await fetch(url, { headers: { cookie: sessionCookie } }).then((r) => r.json());11 events.push(...page.events);12 cursor = page.nextCursor;13} while (cursor);1415events.sort((a, b) => a.sequence - b.sequence);