# Audit Logs: SparkVault API Reference

> Query the unified audit trail for your account, integrations, vaults, and ingots: cursor-paged event streams with per-vault retention controls.

Canonical: https://sparkvault.com/api/docs/audit-logs/ · OpenAPI: https://sparkvault.com/openapi.yaml

## Overview

SparkVault records security-relevant activity in a unified audit log. Every entry captures what happened (`event_type`), when (`timestamp`), who did it (`actor_id`), and from where (`ip_address`, `user_agent`), plus event-specific `metadata`. Events are partitioned into four scopes, each with its own list endpoint:

| Scope | What it records | Endpoint | Auth |
| --- | --- | --- | --- |
| Account | Logins, user and license management, billing, API keys, custom domains, vault create/delete, SparkLink receipts, Messaging governance | `GET /v1/audit-logs` | API key or JWT |
| Integration / Product | Per-integration and per-product event streams (Slack, HubSpot, Identity, Structured Ingots, …), called `apps` in the API | `GET /v1/audit-logs/apps/{app_slug}` | API key or JWT |
| Vault | Vault settings, seal/unseal, sharing, upload portals, DVAKs, folder operations | `GET /v1/vaults/{vault_id}/audit-logs` | API key or JWT + `X-Vault-Access-Token` |
| Ingot | Per-file activity: uploads, downloads, accesses, renames, sharing | `GET /v1/vaults/{vault_id}/ingots/{ingot_id}/audit-logs` | API key or JWT + `X-Vault-Access-Token` |

Entries are returned newest-first with signed cursor pagination. Account and integration entries are stored in plaintext. Vault and ingot entry metadata is encrypted at rest with AES-256-GCM under a key derived (HKDF) from the vault's Vault Access Key (VAK) when the event is recorded through an unsealed session. It can only be decrypted with a valid [Vault Access Token](/api/docs/vaults/). Events recorded outside a vault session (such as `retention_changed`) store their metadata in plaintext.

### Entry Fields

#### Audit Log Entry

| Field | Type | Description |
| --- | --- | --- |
| `timestamp` | integer | Unix timestamp of the event in epoch **seconds** |
| `event_type` | string | Event identifier (see Event Types) |
| `actor_id` | string? | Who performed the action: a user (`usr_...`) or an API key (`key_...`). `null` for unattributed events; omitted on integration-scope entries. |
| `actor_type` | string? | `user` or `api_key`, present when the actor still exists in your account and could be resolved |
| `actor_display` | string? | Human-readable actor: the user's email or the API key's name, present when resolved |
| `ip_address` | string? | Client IP address that triggered the event |
| `user_agent` | string? | Client user agent |
| `metadata` | object? | Event-specific details. Decrypted transparently on vault/ingot endpoints using your VAT session; `null` when the event carries none. |
| `vault_id` | string? | Present on vault- and ingot-scope entries |
| `ingot_id` | string? | Present on ingot-scope entries |
| `app_slug` | string? | Present on integration-scope entries |

Audit logs answer _who did what, and when_. For usage aggregates and analytics dashboards, see [Reporting](/api/docs/reporting/).

## Event Types

Each scope has its own set of event types. The account, vault, and ingot list endpoints return the full valid set for their scope in the response's `event_types` field, so you can build filters without hardcoding the lists below.

### Account Events

| Category | Event Types |
| --- | --- |
| User management | `user_invited`, `user_removed`, `user_role_changed` |
| License management | `user_seat_type_set`, `user_license_cancel_scheduled`, `user_license_cancel_undone`, `user_license_released`, `license_migration_applied` |
| Authentication | `login`, `logout`, `login_failed` |
| Identity verification | `auth_attempt`, `auth_success`, `auth_failure` |
| Account settings | `email_changed`, `name_changed`, `organization_name_changed`, `logo_updated` |
| Custom domains | `custom_domain_added`, `custom_domain_removed`, `custom_domain_verified` |
| Vault lifecycle | `vault_created`, `vault_deleted` |
| Billing | `payment_added`, `payment_failed`, `credit_card_added`, `credit_card_removed` |
| API keys | `api_key_created`, `api_key_revoked` |
| SparkLink lifecycle | `sparklink_created`, `sparklink_deleted`, `sparklink_revoked` |
| Verified-interaction receipts | `sparklink_accessed`, `sparklink_signed`, `sparklink_approved`, `sparklink_denied`, `sparklink_replied` |
| Messaging governance | `messaging_conversation_created`, `messaging_participant_added`, `messaging_participant_removed`, `messaging_recipient_verified`, `messaging_message_sent`, `messaging_message_recalled`, `messaging_opted_out`, `messaging_opted_in`, `messaging_legal_hold_set`, `messaging_legal_hold_cleared`, `messaging_receipt_issued` |

