APIAudit

Audit log

Every mutation that changes a group’s state writes one or more rows to that group’s audit log. Reads return the entries that were written for a single group, newest first, with optional filtering by action and timestamp.

The audit log is the durable counterpart to the SSE event stream. SSE delivers events to live subscribers but does not replay across reconnects; the audit log is the source of truth for “what has happened to this group” and supports pagination over the full history.

GET /v1/groups/:id/audit

Returns the most recent audit entries for the group, sorted by createdAt descending (and id descending as a tiebreaker for entries that share a timestamp).

Path parameters

FieldRequiredNotes
idyesThe group to fetch audit entries for. Must belong to the calling game and not be soft-deleted.

Query parameters

FieldTypeDefaultNotes
limitinteger (1-100)50Max number of entries to return.
beforeISO 8601 timestampnoneIf supplied, only entries with createdAt < before are returned. Used for pagination by feeding the response’s nextCursor back in.
actionsrepeated string(no filter)Filter to entries whose action is one of the supplied values. Repeat the parameter for OR semantics: ?actions=group.created&actions=group.updated.

The actions strings must match the AuditAction union in @junjo/shared. Unknown values are rejected with 400 bad_request.

Response

200 OK with the standard Page<AuditEntry> envelope:

{
  "items": [
    {
      "id": "audit_2",
      "groupId": "grp_xyz",
      "actorUserId": "user_alice",
      "action": "member.invited",
      "targetId": "user_bob",
      "payload": { "invitationId": "inv_1", "code": "abc123" },
      "createdAt": "2026-04-28T05:01:00.000Z"
    },
    {
      "id": "audit_1",
      "groupId": "grp_xyz",
      "actorUserId": null,
      "action": "group.created",
      "targetId": "grp_xyz",
      "payload": { "kind": "guild", "name": "Crimson Wolves" },
      "createdAt": "2026-04-28T05:00:00.000Z"
    }
  ],
  "nextCursor": "2026-04-28T05:00:00.000Z"
}

nextCursor is the ISO 8601 createdAt of the last item when the page is full; null when no further entries exist. To fetch the next page, pass nextCursor back as before.

Audit entry shape

FieldTypeNotes
idstringStable cuid for the entry.
groupIdstringThe group the entry belongs to.
actorUserIdstring | nullThe Junjo user id that performed the action, when known. null for system-driven actions and for actions taken via the API key (no auth-adapter actor wired for most mutations).
actionAuditActionOne of the strings in the union.
targetIdstring | nullFree-form pointer to whatever the action targeted: a user id, role id, permission key, group id. Type depends on action.
payloadobjectAction-specific details. The before / after fields on update actions only contain the keys that actually changed.
createdAtISO 8601 timestampWhen the entry was written.

Errors

StatusCodeWhen
400bad_requestThe query is malformed (out-of-range limit, malformed before, unknown actions value).
401invalid_api_keyAPI key missing or invalid.
404not_foundGroup does not exist, is soft-deleted, or belongs to a different game.

Pagination semantics

The pagination uses before (a timestamp) rather than an opaque cursor. To walk all entries:

  1. Call without before. Receive items and nextCursor (an ISO timestamp).
  2. If nextCursor is non-null, call again with before = nextCursor.
  3. Repeat until nextCursor is null.

If two entries share the same createdAt to millisecond precision, the page boundary may skip entries with that exact timestamp on the next call (since before is exclusive). Audit entries are written one per transaction, so collisions are rare in practice; consumers that need strict ordering should treat the audit log as eventually consistent across page boundaries.