API reference

Introspection API

Read back what happened in your workspace. Built for end-to-end tests: mark a point, act, then wait for exactly the events you caused. All routes need the audit.read scope and every response carries X-Audit-Seq, the workspace's current high-water seq.

Routes

RouteReturns
GET /v1/audit-events/cursor{seq}: the latest seq (0 before the first event). Your mark.
GET /v1/audit-eventsA page of events, filtered, newest first by default.
GET /v1/audit-events/:idOne event by aud_… id (404 for another workspace's id).
GET /v1/audit-events/waitAnswers as soon as matching events exist, or at a timeout.
GET /v1/audit-events/exportA CSV or NDJSON file of matching events.
GET /v1/audit-events/actionsThe action catalogue (the table on Audit events).

Filters

The list, wait and export routes take the same filters; all are combined with AND.

ParameterMatches
after_seqEvents with seq greater than this: everything since your mark.
before_seqEvents with seq less than this.
since, untilISO 8601 instants (since inclusive, until exclusive).
actionComma list, up to 20 entries, ORed. An entry ending in .* matches a family: dispute.* matches dispute.accept but not dispute_x.accept.
correlation_idExact, or a prefix: at least 4 characters and one trailing * (ci-4812:*). _ and % are literal characters. * alone or a shorter prefix (ci*) is rejected with 400 validation_failed, whose details name the field correlation_id and the constraint invalid_correlation_filter.
request_idExact req_….
target_type, target_idExact, e.g. dispute and dsp_….
actor_type, actor_idE.g. api_key and tak_….
outcomesuccess, denied or failed.
sourceportal, api or system.
parent_idThe children of one batch or lifecycle run (aud_…).

List and get

Everything one CI run did, oldest first
curl -s "https://api.dispute.markets/v1/audit-events?correlation_id=ci-4812:*&order=asc&limit=200" \
  -H "Authorization: Bearer $DM_API_KEY"

The response is {data, pagination: {next_cursor, has_more}, high_water_seq}. limit is 1 to 200 (default 50). Pass next_cursor back as cursor with the same filters and order; a cursor reused with other filters is 400 invalid_cursor.

high_water_seq is read in the same snapshot as data: continuing with after_seq=high_water_seq neither misses nor repeats an event.

Mark a point

GET /v1/audit-events/cursor returns the workspace's current seq. Call it before acting and filter with after_seq afterwards, so you only ever look at what happened since. Sequence numbers follow commit order, so a slow transaction that commits after your mark is never skipped.

Wait instead of sleeping

GET /v1/audit-events/wait is a bounded long-poll: it answers as soon as at least min_count matching events exist after after_seq, or when timeout_s runs out. Use it for anything that happens after a response (generate jobs, lifecycle runs, webhook deliveries), and to avoid a sleep-then-check loop.

Wait for a generate job to finish
curl -s "https://api.dispute.markets/v1/audit-events/wait?after_seq=1041&action=generate_job.run&target_id=gj_…&timeout_s=20" \
  -H "Authorization: Bearer $DM_API_KEY"

{ "satisfied": true, "data": [ { "action": "generate_job.run", … } ], "high_water_seq": 1047 }

Rules

  • after_seq is required, plus at least one narrowing filter: correlation_id, request_id, target_id, or action made only of exact names (dispute.accept,dispute.challenge) or prefixes of at least two segments (webhook_delivery.send.*). A family such as dispute.* alone would tail the whole workspace and is 400 wait_filter_too_broad.
  • min_count 1 to 50 (default 1); timeout_s 1 to 25 (default 10). Longer waits wait again.
  • Always 200. On a timeout satisfied is false and data holds the partial matches seen so far, so a failing test can print what did happen. data holds at most 200 events.
  • To keep waiting without gaps or repeats, call again with after_seq=high_water_seq.
  • At most 12 concurrent waits per caller and 12 per workspace on each API server; over that, 429 too_many_waits with Retry-After: 1. Each server's wait slots are shared fairly between workspaces, so another workspace cannot use them all up: when they are full, a busy workspace's oldest wait may answer early with satisfied: false, just like a timeout. Events committed on another server are seen within about a second.

The example fixture wraps it like this:

examples/e2e-playwright/src/dm.ts (excerpt)
  async waitFor(filter: AuditFilter, options: WaitOptions = {}): Promise<AuditEvent[]> {
    const minCount = options.minCount ?? 1;
    const deadline = Date.now() + (options.timeout ?? 15_000);
    for (;;) {
      const res = await this.waitOnce(filter, minCount, deadline - Date.now());
      if (res.satisfied) return res.data;
      if (Date.now() >= deadline) {
        throw new Error(
          `waitFor(${JSON.stringify(filter)}) saw ${res.data.length} of ${minCount} event(s):\n` +
            res.data.map((e) => `  ${describeEvent(e)}`).join('\n'),
        );
      }
    }
  }

Export

curl -s -o trail.csv "https://api.dispute.markets/v1/audit-events/export?format=csv&since=2026-10-01T00:00:00Z" \
  -H "Authorization: Bearer $DM_API_KEY"
  • format=csv (one header row; changes and metadata as JSON cells) or format=ndjson (one event per line). Same filters as the list, oldest first.
  • At most 31 days per export (default: the last 31 days; wider is 400 export_range_too_large).
  • At most 100,000 rows for people and 10,000 for API keys, checked before the first byte (413 export_too_large: narrow the window or the filters).
  • One export per workspace at a time (429 export_in_progress with Retry-After).
  • Each export is itself recorded (audit_event.export); the X-Audit-Seq of the response is that event's seq.
  • CSV cells that start with =, +, -, @, a tab or a carriage return are prefixed with ' so a spreadsheet does not evaluate them.