The account-scope `event_types` response set also includes platform-admin event types (`admin_impersonate`, `admin_account_deleted`, `admin_status_changed`, `admin_pricing_updated`, `admin_blocklist_source_toggled`, `admin_blocklist_domain_added`, `admin_blocklist_domain_removed`). These are recorded only under the SparkVault platform admin account, so your own audit logs will never contain them.

> **Portable Receipts Live at the Account Scope**
>
> Verified-interaction receipt events (`sparklink_accessed`, `sparklink_signed`, `sparklink_approved`, `sparklink_denied`, `sparklink_replied`) are recorded at the **account** scope in plaintext so they can be presented as third-party proof without a vault key: the signed EdDSA receipt token and `action_hash` ride in `metadata` and are JWKS-verifiable. Messaging events never contain message content: only IDs, the verified sender, and the SparkVault-signed receipt token.

### Vault Events

| Category | Event Types |
| --- | --- |
| Vault operations | `vault_renamed`, `vault_settings_changed`, `vault_unsealed`, `vault_sealed` |
| Sharing | `sharing_enabled`, `sharing_disabled`, `sharing_config_changed` |
| Upload portal and widget | `upload_portal_enabled`, `upload_portal_disabled`, `upload_portal_repaired`, `upload_widget_enabled`, `upload_widget_disabled`, `upload_widget_repaired` |
| VMK hosting | `vmk_hosted`, `vmk_removed`, `hvmk_key_created` |
| Delegated Vault Access Keys | `dvak_created`, `dvak_revoked`, `dvak_used`, `dvak_auto_revoked` |
| Folders | `folder_created`, `folder_renamed`, `folder_moved`, `folder_deleted`, `folder_updated` |
| Retention | `retention_changed` |

### Ingot Events

| Category | Event Types |
| --- | --- |
| Lifecycle | `ingot_created`, `ingot_renamed`, `ingot_deleted`, `ingot_moved` |
| Sharing | `ingot_shared`, `ingot_unshared`, `invite_created`, `invite_revoked` |
| Access | `ingot_accessed`, `ingot_downloaded`, `ingot_uploaded` |
| SparkLink access | `sparklink_accessed` |
| Errors | `upload_error`, `download_error` |

### Integration and Product Events

Integration-scope event types are **free-form strings**: integrations and products define their own. Common patterns:

| Category | Event Types |
| --- | --- |
| Installation | `app_installed`, `app_uninstalled`, `app_config_changed` |
| Identity Product | `passkey_created`, `passkey_removed` |
| Slack Integration | `slack_connected`, `slack_disconnected`, `slack_message_sent` |
| Structured Ingots Product | `structure_created`, `structure_updated`, `key_accessed` |

## Pagination and Filtering

All list endpoints share the same cursor-based pagination and filtering contract:

#### Query Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | Optional | Maximum entries per page. Values outside 1-100 are clamped into range.Default: `25` |
| `cursor` | string | Optional | Opaque signed pagination cursor: pass back the `next_cursor` from the previous responseDefault: `null` |
| `event_types` | string | Optional | Comma-separated event type filter, e.g. `?event_types=login,login_failed`Default: `none (all events)` |

-   **Ordering**: entries are returned newest-first; `timestamp` is epoch seconds.
-   **`has_more` / `next_cursor`**: `next_cursor` is present only when `has_more` is `true`. Keep requesting with `?cursor=` until `has_more` is `false`.
-   **Signed cursors**: cursors are HMAC-signed per account and bound to the exact audit scope they were minted for. A tampered cursor, a cursor from another account, or a cursor replayed against a different scope returns `400 VALIDATION_ERROR`.
-   **Filtered pages can run short**: with an `event_types` filter, a page may contain fewer than `limit` entries (even zero) while `has_more` is still `true`. Always page by `has_more`, not by page size.

## Account Audit Logs

#### `GET /v1/audit-logs`

List account-scope audit logs: authentication, user and license management, billing, API keys, custom domains, vault lifecycle, SparkLink receipts, and Messaging governance events.

#### Query Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | Optional | Maximum entries per page (1-100)Default: `25` |
| `cursor` | string | Optional | Signed pagination cursor from a previous responseDefault: `null` |
| `event_types` | string | Optional | Comma-separated filter of account event typesDefault: `none (all events)` |

#### Response

