API reference

Audit events

Every action in your workspace, by a person, a synthetic account, an API key or the system, is recorded as an audit event in the same database transaction as the action itself. When a call returns, its event already exists.

The event

AuditEvent (schema_version 1)
{
  "id": "aud_01J9Z3K4M5N6P7Q8R9S0T1V2W3",
  "object": "audit_event",
  "seq": 1042,
  "occurred_at": "2026-10-23T09:14:03.512Z",
  "action": "dispute.accept",
  "outcome": "success",
  "error_code": null,
  "actor": { "type": "synthetic_account", "id": "sa_…", "display": "agent-1", "on_behalf_of": null },
  "target": { "type": "dispute", "id": "dsp_…" },
  "parent_id": null,
  "changes": { "status": { "from": "evidence_required", "to": "accepted" } },
  "metadata": { "webhooks_scheduled": 1 },
  "request": {
    "id": "req_…", "correlation_id": "ci-4812:accept-dispute", "source": "portal",
    "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 …",
    "method": "POST", "path": "/v1/disputes/:id/accept"
  },
  "schema_version": 1
}
FieldMeaning
seqPer-workspace sequence number: gapless, strictly increasing, in commit order. Use it to read “everything after this point”.
action<resource>.<verb>, one of the actions below. The verb never says whether it worked; outcome does.
error_codeThe same lowercase code the API answered with, on denied and failed rows.
targetThe resource acted on, by display id (dsp_…, wh_…), never an internal UUID.
parent_idSet on the children of a batch (dispute.bulk_decide, dispute.apply_playbook) and of a lifecycle run.
changesOnly the fields the action declares, as {field: {from, to}}.
metadataOnly the keys (and types) the action declares, plus the common keys below.
request.pathThe route template, with parameters named as in the OpenAPI document (/v1/team/synthetic-accounts/:display_id), never the concrete path or query string.

