# Audit log

> Export the tamper-evident audit log of a grovs project to your SIEM.

Source: https://docs.grovs.io/docs/api-reference/audit-log

The audit log is an append-only record of security-relevant actions: logins, membership, configuration, credentials, links and billing. Each entry is chained to the previous one with a SHA-256 hash. A SIEM pulls it with an export token and a cursor.

**Availability:** grovs Cloud instances with an enterprise subscription, and every self-hosted Enterprise deployment. Other instances get `403`. The open-source image has no audit endpoints.

**Auth:** an [export token](https://docs.grovs.io/docs/api-reference/authentication#scoped-keys) (`aet_…`). An instance admin creates it in **Settings** › **Audit Logs** › **Export tokens** › **Create token**. The token is shown once.

The [audit log overview](https://docs.grovs.io/docs/enterprise/audit-log) lists what is recorded. The [SIEM integration guide](https://docs.grovs.io/docs/enterprise/audit-log/siem-integration) covers the poll loop and how to verify the hash chain.

**Endpoints**

- `GET /api/v1/instances/{instance_id}/audit_events`
- `GET /api/v1/instances/{instance_id}/audit_events/head`

## Find the instance ID

The log covers a whole grovs project, production and test together. Paths use the numeric `{instance_id}`, not an environment's project ID.

The dashboard does not show the instance ID yet. To find it:

1. Open **Settings** › **Audit Logs**.
2. Open your browser's network panel.
3. Click **Export tokens**.
4. Read the number in the request path `/api/v1/instances/INSTANCE_ID/audit_export_tokens`.

A token used with another instance's ID gets `403`.

```text
/api/v1/instances/1395/audit_events
                  ^^^^
                  instance ID (integer)
```

## The audit event object

**Attributes**

- `id` (integer): Row ID.
- `sequence` (integer): Gap-free counter per instance. The only cursor you need.
- `occurred_at` (string): UTC, ISO 8601 with six fractional digits.
- `action` (string): What happened, such as `instance.member_added` or `user.login`. See the [action families](https://docs.grovs.io/docs/enterprise/audit-log#what-gets-recorded).
- `outcome` (enum): `success`, `failure` or `pending`.
- `actor` (object): Who did it: `type`, `id`, `email`, and `via`, the credential channel (for example `dashboard`, `password`, `sso:google_oauth2`, `api_key`).
- `target` (object): What it was done to, such as `{ "type": "instance_role", "id": 9, "email": "…" }`.
- `changes` (object): A `before` / `after` diff where one applies. Secrets appear as `"[FILTERED]"`.
- `ip` (string | null): Client IP.
- `user_agent` (string | null): Client user agent.
- `request_id` (string | null): Request ID. Echoes a client-supplied `X-Request-Id`, so use it for correlation, not as evidence.
- `prev_hash` (string | null): `hash` of the previous entry. `null` on the first one.
- `hash` (string): SHA-256 of this entry. See [verifying the chain](https://docs.grovs.io/docs/enterprise/audit-log/siem-integration#verifying-the-chain).

**The audit event object**

```json
{
  "id": 42,
  "sequence": 12,
  "action": "instance.member_added",
  "outcome": "success",
  "actor": { "type": "user", "id": 3, "email": "ops@medly.health", "via": "dashboard" },
  "target": { "type": "instance_role", "id": 9, "email": "dana@medly.health" },
  "ip": "203.0.113.9",
  "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) …",
  "request_id": "b1e5c0d2-7a41-4f6e-9a0b-3c2d1e0f9a8b",
  "prev_hash": "9f2c41e0…",
  "occurred_at": "2026-10-22T09:12:04.123456Z",
  "changes": { "after": { "role": "member" } },
  "hash": "4a7d93b1…"
}
```

## List audit events

`GET /api/v1/instances/{instance_id}/audit_events`

Returns a page of audit events ordered by `sequence`. A SIEM starts at `after=0` and sets `after` to `next_after` until `events` comes back empty.

Auth: Scoped key. See [Authentication](https://docs.grovs.io/docs/api-reference/authentication).

Send an export token as `Authorization: Bearer aet_…`.

**Path**

- `instance_id` (integer, required): The numeric instance ID. See [Find the instance ID](#find-the-instance-id).

**Query**

- `after` (integer, optional): Only entries with a `sequence` greater than this.
- `before` (integer, optional): Only entries with a `sequence` less than this. Use with `order=desc` to page backwards.
- `limit` (integer, optional): 1 to 1000. Default `100`.
- `order` (enum, optional): `asc` (default, oldest first) or `desc`.
- `event_action` (string, optional): Only this action. A SIEM should page on `after` alone.
- `actor_email` (string, optional): Only this actor.
- `from` (string, optional): ISO 8601. Only entries at or after this time.
- `to` (string, optional): ISO 8601. Only entries at or before this time.

**Returns** `schema_version`, `events` (an array of [audit event objects](#the-audit-event-object)), `next_after` (the highest `sequence` on the page) and `next_before` (the lowest). Both are `null` on an empty page.

In production, each export token can make 60 requests a minute. Over that, the API answers `503`.

**Errors**

| Status | Meaning                                                                                   |
| ------ | ----------------------------------------------------------------------------------------- |
| 400    | \`order\` is not \`asc\` or \`desc\`, or a cursor or date is not valid.                   |
| 401    | Missing, invalid or revoked token.                                                        |
| 403    | The token belongs to another instance, or the audit log is not enabled for this instance. |
| 503    | More than 60 requests a minute for this token. Back off and retry.                        |

**Request**

```bash
curl -X GET "https://api.sqd.link/api/v1/instances/{instance_id}/audit_events?after=0&limit=1000" \
  -H "Authorization: Bearer $GROVS_AUDIT_KEY"
```

**Response 200 OK**

```json
{
  "schema_version": 1,
  "events": [
    {
      "id": 42,
      "sequence": 12,
      "action": "instance.member_added",
      "outcome": "success",
      "actor": { "type": "user", "id": 3, "email": "ops@medly.health", "via": "dashboard" },
      "target": { "type": "instance_role", "id": 9, "email": "dana@medly.health" },
      "ip": "203.0.113.9",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) …",
      "request_id": "b1e5c0d2-7a41-4f6e-9a0b-3c2d1e0f9a8b",
      "prev_hash": "9f2c41e0…",
      "occurred_at": "2026-10-22T09:12:04.123456Z",
      "changes": { "after": { "role": "member" } },
      "hash": "4a7d93b1…"
    }
  ],
  "next_after": 12,
  "next_before": 12
}
```

**Response 403 Forbidden**

```json
{ "error": "Audit log is not enabled for this instance" }
```

## Get the chain head

`GET /api/v1/instances/{instance_id}/audit_events/head`

Returns the latest `sequence` and `hash` without fetching entries. Compare it with your stored cursor to see how far behind a poller is, or with the last entry you ingested to detect a gap.

Auth: Scoped key. See [Authentication](https://docs.grovs.io/docs/api-reference/authentication).

**Path**

- `instance_id` (integer, required): The numeric instance ID.

**Returns** `schema_version`, `sequence` (`0` when the log is empty) and `hash` (`null` when empty). Shares the 60 requests a minute per token limit.

**Errors**

| Status | Meaning                                                                                   |
| ------ | ----------------------------------------------------------------------------------------- |
| 401    | Missing, invalid or revoked token.                                                        |
| 403    | The token belongs to another instance, or the audit log is not enabled for this instance. |

**Request**

```bash
curl -X GET "https://api.sqd.link/api/v1/instances/{instance_id}/audit_events/head" \
  -H "Authorization: Bearer $GROVS_AUDIT_KEY"
```

**Response 200 OK**

```json
{ "schema_version": 1, "sequence": 4182, "hash": "4a7d93b1…" }
```