| Field | Type | Description |
| --- | --- | --- |
| `entries` | array | Audit log entries, newest first (see Entry Fields in the Overview) |
| `count` | integer | Number of entries in this page |
| `has_more` | boolean | Whether more pages exist |
| `next_cursor` | string? | Signed cursor for the next page (present when `has_more` is true) |
| `event_types` | string\[\] | Every valid account-scope event type, useful for building filter UIs |

#### List Recent Logins

Request

```bash
curl "https://api.sparkvault.com/v1/audit-logs?limit=2&event_types=login,login_failed" \
  -H "X-API-Key: sv_live_xxx"
```

Response

```json
{
  "data": {
    "entries": [
      {
        "timestamp": 1702000600,
        "event_type": "login",
        "actor_id": "usr_abc123...",
        "actor_type": "user",
        "actor_display": "admin@example.com",
        "ip_address": "203.0.113.7",
        "user_agent": "Mozilla/5.0...",
        "metadata": null
      },
      {
        "timestamp": 1702000000,
        "event_type": "login_failed",
        "actor_id": null,
        "ip_address": "198.51.100.23",
        "user_agent": "Mozilla/5.0...",
        "metadata": null
      }
    ],
    "count": 2,
    "has_more": true,
    "next_cursor": "eyJrZXkiOnsi...",
    "event_types": ["user_invited", "user_removed", "user_role_changed", "..."]
  }
}
```

## Integration and Product Audit Logs

Every installed [integration](/api/docs/integrations/) and every product keeps its own event stream, addressed by its slug. The API calls this scope `apps`. Integration-scope entries record `timestamp`, `event_type`, `metadata`, and `app_slug`. They carry no actor or IP fields.

#### `GET /v1/audit-logs/apps/{app_slug}`

List the audit log for one integration or product stream.

#### Path Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `app_slug` | string | Required | Slug of the integration or product stream, e.g. `slack` |

#### Query Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | Optional | Maximum entries per page (1-100)Default: `25` |
| `cursor` | string | Optional | Signed pagination cursor from a previous responseDefault: `null` |
| `event_types` | string | Optional | Comma-separated event type filterDefault: `none (all events)` |

#### Response

| Field | Type | Description |
| --- | --- | --- |
| `entries` | array | Audit log entries, newest first |
| `count` | integer | Number of entries in this page |
| `has_more` | boolean | Whether more pages exist |
| `next_cursor` | string? | Signed cursor for the next page (present when `has_more` is true) |
| `app_slug` | string | The requested stream slug |

#### List Slack Integration Events

Request

```bash
curl "https://api.sparkvault.com/v1/audit-logs/apps/slack?limit=25" \
  -H "X-API-Key: sv_live_xxx"
```

Response

```json
{
  "data": {
    "entries": [
      {
        "timestamp": 1702000000,
        "event_type": "slack_connected",
        "metadata": null,
        "app_slug": "slack"
      }
    ],
    "count": 1,
    "has_more": false,
    "app_slug": "slack"
  }
}
```

#### `POST /v1/audit-logs/apps`

Append an event to an integration or product stream. Event types are free-form, so your own automations can write their own audit trail. Returns 201 Created.

#### Request Body

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `app_slug` | string | Required | Stream to log under, e.g. `slack` |
| `event_type` | string | Required | Free-form event identifier |
| `metadata` | object | Optional | Arbitrary event details stored with the entry (plaintext; never put secrets here)Default: `null` |

#### Response

| Field | Type | Description |
| --- | --- | --- |
| `logged` | boolean | `true`: the entry was written |
| `app_slug` | string | Stream the entry was written to |
| `event_type` | string | The recorded event type |

#### Log a Custom Event

Request

```bash
curl -X POST https://api.sparkvault.com/v1/audit-logs/apps \
  -H "X-API-Key: sv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "app_slug": "hubspot",
    "event_type": "export_completed",
    "metadata": { "record_count": 42 }
  }'
```

Response

```json
{
  "data": {
    "logged": true,
    "app_slug": "hubspot",
    "event_type": "export_completed"
  }
}
```

> **Append-Only**
>
> Audit streams are append-only: entries cannot be edited or deleted through the API, and each `POST` writes a new entry. SparkVault's own integrations and products log to these streams via internal service calls.

## Vault Audit Logs

#### `GET /v1/vaults/{vault_id}/audit-logs`

List vault-scope audit logs: settings, seal/unseal, sharing, upload portals, DVAKs, and folder operations. Requires an unsealed vault session (VAT).

This endpoint requires the `X-Vault-Access-Token` header with a valid VAT obtained by [unsealing the vault](/api/docs/vaults/). Entry metadata is stored encrypted and is decrypted transparently with the VAK held by your VAT session. Vault-scope events cover the vault itself. Per-file events live on the [per-ingot endpoint](/api/docs/ingots/).