Secrets never reach the trail: passwords, codes, tokens, keys, signing secrets, signed URLs and e-mail addresses (except a person's own, as actor) are redacted before a row is written.

Outcomes, sources and actors

outcomeWhen
successThe action happened.
deniedA permission or scope check refused it (missing_permission, missing_scope).
failedIt was attempted and failed with a 4xx or 5xx, recorded with its error_code. Plain validation errors (400 validation_failed) and idempotency conflicts are not recorded.

Repeated identical refusals (same actor, action, error and correlation id) within a minute are folded into one row whose metadata.coalesced_count says how many there were; the first one is always written at once.

request.sourceMeaning
portalSomeone clicked in the portal.
apiA direct API call (an API key, or a bearer token outside the portal).
systemThe hourly lifecycle run and other scheduled work.
actor.typeactor.idNotes
userend-user idA person in the workspace: an owner or a colleague. display is their e-mail, shown only with audit.read_sensitive. A person who has since been removed keeps their e-mail on their old rows.
synthetic_accountsa_…A synthetic account from Team > Synthetic accounts; display is the username it signs in with (GET /v1/whoami returns it), not its label. Until the account has signed in once, and on events written before 25 October 2026, it is the label.
api_keytak_…The key itself, never its creator; display is the key's name and on_behalf_of its creator.
systemnullon_behalf_of names the job (lifecycle, generate).

Work a request starts later, such as a generate job, a lifecycle run or a webhook delivery, is recorded as the systemactor but keeps the request's correlation_id, so a test still finds it. The hourly lifecycle run has none.

Actions

The 116 actions below are the whole catalogue, generated from the code that writes them; the same list, with the same declarations, is served by GET /v1/audit-events/actions. async actions are written after the response (a job, a delivery, a scheduled run): wait for them with /wait instead of reading them at once.

ActionTriggered byTargetDeclared fields

auth.signup

An end user signed up for dispute.markets.

  • POST /v1/auth/signup
user
metadata:
mfa_required boolean

tenant.create

A workspace was created; the person who created it is its first owner. source: signup (the sandbox a new sign-up gets), bootstrap (an account that had none) or create (POST /v1/workspaces); copied_settings_from names the workspace whose default currency and time zone it started with.

  • POST /v1/auth/signup
  • POST /v1/auth/bootstrap
  • POST /v1/workspaces
tenant
changes:
name, company
metadata:
source string, copied_settings_from string, environment_label string

tenant.copy_settings

An owner created a new workspace (target_workspace_id) starting from this workspace's default currency and time zone (fields). Written in this, the source, workspace; nothing here changed.

  • POST /v1/workspaces
tenant
metadata:
target_workspace_id string, fields string[]

auth.switch_workspace

A person switched their session into this workspace (from_workspace_id: the workspace the session was in, absent when it had none). Written in the workspace switched to. outcome: failed (workspace_not_found, workspace_switch_not_allowed) is written in the workspace the session was in.

  • POST /v1/auth/switch-workspace
tenant
metadata:
from_workspace_id string

auth.signin

Password sign-in. success when the password was right (mfa_required when a challenge follows); failed (invalid_credentials) only for an identifier that is a tenant member, written just after the response and coalesced per identifier per minute. Only success rows carry the caller's correlation id; a failed attempt cannot be found by it.

  • POST /v1/auth/signin
user
metadata:
mfa_required boolean, method string

auth.bootstrap

A signed-in end user bootstrapped or re-entered their tenant. denied (already_a_member) when a team member (synthetic account) tried to create a workspace; written in the workspace it belongs to.

  • POST /v1/auth/bootstrap
tenant
metadata:
created boolean

auth.session_expire

A synthetic account's access token was refused because it is older than the session.max_age flow variant allows (outcome=failed, error_code=session_expired; the request answered 401). Written exactly once per token (the first refused request, on whichever server). max_age_s is the limit that applied.

  • Key check (any key route)
synthetic_account
metadata:
max_age_s number

auth.refresh

Token refresh; recorded only when it fails and the refresh token was issued for a known workspace.

  • POST /v1/auth/refresh
usernone

mfa.send_code

An MFA code was e-mailed: for a sign-in challenge (the challenge must come from a sign-in this API attributed to the tenant), or to a signed-in user for a step-up.

  • POST /v1/auth/mfa/send-code
  • POST /v1/auth/mfa/step-up/send-code
user
metadata:
factor_type string

mfa.verify

The MFA challenge of a sign-in was answered. failed rows (wrong code) are written just after the response and coalesced per challenge per minute.

  • POST /v1/auth/mfa/verify
user
metadata:
factor_type string

mfa.recover

A recovery code was used to finish a sign-in. failed rows as for mfa.verify.

  • POST /v1/auth/mfa/recover
user
metadata:
recovery_codes_remaining number

mfa.step_up

A signed-in user re-proved MFA (step-up). A wrong code (401) is failed with error_code invalid_code (step_up_rejected for any other 401 from Auth), coalesced per user per minute.

  • POST /v1/auth/mfa/step-up
user
metadata:
factor_type string

mfa_factor.create

An MFA factor was enrolled (pending verification).

  • POST /v1/auth/mfa/factors
mfa_factor
metadata:
factor_type string

mfa_factor.verify

A pending MFA factor was verified and activated.

  • POST /v1/auth/mfa/factors/:id/verify
mfa_factor
metadata:
factor_type string

mfa_factor.delete

An MFA factor was removed.

  • DELETE /v1/auth/mfa/factors/:id
mfa_factor
metadata:
factor_type string

recovery_codes.regenerate

MFA recovery codes were regenerated (the codes are never recorded).

  • POST /v1/auth/mfa/recovery-codes/regenerate
user
metadata:
count number

email_verification.resend

The e-mail verification message was sent again (sent: false when the account has no e-mail).

  • POST /v1/auth/verify-email/resend
user
metadata:
sent boolean

email_verification.confirm

An e-mail address was verified with its code.

  • POST /v1/auth/verify-email/confirm
user
changes:
email_verified

password.forgot

A password reset was requested for a tenant member's e-mail. Written just after the response, coalesced per address per minute; the caller proved nothing, so the correlation id is only metadata.unverified_correlation_id.

  • POST /v1/auth/forgot-password
usernone

password.reset

A password was reset with a reset code. Not recorded yet: a reset code does not say which account or workspace it belongs to, so the event cannot be placed in a workspace's trail.

  • POST /v1/auth/reset-password
usernone

user_email.create

An e-mail address was added to the signed-in user, or its code re-sent (existing); sent: false when it was already verified. The address itself is never recorded.

  • POST /v1/me/emails
user_email
metadata:
sent boolean, existing boolean

user_email.verify

A secondary e-mail address was verified.

  • POST /v1/me/emails/verify
user_emailnone

user_email.delete

A secondary e-mail address was removed.

  • DELETE /v1/me/emails/:id
user_emailnone

synthetic_account.create

A synthetic account was provisioned (credentials are never recorded).

  • POST /v1/team/synthetic-accounts
synthetic_account
changes:
label, role, flow_profile
metadata:
mfa_enabled boolean, mfa_method string, required_permission string, credential_mode string

synthetic_account.update

A synthetic account's label, role, notes or flow profile changed (flow_profile as {from, to} refs, only when it changed). denied with required_permission: flow.write when flow_profile was sent without that permission.

  • PATCH /v1/team/synthetic-accounts/:display_id
synthetic_account
changes:
label, role, notes, flow_profile
metadata:
required_permission string

synthetic_account.flow_profile_releasedasync

A synthetic account's flow profile assignment was cleared because the profile was archived more than 24 hours ago (system; changes.flow_profile {from, to: null}, reason: profile_archived). Written by the release job scheduled at archive time, or by the hourly flow retention for leftovers.

  • Background job
  • Hourly lifecycle run
synthetic_account
changes:
flow_profile
metadata:
reason string

synthetic_account.rotate_password

A synthetic account's password was rotated (also written by rotate-credentials when the password rotated but the MFA rotation then failed). On an email-mode account this switches it back to a generated password (changes.credential_mode) and revokes any unused setup link (setup_link_revoked).

  • POST /v1/team/synthetic-accounts/:display_id/rotate-password
  • POST /v1/team/synthetic-accounts/:display_id/rotate-credentials
synthetic_account
changes:
credential_mode
metadata:
rotated string[], setup_link_revoked boolean

synthetic_account.rotate_mfa

A synthetic account's TOTP secret was rotated.

  • POST /v1/team/synthetic-accounts/:display_id/rotate-mfa
synthetic_account
metadata:
rotated string[], mfa_method string

synthetic_account.rotate_credentials

A synthetic account's password and TOTP secret were rotated together. On an email-mode account this switches it back to a generated password (changes.credential_mode) and revokes any unused setup link (setup_link_revoked).

  • POST /v1/team/synthetic-accounts/:display_id/rotate-credentials
synthetic_account
changes:
credential_mode
metadata:
rotated string[], mfa_method string, setup_link_revoked boolean

synthetic_account.disable_mfa

MFA was turned off for a synthetic account.

  • DELETE /v1/team/synthetic-accounts/:display_id/mfa
synthetic_account
changes:
mfa_enabled
metadata:
factors_disabled number

synthetic_account.revoke_sessions

Every session of a synthetic account was revoked (owner only; a security action): its next request with an access token issued before this answers 401 session_revoked, and its refresh tokens stop working at the auth service too (auth_sessions_revoked; false when that call failed and only the access-token cut-off applies). A new sign-in works.

  • POST /v1/team/synthetic-accounts/:display_id/revoke-sessions
synthetic_account
metadata:
auth_sessions_revoked boolean

synthetic_account.expire_sessions

A synthetic account's sessions were expired on purpose (flow.write; a simulation for testing the client's expiry handling): its next request with a token issued before this answers 401 session_expired. A new sign-in works.

  • POST /v1/team/synthetic-accounts/:display_id/expire-sessions
synthetic_accountnone

synthetic_account.delete

