> ## Documentation Index
> Fetch the complete documentation index at: https://docs.upsonic.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Audit API

> Pull an environment's audit events in order, for a SIEM, an archive or your own checks.

Every action in AgentOS — deploys, runs, decisions, secrets, guardrails, memory erases, webhooks, models, evals, users, roles, API keys, sign-ins and refused requests — writes an **audit event** in the same transaction as the change itself. Events can't be changed or deleted, and each one is chained to the one before it with a keyed hash, so a changed, removed or reordered event is detected. For what is recorded and how to read it in the panel, see [Audit log](https://docs.upsonic.ai/agentos/platform/audit-log.md).

This route lets a SIEM, a nightly archive job or a script pull the events of an environment, oldest first, picking up where it left off. It needs the **admin** role in the environment.

## Read the audit log

`GET /v0/envs/{env}/audit`

```bash theme={null}
curl -sS -H "Authorization: Bearer $AGENTOS_API_KEY" "$AGENTOS/v0/envs/default/audit?limit=500"
```

```json theme={null}
{"events": [
  {"id": 1041, "chain": 3, "seq": 977, "at": "2026-10-27T09:12:44.501000Z",
   "action": "version.uploaded", "outcome": "success",
   "actor_type": "service_account", "actor_id": 18, "api_key_id": 4, "on_behalf_of_id": null,
   "environment_id": 2, "agent_id": 7,
   "target_type": "version", "target_id": "v2", "target_name": "hello-agent v2",
   "changes": {}, "data": {"deploy_id": "d_14"}, "reason": "",
   "via": "api", "request_id": "6f1c…", "ip": "10.0.4.21", "user_agent": "curl/8.5.0",
   "schema": 1, "prev_hash": "a3e1…", "hash": "5bd0…"}],
 "next": 1041}
```

**after** `integer` (default: 0)

Only events with a larger `id`: pass the previous page's `next`.

**limit** `integer` (default: 100)

1–500.

**agent** `string`

Only this agent's events (its name).

Events come oldest first. `next` is the last event's `id` when the page is full, and `null` when there is nothing more right now. To follow the log, store the last `id` you processed and call again with `after=<that id>` on a schedule:

```bash theme={null}
AFTER=$(cat audit.cursor 2>/dev/null || echo 0)
while :; do
  PAGE=$(curl -sS -H "Authorization: Bearer $AGENTOS_API_KEY" \
    "$AGENTOS/v0/envs/default/audit?after=$AFTER&limit=500")
  echo "$PAGE" | jq -c '.events[]' >> audit.jsonl
  LAST=$(echo "$PAGE" | jq '.events[-1].id // empty')
  [ -n "$LAST" ] && AFTER=$LAST && echo "$AFTER" > audit.cursor
  [ "$(echo "$PAGE" | jq '.next')" = "null" ] && break
done
```

> [!NOTE]
>
> Reading the log is itself audited: every pull that returns events records an `audit.exported` event (the format, how many events, and from which id). An empty pull records nothing, so polling an idle environment doesn't fill the log.

## The event object

**id** `integer`

Increases with every event of the installation. Use it as your cursor.

**chain, seq** `integer`

The hash chain the event belongs to (one per tenant) and its position in it. A tenant's environments share one chain, so `seq` has gaps when you read one environment.

**at** `string`

When it happened, ISO 8601 in UTC.

**action** `string`

What happened, as `resource.verb` in the past tense, from the catalog below.

**outcome** `string`

`success`, `denied` (the request was refused) or `failure`.

**actor\_type** `string`

`person`, `service_account`, `agent` (acting for a run's caller), `system` (the platform on its own) or `anonymous` (for example a failed sign-in).

**actor\_id** `integer | null`

The user who acted (a person or a service account's user).

**api\_key\_id** `integer | null`

The API key a service account used.

**on\_behalf\_of\_id** `integer | null`

For an agent's action, the caller of the run it acted in.

**environment\_id, agent\_id** `integer | null`

Where it happened.

**target\_type, target\_id, target\_name** `string`

What it was done to. Names are kept for things (agents, guardrails, webhooks…), never for people: a user is named only by id, so erasing a person never means editing the log.

**changes** `object`

`{field: [from, to]}` for the fields that changed. Secrets are stored as keyed fingerprints, and long text such as prompts and memory values as a length and a fingerprint, never the text itself.

**data** `object`

More about the action, such as a deploy id, a receipt's counts or a refused request's reason.

**reason** `string`

Why, when there is one.

**via** `string`

`panel`, `api`, `agent`, `system` or `cli`.

**request\_id, ip, user\_agent** `string`

The request it came from; empty for background work.

**schema** `integer`

The event format's version, `1`.

**prev\_hash, hash** `string`

The hash chain: `hash` is an HMAC-SHA256 over the event's fields and `prev_hash`, the hash of the previous event in the same chain. Checking the hashes needs the installation's audit key; the panel's **Log intact** check does this on every visit to the Audit page.

## Actions

| Group | Actions |
| - | - |
| **Deploys** | `agent.created`, `version.uploaded`, `version.promoted`, `version.rolled_back`, `version.config_changed`, `deploy.succeeded`, `deploy.failed` |
| **Runs and reviews** | `run.started`, `run.cancelled`, `hold.decided`, `hold.expired` |
| **Secrets and guardrails** | `secret.set`, `secret.deleted`, `guardrail.created`, `guardrail.edited`, `guardrail.enabled`, `guardrail.disabled`, `guardrail.deleted`, `network.mode_changed`, `network.rule_added`, `network.rule_removed` |
| **Memory** | `memory.fact_edited`, `memory.fact_deleted`, `memory.path_erased`, `memory.user_erased` |
| **Webhooks** | `webhook.created`, `webhook.secret_rotated`, `webhook.enabled`, `webhook.disabled`, `webhook.deleted`, `webhook.replayed` |
| **Models** | `provider.created`, `provider.edited`, `provider.deleted`, `provider.models_changed`, `model.added`, `model.removed` |
| **Evals** | `dataset.created`, `dataset.published`, `collect_rule.set`, `evaluator.created`, `evaluator.edited`, `eval.created`, `eval.edited`, `experiment.started`, `experiment.cancelled` |
| **Access** | `user.signed_in`, `user.signed_out`, `user.sign_in_failed`, `user.password_changed`, `user.password_reset`, `user.created`, `user.password_set`, `user.deactivated`, `user.reactivated`, `user.roles_changed`, `service_account.created`, `service_account.roles_changed`, `service_account.deactivated`, `service_account.reactivated`, `api_key.issued`, `api_key.revoked`, `tenant.created`, `tenant.admin_added`, `environment.created`, `access.denied`, `audit.exported` |

Reads are not recorded, except exports of the log itself. A prompt deployed from the [Prompts](https://docs.upsonic.ai/agentos/platform/prompts.md) tab appears as `version.config_changed` followed by `version.promoted`.

> [!NOTE]
>
> This route returns the events of one environment. Events that belong to no environment — such as sign-ins, or changes to a tenant's users made on the Users page — are shown to tenant admins in the panel's **Govern → Audit** page.

## Errors

| Status | Code | When |
| - | - | - |
| `403` | `forbidden` | No role in the environment, or no admin role |
| `404` | `not_found` | No such environment |
| `422` | `invalid_request` | A `limit` outside 1–500, a negative `after`, or an unknown query parameter |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.