#### Path Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `vault_id` | string | Required | Vault identifier (`vlt_...`) |

#### Query Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | Optional | Maximum entries per page (1-100)Default: `25` |
| `cursor` | string | Optional | Signed pagination cursor from a previous responseDefault: `null` |
| `event_types` | string | Optional | Comma-separated filter of vault event typesDefault: `none (all events)` |

#### Response

| Field | Type | Description |
| --- | --- | --- |
| `entries` | array | Audit log entries, newest first; each includes `vault_id` |
| `count` | integer | Number of entries in this page |
| `has_more` | boolean | Whether more pages exist |
| `next_cursor` | string? | Signed cursor for the next page (present when `has_more` is true) |
| `vault_id` | string | The requested vault |
| `event_types` | string\[\] | Every valid vault-scope event type |

#### List Vault Events

Request

```bash
curl "https://api.sparkvault.com/v1/vaults/vlt_abc123/audit-logs?limit=25" \
  -H "X-API-Key: sv_live_xxx" \
  -H "X-Vault-Access-Token: vat_xyz789..."
```

Response

```json
{
  "data": {
    "entries": [
      {
        "timestamp": 1702000000,
        "event_type": "vault_unsealed",
        "actor_id": "usr_abc123...",
        "actor_type": "user",
        "actor_display": "admin@example.com",
        "ip_address": "203.0.113.7",
        "user_agent": "Mozilla/5.0...",
        "metadata": { "ttl_seconds": 3600 },
        "vault_id": "vlt_abc123..."
      }
    ],
    "count": 1,
    "has_more": false,
    "vault_id": "vlt_abc123...",
    "event_types": ["vault_renamed", "vault_settings_changed", "..."]
  }
}
```

> **VAT Context Checks**
>
> The VAT must belong to the requested vault and to your account: a mismatch, or a token that has expired, returns `403 FORBIDDEN`. A missing, malformed, or unknown token returns `401 AUTHENTICATION_ERROR`, and a vault that does not exist (or is not owned by your account) returns `404 NOT_FOUND`.

## Ingot Audit Logs

#### `GET /v1/vaults/{vault_id}/ingots/{ingot_id}/audit-logs`

List per-ingot audit logs: uploads, downloads, accesses, renames, moves, and sharing on a single file. Requires an unsealed vault session (VAT).

The per-ingot endpoint follows the same auth, pagination, and filtering contract as the vault endpoint above: `X-Vault-Access-Token` required, encrypted metadata decrypted via your VAT session, and a response carrying `entries` (each with `vault_id` and `ingot_id`), `count`, `has_more`, `next_cursor`, and the valid ingot-scope `event_types`. Full documentation lives on the [Ingots page](/api/docs/ingots/).

## Retention

Every audit entry is written with an expiry and is removed automatically when it lapses. The default retention window is **90 days** (7,776,000 seconds).

Each vault additionally carries a configurable **access-log retention** setting (`GET`/`PUT /v1/vaults/{vault_id}/access-log-retention`, see the [Vaults docs](/api/docs/vaults/)) that governs how long ingot access events recorded through public sharing surfaces (public portal uploads and SparkLink downloads) are kept. Setting it to `0` (Disabled) turns that logging off entirely: no entries are written. The change itself is always recorded as a `retention_changed` vault event under the default window, so the configuration trail survives even when access logging is disabled.

| Value (seconds) | Window |
| --- | --- |
| `0` | Disabled: no access-log entries are written |
| `86400` | 24 hours |
| `604800` | 7 days |
| `2592000` | 1 month |
| `5184000` | 2 months |
| `7776000` | 3 months (default) |
| `15552000` | 6 months |
| `23328000` | 9 months |
| `31536000` | 1 year |
| `63072000` | 2 years |
| `94608000` | 3 years |

> **Deletion Cascades to Audit History**
>
> Audit logs are owned by the entity they describe. Deleting an ingot, a vault, an integration installation, or your account **permanently deletes** the audit history recorded under it. Export anything you need for compliance before deleting.

## Error Reference

#### Error Responses

| Status | Code | Description |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Invalid or tampered pagination cursor (wrong account or scope), or missing app\_slug/event\_type when creating an entry |
| 401 | `AUTHENTICATION_ERROR` | Missing or invalid API key/JWT; missing, malformed, or unknown X-Vault-Access-Token on vault and ingot endpoints |
| 403 | `FORBIDDEN` | The VAT belongs to a different vault or account than the one requested, or the token has expired |
| 404 | `NOT_FOUND` | Vault does not exist or is not owned by your account |
| 429 | `RATE_LIMIT_EXCEEDED` | Too many requests |