A synthetic account was archived and its sign-in account removed; setup_link_revoked when an unused setup link stopped working with it. reason: mail_owner_departed when it was archived as a disposition of removing or demoting the person its mail went to (docs/15 §5.5).

  • DELETE /v1/team/synthetic-accounts/:display_id
  • DELETE /v1/team/members/:id
  • PATCH /v1/team/members/:id
  • POST /v1/me/workspaces/:id/leave
synthetic_account
metadata:
already_archived boolean, setup_link_revoked boolean, reason string

synthetic_account.send_setup_email

A setup link was emailed to an email-mode synthetic account (docs/14): at creation (reason: create), on a resend (resend), a restart that signed the account out first (restart), an address change (email_change) or a switch from a generated password (mode_change, with changes.credential_mode). credentials_revoked says whether the password, sessions and MFA factors were revoked first; delivery is queued or send_failed (Envoi refused or was unreachable; the link was not delivered). outcome: failed with setup_email_rate_limited, setup_pending_limit, setup_email_suppressed or setup_email_unavailable when nothing was sent. Neither the link, the base address nor the derived address is recorded; email_changed says whether the address changed.

  • POST /v1/team/synthetic-accounts
  • POST /v1/team/synthetic-accounts/:display_id/email-setup
synthetic_account
changes:
credential_mode
metadata:
reason string, expires_at string, email_changed boolean, credentials_revoked boolean, delivery string

synthetic_account.complete_setup

A setup link was used to set an email-mode synthetic account's password (changes.credential_status {from: pending, to: set}; mfa_method names the factor provisioned with it, or null). The actor is the synthetic account. outcome: failed with error_code (setup_link_used, setup_link_expired, setup_link_revoked, password_rejected, mfa_setup_failed, credential_change_in_progress) when the link maps to an account; an unknown link is not attributable and writes no row.

  • POST /v1/auth/account-setup/complete
synthetic_account
changes:
credential_status
metadata:
mfa_method string

synthetic_account.setup_email_bouncedasync

A setup mail could not be delivered: the address it was sent to is on the dispute.markets mail suppression list (a hard bounce or a complaint was recorded), found by the synthetic_account.setup_delivery_check job 5 or 30 minutes after the send (system; source: suppression_list, checked_at the time of the lookup). The link is revoked and the account's setup.status reads bounced until a link is sent to a different address; a resend to the same address answers 409 setup_email_suppressed without a send. The address is not recorded.

  • Background job
synthetic_account
metadata:
source string, checked_at string

workspace_invite.create

An owner invited someone to the workspace by e-mail (role, the invited email, shown to owners only; expires_at; delivery is sent or send_failed; replaces_expired_invite_id when the address had an expired invite, which this one replaces). The link is never recorded. outcome: failed with error_code when nothing was sent: invite_rate_limited, invite_pending_limit, member_limit_reached, invite_undeliverable, invite_paused, invite_unavailable, invite_pending, already_a_member, synthetic_address, step_up_required, email_not_verified.

  • POST /v1/team/invites
workspace_invite
metadata:
role string, email string, expires_at string, delivery string, replaces_expired_invite_id string

tenant.delete

An owner deleted the workspace (DELETE /v1/workspaces/:id). From this row on every member's next request here answers 401 account_revoked, its API keys answer 401 invalid_api_key, its webhooks are disabled (webhooks_disabled), its scheduled jobs and background tasks are canceled (jobs_canceled, tasks_canceled), its pending invites are cancelled (invites_revoked) and every member was removed (members_removed, removal_reason: workspace_deleted). The counts are what the confirm dialog showed (transactions, disputes, reports, webhooks, people, synthetic_accounts, api_key_count; api_key_count absent when Auth could not be asked). step_up says whether a second factor from the last 5 minutes was required (an empty workspace whose only member is the caller needs none). The data is purged after purge_after (30 days; tenant.purge follows). outcome: failed with confirm_name_mismatch, step_up_required.

  • DELETE /v1/workspaces/:id
tenant
changes:
deleted_at
metadata:
transactions number, disputes number, reports number, webhooks number, people number, synthetic_accounts number, api_key_count number, members_removed number, invites_revoked number, webhooks_disabled number, jobs_canceled number, tasks_canceled number, step_up boolean, purge_after string, environment_label string

tenant.purgeasync

A deleted workspace's data was purged, 30 days after tenant.delete (system, on_behalf_of: workspace_purge): every row of every table except this audit trail (rows_deleted, files_deleted evidence and report files), the Auth accounts of its synthetic accounts (synthetic_accounts_deleted) and the Auth tenant (auth_tenant: deleted, or absent when it was already gone). The last row ever written for the workspace.

  • Background job
tenant
metadata:
rows_deleted number, files_deleted number, synthetic_accounts_deleted number, auth_tenant string, batches number

workspace_member.reconcileasync

The hourly reconcile (system, on_behalf_of: membership_reconcile) compared the workspace's people and synthetic accounts with the authentication service, which only ever narrows access. correction: auth_removed (an Auth membership without a live member here was removed), auth_narrowed (an Auth role broader than the role here was set to it, auth_role → role) or account_deleted (the person's Auth account was deleted: their row here was removed and they no longer count as an owner). drift (alert only, nothing changed): auth_membership_missing (a live member here has no Auth membership; an operator may have removed it on purpose) or auth_role_narrower (the Auth role grants less than the role here). The target is the member (wm_… / sa_…) when one exists here.

  • Hourly lifecycle run
workspace_member
metadata:
correction string, drift string, account_id string, role string, auth_role string, kind string

workspace_invite.resend

