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.
beforestringnoneCursor for pagination: feed the response’s nextCursor (an opaque entry id) back in. Also accepts a bare ISO 8601 timestamp (the original contract, kept working for stored cursors and hand-written calls) with createdAt < before semantics.
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.io/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": "audit_1"
}

nextCursor is the id of the last item when more entries exist; null when this is the last page. 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, invalid before cursor, unknown actions value). A before id that does not resolve inside the group also reads as an invalid cursor rather than leaking whether the row exists.
401invalid_api_keyAPI key missing or invalid.
404not_foundGroup does not exist, is soft-deleted, or belongs to a different game.

Pagination semantics

To walk all entries:

  1. Call without before. Receive items and nextCursor (the id of the last item, or null).
  2. If nextCursor is non-null, call again with before = nextCursor.
  3. Repeat until nextCursor is null.

Feeding an id back as before gives exact keyset pagination on the (createdAt, id) sort order, so page boundaries are immune to entries sharing a millisecond. The timestamp form of before remains strictly exclusive on createdAt: entries sharing the exact boundary timestamp can be skipped, so prefer the id cursor the server hands back.