Reference
Audit Logs
Audit log events are append-only: an actor, a target, a time and enough context to answer who did this and from where. They are searchable in the dashboard, exportable on demand, and streamable into whichever tool the customer's security team already lives in. Nothing here can be edited or deleted through the API, which is the point.
The event object
The hard part of an audit log is not writing events, it is deciding which ones matter. A useful starting set is anything that changes access, anything that moves money, and anything that leaves the system as an export — those three cover most of what an auditor asks about, and none of them are high volume.
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier, prefixed event_. Assigned on write; you cannot supply one. |
| object | string | Always "audit_log_event". |
| organization_id | string | The tenant the event belongs to. Required — an event with no tenant cannot be exported to anybody. |
| action | string | What happened, in resource.verb form: invoice.exported, member.invited, connection.deactivated. |
| occurred_at | timestamp | RFC 3339, from your system. This is the time customers search on, not the time we received it. |
| version | integer | Schema version for the action. Increment it when you change the shape of targets or context. |
| actor | object | Who did it: id, type (user, api_key, system) and an optional name captured at the time. |
| targets | array | What it was done to. Each entry has an id, a type and an optional name. |
| context | object | Where from: location (IP), user_agent, and anything else you attach. |
| metadata | object | Free-form detail specific to the action. Searchable, but not indexed for range queries. |
Capture names, not just ids
An event that records only actor id reads as a row of identifiers two years later, after the person has left and the record has been deleted. Capture the display name at write time; it is a snapshot of what was true then, which is exactly what an audit log is for.
Actor types
Three actor types cover the cases that actually occur. Resist adding a fourth for convenience: a vague actor type is how an audit log turns into a stream of events nobody can attribute.
| Type | When it applies | Notes |
|---|---|---|
| user | A person acting in your interface | Include the user id so the event survives a name change. |
| api_key | An integration acting on the tenant's behalf | Record which key, never the key itself. |
| system | Your own scheduled work | Use it for expiries and automated cleanups, not as a catch-all. |
Endpoints
Paths are relative to https://api.paycux.com. The reference overview covers authentication.
| Method | Path | What it does |
|---|---|---|
| POST | /audit_logs/events | Write one event from your own application. |
| GET | /audit_logs/schemas | List the actions registered in this environment, with their target types. |
| POST | /audit_logs/schemas | Register an action and the shape of its targets and metadata. |
| POST | /audit_logs/exports | Start a CSV or JSONL export for a range, organization and optional filters. |
| GET | /audit_logs/exports/:id | Poll an export and collect the download URL once it is ready. |
Emit an event
Emit at the point the change is committed, not at the point the request arrives — an audit log that records attempts as if they were actions is worse than none. The call is fire-and-forget from your request path's point of view; do not block a user's response on it.
1curl -X POST https://api.paycux.com/audit_logs/events \2 -H "Authorization: Bearer sk_example_123456789" \3 -H "Content-Type: application/json" \4 -d '{5 "organization_id": "org_01HQ8ZK3M4N5P6R7S8T9V0W1X2",6 "event": {7 "action": "invoice.exported",8 "version": 1,9 "occurred_at": "2026-06-18T08:14:03.220Z",10 "actor": {11 "id": "user_01HQ8ZK3M4N5P6R7S8T9V0W1X2",12 "type": "user",13 "name": "Avery Lindqvist"14 },15 "targets": [{ "id": "invoice_2026_06", "type": "invoice", "name": "June 2026" }],16 "context": { "location": "203.0.113.24", "user_agent": "Mozilla/5.0" },17 "metadata": { "format": "csv", "row_count": 1420 }18 }19 }'
1HTTP 2012{3 "success": true,4 "id": "event_01HQ8ZK3M4N5P6R7S8T9V0W1X2"5}
List event schemas
Registering a schema is what makes an action searchable and translatable in the customer's view. An unregistered action is still accepted and stored, but it arrives as a bare string with no structure behind it.
1curl https://api.paycux.com/audit_logs/schemas \2 -H "Authorization: Bearer sk_example_123456789"
1HTTP 2002{3 "object": "list",4 "data": [5 {6 "object": "audit_log_schema",7 "action": "invoice.exported",8 "version": 1,9 "targets": [{ "type": "invoice" }],10 "actor_metadata": { "role": "string" },11 "metadata": { "format": "string", "row_count": "number" },12 "created_at": "2026-05-11T12:00:00.000Z"13 }14 ],15 "list_metadata": { "before": null, "after": null }16}
Register an event schema
Register each action once, at deploy time rather than at runtime. Versions are additive: publishing version 2 leaves version 1 events readable, which matters because an audit log you cannot read is a compliance liability rather than an asset.
1curl -X POST https://api.paycux.com/audit_logs/schemas \2 -H "Authorization: Bearer sk_example_123456789" \3 -H "Content-Type: application/json" \4 -d '{5 "action": "invoice.exported",6 "version": 1,7 "targets": [{ "type": "invoice" }],8 "metadata": { "format": "string", "row_count": "number" }9 }'
1HTTP 2012{3 "object": "audit_log_schema",4 "action": "invoice.exported",5 "version": 1,6 "targets": [{ "type": "invoice" }],7 "metadata": { "format": "string", "row_count": "number" },8 "created_at": "2026-05-11T12:00:00.000Z"9}
Start an export
Exports are asynchronous because the useful ranges are large. Start one, poll for it, then hand the customer the URL — or, better, stream events continuously and treat exports as the thing an auditor asks for once a year.
1curl -X POST https://api.paycux.com/audit_logs/exports \2 -H "Authorization: Bearer sk_example_123456789" \3 -H "Content-Type: application/json" \4 -d '{5 "organization_id": "org_01HQ8ZK3M4N5P6R7S8T9V0W1X2",6 "range_start": "2026-04-01T00:00:00.000Z",7 "range_end": "2026-06-30T23:59:59.999Z",8 "actions": ["invoice.exported", "member.invited"],9 "format": "csv"10 }'
1HTTP 2012{3 "object": "audit_log_export",4 "id": "audit_log_export_01HQ8ZK3M4N5P6R7S8T9V0W1X2",5 "state": "pending",6 "url": null,7 "created_at": "2026-07-01T09:00:00.000Z"8}
Retrieve an export
Poll with a backoff rather than a tight loop; a quarter of events for a busy tenant takes a while to assemble. The URL is time-limited and signed, so hand it to the customer's browser rather than storing it.
1curl https://api.paycux.com/audit_logs/exports/audit_log_export_01HQ8ZK3M4N5P6R7S8T9V0W1X2 \2 -H "Authorization: Bearer sk_example_123456789"
1HTTP 2002{3 "object": "audit_log_export",4 "id": "audit_log_export_01HQ8ZK3M4N5P6R7S8T9V0W1X2",5 "state": "ready",6 "url": "https://exports.paycux.com/audit_log_export_01HQ8ZK3.csv?expires=1782374400",7 "row_count": 18402,8 "created_at": "2026-07-01T09:00:00.000Z",9 "updated_at": "2026-07-01T09:02:41.117Z"10}
Errors
An event whose metadata does not match its registered schema returns 422 with the offending keys, so a schema change deployed ahead of the code that uses it fails loudly. An occurred_at far in the future is refused; one in the past is accepted, because backfills are legitimate.
Pagination
Schemas and exports are cursor paginated with limit, before and after. Events themselves are not read back through a list endpoint — use an export for a bounded range, or streaming for a continuous feed, both of which are built for volumes a paginated read would struggle with.
1// Disari aktarimi bekle — sabit araliksiz, geri cekilerek.2let wait = 1000;3let job = await paycux.auditLogs.getExport(exportId);45while (job.state === 'pending') {6 await new Promise((r) => setTimeout(r, wait));7 wait = Math.min(wait * 2, 30000);8 job = await paycux.auditLogs.getExport(exportId);9}1011return job.url;