An owner sent an invite again: a new link (this row's target, send_count the sends to that address so far) replaces the previous one (previous_invite_id), which stops working. delivery as on create.

  • POST /v1/team/invites/:id/resend
workspace_invite
metadata:
send_count number, previous_invite_id string, expires_at string, delivery string, email string

workspace_invite.revoke

A pending invite was cancelled: by an owner (reason: revoked), because the owner who sent it stopped being an owner (inviter_lost_owner, written by the removal or role change, or by the hourly reconcile when their Auth account was deleted), because its mail bounced (bounced, system, found by the delivery check 5 or 30 minutes after the send), or because the workspace was deleted (workspace_deleted).

  • DELETE /v1/team/invites/:id
  • DELETE /v1/team/members/:id
  • PATCH /v1/team/members/:id
  • POST /v1/me/workspaces/:id/leave
  • DELETE /v1/workspaces/:id
  • Background job
  • Hourly lifecycle run
workspace_invite
metadata:
reason string, email string

workspace_invite.pausedasync

A recipient marked an invite as spam (an SES complaint, found by the delivery check): the owner who sent it cannot invite anyone, in any workspace, until paused_until (24 hours). System row in the workspace of the invite.

  • Background job
workspace_invite
metadata:
paused_until string

workspace_invite.accept

Someone joined the workspace with an invite (role; created_account when they signed up for it). The actor is the person who joined. outcome: failed with error_code (invite_email_mismatch, synthetic_account_not_allowed, already_a_member, membership_limit_reached, member_limit_reached, invite_expired, invite_used, invite_revoked, auth_unavailable) when the invite was found but the join refused; coalesced per person per minute.

  • POST /v1/auth/invites/accept
  • POST /v1/me/invites/:id/accept
workspace_invite
metadata:
role string, created_account boolean, email string

workspace_member.update

An owner changed a person's role (changes.role). Demoting an owner also cancels the invites they sent (invites_revoked) and carries the dispositions of the synthetic accounts mailed to them or whose credentials they knew (synthetic_dispositions, sa_…:move_to_me|rotate|archive). outcome: failed with last_owner when it would leave the workspace without an owner, or step_up_required, disposition_required.

  • PATCH /v1/team/members/:id
workspace_member
changes:
role
metadata:
email string, invites_revoked number, synthetic_dispositions string[]

workspace_member.remove

An owner removed a person from the workspace: their next request here answers 401 account_revoked, their API keys stop working (api_keys_revoked), the invites they sent are cancelled (invites_revoked) and their running background tasks here are cancelled (tasks_canceled). What they left behind follows the dispositions: synthetic accounts mailed to them or whose credentials they knew (synthetic_dispositions, sa_…:move_to_me|rotate|archive), and the webhooks, webhook sinks and playbooks they created (webhooks_disabled/webhooks_kept, sinks_disabled/sinks_kept, playbooks_disabled/playbooks_kept); each item also gets its own row. outcome: failed with last_owner, step_up_required or disposition_required.

  • DELETE /v1/team/members/:id
workspace_member
metadata:
email string, role string, api_keys_revoked number, invites_revoked number, tasks_canceled number, synthetic_dispositions string[], webhooks_disabled number, webhooks_kept number, sinks_disabled number, sinks_kept number, playbooks_disabled number, playbooks_kept number

workspace_member.leave

A person left the workspace. Their next request here answers 401 account_revoked (their sign-in and their other workspaces are untouched), their API keys here stop working, the invites they sent are cancelled (invites_revoked) and their running background tasks here are cancelled (tasks_canceled). Synthetic accounts mailed to them were archived, and those whose credentials they knew were archived or had their credentials reset (synthetic_dispositions, sa_…:rotate|archive); the webhooks, webhook sinks and playbooks they created are disabled (webhooks_disabled, sinks_disabled, playbooks_disabled), each with its own row. outcome: failed with last_owner (the last owner cannot leave) or synthetic_mail_owner (synthetic accounts are still mailed to them).

  • POST /v1/me/workspaces/:id/leave
workspace_member
metadata:
email string, role string, invites_revoked number, tasks_canceled number, synthetic_dispositions string[], webhooks_disabled number, sinks_disabled number, playbooks_disabled number

synthetic_account.reset_credentials

A synthetic account's credentials were reset to ones nobody knows because a person who saw or set its password, or whose mailbox received its mail, was removed, left or stopped being an owner (docs/15 §5.6). Its password was replaced (nobody is shown the new one), its sessions were ended at the authentication service (auth_sessions_revoked) and its access tokens answer 401 session_revoked; any unused setup link stopped working (setup_link_revoked). The account keeps its role and history: an owner rotates its credentials (generated mode) or sends a new setup link (email mode) to use it again. reason: creator_departed (someone who knew the credentials left) or mail_owner_departed (its mail moved to the acting owner).

  • DELETE /v1/team/members/:id
  • PATCH /v1/team/members/:id
  • POST /v1/me/workspaces/:id/leave
synthetic_account
metadata:
reason string, credential_mode string, setup_link_revoked boolean, auth_sessions_revoked boolean

synthetic_account.move_mail

An owner moved a synthetic account's mail to themselves: its sign-in address is re-derived on one of their verified e-mails (email_changed; an address on the agents.dispute.markets sink keeps its address and only changes hands), any unused setup link stops working (setup_link_revoked), and setup, reset and sign-in codes go to their mailbox from now on. reason: mail_owner_departed when it was a disposition of a removal or demotion, requested from the row action.

  • POST /v1/team/synthetic-accounts/:display_id/move-mail
  • DELETE /v1/team/members/:id
  • PATCH /v1/team/members/:id
synthetic_account
metadata:
reason string, email_changed boolean, setup_link_revoked boolean

webhook.revouch

A webhook created by a person who left the workspace was kept: an owner vouched for it (reason: creator_departed); it keeps sending.

  • DELETE /v1/team/members/:id
webhook
metadata:
reason string

playbook.revouch

A playbook created by a person who left the workspace was kept: an owner vouched for it (reason: creator_departed); it keeps running.

  • DELETE /v1/team/members/:id
playbook
metadata:
reason string

webhook_sink.revouch

A webhook sink created by a person who left the workspace was kept: an owner vouched for it (reason: creator_departed).

  • DELETE /v1/team/members/:id
webhook_sink
metadata:
reason string

dispute.accept

The merchant accepted the dispute (no challenge).

  • POST /v1/disputes/:id/accept
dispute
changes:
status, outcome
metadata:
reason_code string, note_present boolean, webhooks_scheduled number, batch_id string, requested_dispute_id string, simulated_timeout boolean

dispute.challenge

The merchant challenged the dispute with evidence.

  • POST /v1/disputes/:id/challenge
dispute
changes:
status
metadata:
reason_code string, note_present boolean, webhooks_scheduled number, batch_id string, requested_dispute_id string, simulated_timeout boolean, pending boolean, completes_at string, job_id string, evidence_count number

dispute.respond_rfi

The merchant answered a request for information.

  • POST /v1/disputes/:id/rfi-response
dispute
changes:
status
metadata:
reason_code string, note_present boolean, webhooks_scheduled number, batch_id string, requested_dispute_id string, simulated_timeout boolean

dispute.arbitrate

The merchant escalated the dispute to arbitration.

  • POST /v1/disputes/:id/arbitrate
dispute
changes:
status
metadata:
reason_code string, note_present boolean, webhooks_scheduled number, batch_id string, requested_dispute_id string, simulated_timeout boolean, pending boolean, completes_at string, job_id string

dispute.add_note

A note was added to the dispute (the text is not recorded).

  • POST /v1/disputes/:id/notes
dispute
metadata:
length number, webhooks_scheduled number, batch_id string, requested_dispute_id string

dispute.add_to_decline_list

The dispute's card or customer was added to the decline list.

  • POST /v1/disputes/:id/decline-list
dispute
metadata:
webhooks_scheduled number

dispute.request_support

A support case was opened for the dispute.

  • POST /v1/disputes/:id/support-case
dispute
metadata:
webhooks_scheduled number

dispute.bulk_decide

One decision applied to many disputes; parent of one child row per dispute.

  • POST /v1/disputes/bulk
—
metadata:
decision string, requested number, succeeded number, failed number, webhooks_scheduled number

dispute.apply_playbook

A playbook was applied to disputes; parent of one child row per dispute.

  • POST /v1/disputes/apply-playbook/:playbook_id
playbook
metadata:
requested number, matched number, applied number, webhooks_scheduled number

evidence.create_upload_url

A presigned upload URL was issued for dispute evidence (the URL is not recorded).

  • POST /v1/disputes/:id/evidence/upload-url
dispute
metadata:
filename string, content_type string, size_bytes number

evidence.create

An uploaded evidence file was attached to the dispute. status is available, or processing under the evidence.processing flow variant (dispute-api/037; processing_until and job_id name its evidence.process job). failed with evidence_too_large (over the 25 MB platform cap or evidence.limits.max_bytes), evidence_type_not_allowed or evidence_limit_reached (evidence.limits) when the upload was refused and the stored file deleted.

  • POST /v1/disputes/:id/evidence
evidence
metadata:
dispute_id string, content_type string, size_bytes number, kind string, filename string, sha256 string, webhooks_scheduled number, status string, processing_until string, job_id string

evidence.processasync

An evidence file uploaded under the evidence.processing flow variant (dispute-api/037) finished processing: changes.status from processing to available, or to rejected with rejection_reason (virus_detected, unsupported_type, unreadable; the stored file was deleted). processing_canceled / processing_failed when its evidence.process job was canceled or failed for good. Actor system, on_behalf_of the uploader, with the upload's correlation id. webhook_enabled: the dispute has lifecycle.webhooks enabled, so an evidence.processed webhook went to its simulation subscribers.

  • Background job
  • POST /v1/scheduled-jobs/cancel
  • POST /v1/scheduled-jobs/:id/cancel
evidence
changes:
status
metadata:
dispute_id string, rejection_reason string, job_id string, file_deleted boolean, webhook_enabled boolean

report.create

A report was created. Under report.limits (dispute-api/060) it carries limit and live_count (the run's live reports including this one; on a refused create, failed + report_limit_reached, the count that refused it). Synchronous (the default): built during the request (failed + report_too_large above 100,000 rows, report_build_failed when the build throws, or simulated_<code> for the code a report.outcome flow variant chose). Async (report.delivery {mode: "async"}, delivery: async): queued, rows null; its report.build row follows.

  • POST /v1/reports
report
metadata:
kind string, rows number, range_days number, delivery string, limit number, live_count number

report.delete

A report was deleted (a tombstone; reads answer 410 report_deleted). status_at_delete is the status it had; canceled_job_id the queued report.build job the delete canceled (null when none). A repeat delete answers 200 and writes this row again with already_deleted: true and no changes. A report.purge row follows once the file is gone. flow carries the request's report.limits when one applied.

  • DELETE /v1/reports/:id
report
changes:
deleted_at
metadata:
status_at_delete string, canceled_job_id string, already_deleted boolean

report.purgeasync

A deleted report's file was removed from storage (system actor, on_behalf_of the deleter, with the delete's correlation id). object_existed is false when there was no file (a queued, failed or expired report).

  • Background job
report
metadata:
object_existed boolean

report.buildasync

An async report finished building (system actor, on_behalf_of its creator, with the creator's correlation id): success (done), or failed with the report's error_code (simulated_report_timeout / simulated_report_too_large / simulated_internal when report.outcome simulated the failure, report_too_large, report_build_failed, canceled, job_lease_expired). flow carries the stamped variants.

  • Background job
report
changes:
status
metadata:
rows number, attempt number, truncated boolean

report.retry

A failed report was run again (changes.attempt). delivery: sync: built during the request, the row's outcome is the build's; delivery: async: queued again, a report.build row follows.

  • POST /v1/reports/:id/retry
report
changes:
attempt
metadata:
delivery string, rows number

playbook.create

A playbook was created.

  • POST /v1/playbooks
playbook
changes:
name, enabled, rules
metadata:
rule_count number, created_by_api_key boolean

playbook.update

A playbook changed.

  • PATCH /v1/playbooks/:id
playbook
changes:
name, enabled, rules
metadata:
rule_count number

playbook.delete

A playbook was deleted.

  • DELETE /v1/playbooks/:id
playbooknone

playbook.dry_run

A playbook was evaluated without applying it.

  • POST /v1/playbooks/:id/dry-run
playbook
metadata:
matched number, evaluated number

playbook.disableasync

A playbook was disabled: one created by an API key because the key expired or was revoked (system, api_key_id), or one created by a person who was removed from the workspace (reason: creator_departed, the remover's disposition).

  • Background job
  • DELETE /v1/team/members/:id
  • POST /v1/me/workspaces/:id/leave
playbook
changes:
enabled
metadata:
reason string, api_key_id string

webhook.create

A webhook endpoint was registered (the signing secret is never recorded).

  • POST /v1/webhooks
webhook
changes:
url, events, enabled, accepts_simulation, filter
metadata:
created_by_api_key boolean, events string[], host string

webhook.update

A webhook endpoint changed.

  • PATCH /v1/webhooks/:id
webhook
changes:
url, events, enabled, accepts_simulation, filter
metadata:
host string, previous_host string

webhook.delete

A webhook endpoint was deleted.

  • DELETE /v1/webhooks/:id
webhooknone

webhook.test

A test event was sent to one webhook endpoint (only that webhook: webhooks_scheduled is always 1). failed with webhook_delivery_failed when the endpoint did not answer 2xx, or webhook_not_found (404) for an unknown id.

  • POST /v1/webhooks/:id/test
webhook
metadata:
status_code number, delivered boolean, delivery_id string, host string, webhooks_scheduled number

webhook.disableasync

A webhook was disabled: one created by an API key because the key expired or was revoked (system, api_key_id), one created by a person who was removed from the workspace (reason: creator_departed, the remover's disposition), or every enabled webhook of a workspace being deleted (reason: workspace_deleted).

  • Background job
  • DELETE /v1/team/members/:id
  • POST /v1/me/workspaces/:id/leave
  • DELETE /v1/workspaces/:id
webhook
changes:
enabled
metadata:
reason string, api_key_id string

generate_job.create

A synthetic-data generate job was started. trickle_batches: with generate.trickle, how many batches (a minute apart) the job will run (dispute-api/038).

  • POST /v1/dev/generate
generate_job
metadata:
count number, dispute_rate number, currency string, range_days number, seed number, lifecycle string, trickle_batches number

generate_job.runasync

A generate job finished: success when its last chunk committed, failed with generate_failed (a chunk kept failing), job_lease_expired (a chunk was interrupted too often) or canceled. Never the exception text. With lifecycle.webhooks enabled, webhooks_fired / webhooks_skipped count the dispute.created webhooks (at most 200 per job); lifecycle_fallback_auto counts disputes left auto because the workspace scheduled 20,000 lifecycle transitions today (dispute-api/033); trickle_batches counts the batches of a generate.trickle job (dispute-api/038).

  • Background job
generate_job
changes:
status
metadata:
transactions number, disputes number, duration_ms number, chunks number, webhooks_fired number, webhooks_skipped number, lifecycle_fallback_auto number, trickle_batches number

generate_job.cleanupasync

Exactly one generate job's data was deleted. Written with the response for a synchronous cleanup (at most 2,000 transactions), else by the last chunk of the generate.cleanup background task (202), with the totals and chunks; failed with the stable code if that task failed or was canceled.

  • POST /v1/dev/generate-jobs/:id/cleanup
  • Background job
generate_job
metadata:
transactions number, disputes number, dispute_events number, evidence number, payment_events number, chunks number

generate_job.cancel

A generate job was canceled: its remaining chunks stopped and it ended canceled, keeping the transactions already committed (rows_done). canceled: false when it had already finished (no change).

  • POST /v1/dev/generate-jobs/:id/cancel
generate_job
changes:
status
metadata:
rows_done number, canceled boolean

scheduled_job.fast_forward

Scheduled jobs were moved earlier (time travel). One row per call: moved of matched jobs, the first 50 ids in job_ids.

  • POST /v1/scheduled-jobs/fast-forward
  • POST /v1/dev/advance-lifecycle
scheduled_job
metadata:
selector string, matched number, moved number, job_ids string[]

scheduled_job.cancel

Scheduled jobs were canceled (by the API, a report cancel, or a generate job cancel or cleanup). One row per call: canceled of matched jobs, the first 50 ids in job_ids.

  • POST /v1/scheduled-jobs/cancel
  • POST /v1/scheduled-jobs/:id/cancel
  • POST /v1/dev/generate-jobs/:id/cleanup
  • POST /v1/dev/generate-jobs/:id/cancel
  • POST /v1/reports/:id/cancel
scheduled_job
metadata:
selector string, matched number, canceled number, job_ids string[]

scheduled_job.expireasync

A scheduled job failed for good because its runner kept losing its lease (failed, job_lease_expired); its target moves to its failed state in the same transaction.

  • Background job
scheduled_job
changes:
state
metadata:
kind string, attempts number

flow_profile.create

A flow profile was created (version 1). Creating the same name with the same body again returns the existing profile and writes no row.

  • POST /v1/flow-profiles
flow_profile
changes:
name, description, variants, version

flow_profile.update

A flow profile got a new immutable version (changes.variants = the per-key {from, to} diff, changes.version the version move).

  • PATCH /v1/flow-profiles/:id
flow_profile
changes:
description, variants, version

flow_profile.delete

A flow profile was archived: by its owner (DELETE, with the synthetic accounts still assigned to it), or by the hourly flow retention after 30 days unused (system, reason: unused). Archived profiles resolve for 24 hours, then stop.

  • DELETE /v1/flow-profiles/:id
  • Hourly lifecycle run
flow_profile
changes:
archived_at
metadata:
already_archived boolean, synthetic_accounts number, workspace_default boolean, reason string

tenant_flow_default.update

The workspace default flow profile changed (changes.default_profile {from, to} as fp_…@N refs, fp_platform for none). Applies to requests without a profile and to resources created afterwards only.

  • PUT /v1/flow-profiles/default
tenant
changes:
default_profile

workspace_settings.update

The owner changed the workspace (changes = {from, to} for each field that changed: the name, company, default currency, default time zone, environment label or colour). Tenant-shared: every member sees the new values.

  • PATCH /v1/settings/workspace
tenant
changes:
name, company, default_currency, default_time_zone, environment_label, environment_color

user_preferences.update

A signed-in user changed their own preferences (changes = {from, to} for each field that changed). Never an API key.

  • PATCH /v1/me/preferences
user
changes:
time_zone, locale, date_format, hour_cycle, page_size, disputes_default_tab, theme

tenant_data.resetasync

Every synthetic transaction and dispute of the tenant was deleted (the audit trail survives). Written once, by the dev.reset background task when its last batch committed, with the totals; failed with the stable code if the task failed or was canceled.

  • POST /v1/dev/reset
  • Background job
tenant
metadata:
transactions number, disputes number, dispute_events number, evidence number, payment_events number, reports number, batches number

lifecycle.run

A lifecycle pass ran (hourly cron or advance-lifecycle); parent of one row per dispute it moved.

  • POST /v1/dev/advance-lifecycle
  • Hourly lifecycle run
—
metadata:
as_of string, issuer_decision string, generate_job_id string, dispute_ids string[], expired_rfi number, expired number, ruled number, escalated number, fast_forwarded number

dispute.expire_rfiasync

A request for information passed its deadline unanswered: the hourly pass, or (a scheduled dispute, dispute-api/033) its dispute.deadline job.

  • Hourly lifecycle run
  • POST /v1/dev/advance-lifecycle
  • Background job
dispute
changes:
status
metadata:
webhooks_scheduled number

dispute.expireasync

A dispute passed its response deadline and expired: the hourly pass, or (a scheduled dispute, dispute-api/033) its dispute.deadline job.

  • Hourly lifecycle run
  • POST /v1/dev/advance-lifecycle
  • Background job
dispute
changes:
status, outcome
metadata:
webhooks_scheduled number

dispute.ruleasync

The issuer ruled on a dispute under review (won or lost): the hourly pass, or (a scheduled dispute, dispute-api/033) its dispute.rule job. With transition: arbitration_ruling (dispute-api/034, lifecycle.escalation) it is the scheme ruling on a dispute in arbitration (arbitration_won / arbitration_lost), from the sub-key's own mode. ruling_mode is the stamped lifecycle.ruling mode (random for an unstamped dispute) and p the win probability a random/probability draw used, when set; an issuer_decision of won/lost overrides the mode.

  • Hourly lifecycle run
  • POST /v1/dev/advance-lifecycle
  • Background job
dispute
changes:
status, outcome
metadata:
webhooks_scheduled number, issuer_decision string, ruling_mode string, p number, transition string

dispute.escalateasync

An opt-in lifecycle.escalation transition (dispute-api/034) moved a dispute out of a status that is a dead end by default. transition names it: rfi_expired_to_chargeback (an expired RFI became a chargeback), rfi_responded (an answered RFI was closed, or escalated to a chargeback; outcome is close or escalate), issuer_cancel (the issuer withdrew an open chargeback) or won_reversal (a win reversed in pre-arbitration, once). p is the probability the dispute's draw was compared with, when one decided it. A reopened chargeback also changes respond_by (the fresh deadline). The hourly pass or advance-lifecycle for an auto dispute, its dispute.escalate job for a scheduled one. The arbitration ruling is dispute.rule with transition: arbitration_ruling.

  • Hourly lifecycle run
  • POST /v1/dev/advance-lifecycle
  • Background job
dispute
changes:
status, respond_by
metadata:
webhooks_scheduled number, transition string, outcome string, p number

dispute.lifecycle_stop

A scheduled dispute's lifecycle job was canceled, so its clock stopped: it is manual now (changes.lifecycle_mode), and only an advance-lifecycle that targets it moves it (dispute-api/033). job_id and kind name the canceled job.

  • POST /v1/scheduled-jobs/cancel
  • POST /v1/scheduled-jobs/:id/cancel
dispute
changes:
lifecycle_mode
metadata:
job_id string, kind string

dispute.complete_actionasync

An action acknowledged under dispute.action_ack (dispute-api/035) took effect: the dispute moved (changes.status). Actor system, on_behalf_of the requester, parent_id the request's own dispute.challenge / dispute.arbitrate row (with metadata.pending: true). action names the action, job_id its dispute.complete_action job (absent when the hourly repair sweep completed it after its job was lost, source: repair). failed with invalid_dispute_state when the dispute had already moved on (the pending action is cleared).

  • Background job
  • Hourly lifecycle run
dispute
changes:
status
metadata:
action string, job_id string, source string, evidence_count number, webhooks_scheduled number

dispute.cancel_action

A pending action (dispute.action_ack, dispute-api/035) was dropped because its dispute.complete_action job was canceled: pending_action is cleared and the status is unchanged. action and job_id name what was canceled.

  • POST /v1/scheduled-jobs/cancel
  • POST /v1/scheduled-jobs/:id/cancel
dispute
changes:
pending_action
metadata:
action string, job_id string

webhook_sink.create

A temporary webhook sink was created for a test.

  • POST /v1/dev/webhook-sinks
webhook_sink
metadata:
expires_at string, correlation_prefix string, generate_job_id string

webhook_sink.delete

A temporary webhook sink was retired before it expired (test cleanup), or because the person who created it was removed from the workspace (reason: creator_departed).

  • DELETE /v1/dev/webhook-sinks/:id
  • DELETE /v1/team/members/:id
  • POST /v1/me/workspaces/:id/leave
webhook_sink
metadata:
was_live boolean, reason string

webhook_sink.receiveasync

A webhook sink received a delivery.

  • POST /v1/dev/webhook-sinks/:id/receive
webhook_sink
metadata:
delivery_id string, event string, body_sha256 string, signature_valid boolean, headers string[]

webhook_delivery.sendasync

A webhook delivery attempt (outcome failed on a non-2xx or network error). Retried attempts are rows of their own with the same delivery_id and attempt 2-5; retry_scheduled says whether another attempt follows. sequence is the body's sequence (first attempt). Under the webhook.delivery flow variant (simulated: true): error_code simulated_failure (an attempt recorded failed without sending), simulated_drop (never sent) or delivery_backlog_full; duplicate copies carry copy (1-3). Every attempt of a variant delivery also records the applied variant: flow_profile_id (the profile that supplied webhook.delivery), flow_source and flow_variants.webhook.delivery, so GET /v1/audit-events?flow_profile_id= finds the deliveries a profile shaped.

  • Webhook delivery
webhook
metadata:
delivery_id string, webhook_id string, event string, status_code number, duration_ms number, attempt number, retry_scheduled boolean, next_attempt_at string, sequence number, simulated boolean, copy number

api_key.create

An API key was created (the secret is never recorded).

  • POST /v1/api-keys
api_key
metadata:
name string, scopes string[], expires_at string, environment string

api_key.rotate

An API key was rotated; the old secret stays valid for the grace period.

  • POST /v1/api-keys/:id/rotate
api_key
metadata:
grace_hours number, expires_at string, new_api_key_id string

api_key.revoke

An API key was revoked (already_revoked: true on a repeat, which changed nothing).

  • DELETE /v1/api-keys/:id
api_key
metadata:
already_revoked boolean

api_key.deactivateasync

An active key stopped working (revoked or rotated in the portal or API, revoked by the app owner, expired, or its creator was removed from the workspace); recorded the first time dispute.markets sees it inactive. Actor system; when a revoke or no-grace rotate here caused it, the row carries that request's request id and correlation id.

  • Key check (any key route)
api_key
metadata:
reason string

api_key.authenticate

A request with an API key was refused (outcome denied only; at most once per key per minute per pod).

  • Key check (any key route)
api_key
metadata:
reason string

api_key.useasync

Hourly per-pod summary of one API key's requests.

  • Background job
api_key
metadata:
requests number, route_groups string[], ips string[], rejected_after_revoke number

api.rate_limit_simulated

The api.rate_limit flow variant answered 429 rate_limited on a GET route (outcome=failed, error_code=simulated_rate_limited). Written once per correlation id and route, on the first simulated 429; the later ones of that run are not recorded. route is the route id (GET /v1/disputes), first_n_per_correlation how many calls answer 429, retry_after_s the Retry-After sent.

  • Key check (any key route)
—
metadata:
route string, first_n_per_correlation number, retry_after_s number

transaction.export

Transactions were exported as CSV: rows the rows sent, filters the filters given (status, card_brand, search, from, to). failed with export_too_large when more than 100,000 rows match (nothing is sent).

  • GET /v1/transactions/export.csv
—
metadata:
rows number, filters string[]

evidence.create_download_url

A presigned download URL was issued for an evidence file (the URL is not recorded): ttl_s its life in seconds, expired: true for a link signed in the past (evidence.download_url {mode: "expired"}).

  • GET /v1/disputes/:id/evidence/:evidence_id/download-url
evidence
metadata:
dispute_id string, ttl_s number, expired boolean

report.create_download_url

A presigned download URL was issued for a report (the URL is not recorded): ttl_s its life in seconds, expired: true for a link signed in the past (report.download_url {mode: "expired"}).

  • GET /v1/reports/:id/download-url
report
metadata:
ttl_s number, expired boolean

audit_event.export

Audit events were exported (CSV or NDJSON).

  • GET /v1/audit-events/export
—
metadata:
format string, rows number, since string, until string, filters string[]

synthetic_account.send_setup_email says why a setup link went out in metadata.reason: create (a new email-mode account), resend (the link was unused), restart (the link was used, so the account was signed out first), email_change (a new sign-in address) or mode_change (a switch from a generated password); see Synthetic accounts.

People and workspaces

Invites, colleagues and workspaces write the actions below (each links to its row in the table above). workspace_invite.* rows target the invite (wi_…), workspace_member.* rows the person (wm_…). The invite link itself never reaches the trail. When someone is removed, leaves or stops being an owner, what happens to what they left behind is recorded on the removal row and as one row per synthetic account (synthetic_account.move_mail, or synthetic_account.delete with reason: mail_owner_departed) and per webhook or playbook (webhook.disable, webhook.revouch, playbook.disable, playbook.revouch). A workspace switch is written into the workspace switched to; the portal's Activity page hides it unless you pick the People chip. Creating a workspace is tenant.create with metadata.source (signup, bootstrap or create); copying settings writes tenant.copy_settings into the workspace copied from.

Metadata any action may carry

KeyTypeMeaning
capturestring“fallback” when the API recorded the action for a route that failed to record it itself.
coalesced_countnumberHow many identical refusals this row stands for.
unverified_correlation_idstringThe correlation id sent with an unauthenticated request that did not succeed (a failed sign-in, say). It is kept here, not in the filterable correlation_id, so strangers cannot add rows to your test’s correlation namespace.

Not audited

  • Sign-out.It only clears the portal's cookies, so there is no server action to record.
  • Plain reads and lists, including reads of the trail itself. Sensitive reads (exports, download links) are audited. An API key's reads are summarised hourly in api_key.use.
  • Successful token refreshes (auth.refresh is written only on failure).

Compatibility policy

  • New fields, actions and metadata keys are added without notice. Ignore what you do not know; never fail on an unknown key or action.
  • An action that is renamed or removed keeps being written under its old name for 90 days.
  • schema_version changes only for a breaking change to the envelope.

What an API key sees

Personal data stays with people. Without audit.read_sensitive, which owners hold and no API key can hold, events are returned (and exported) with:

  • request.ip and request.user_agent set to null;
  • actor.display null for user actors;
  • the metadata of auth.* and mfa.* actions removed (except audit-layer keys such as coalesced_count).

Everything else, including your own keys' and synthetic accounts' actions, is visible.

Retention