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

# Stream audit logs to a SIEM

> Send your workspace audit log to Google SecOps or your own HTTPS collector, or pull it from the API.

The workspace [audit trail](https://docs.openlayer.com/security.md#audit-trail) records who did what in your workspace. Besides
the one-off CSV export, you can feed it into a SIEM continuously in one of three ways:

| Method | Use it when |
| - | - |
| [Pull from the API](#pull-from-the-api) | Your SIEM or collector polls vendor APIs on a schedule. |
| [Stream to Google SecOps](#google-secops) | You use Google SecOps and want events pushed to a webhook feed. |
| [Stream to an HTTPS endpoint](#https-endpoint) | You run your own collector and want events pushed to it, signed per request. |

All three are available to workspace admins only.

## Event format

The pull API and both streams send each event as the same JSON object:

```json theme={null}
{
  "id": "5b0c4c1e-1f7a-4f55-9a7e-2f1d3c9b8a01",
  "dateCreated": "2026-10-14T13:02:11.482913Z",
  "workspaceId": "0f6a1d2c-8b3e-4c5d-9e7f-1a2b3c4d5e6f",
  "userId": "9d8c7b6a-5f4e-4d3c-8b2a-1f0e9d8c7b6a",
  "userEmail": "admin@example.com",
  "projectId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "projectName": "Claims agent",
  "action": "api_key_created",
  "success": true,
  "ipAddress": "203.0.113.7",
  "userAgent": "curl/8.7.1",
  "metadata": {}
}
```

* `dateCreated` is in UTC.
* `action` is the permission exercised or the event type, and `success` says whether the action was
  allowed.
* `userEmail` and `projectName` are the values at the time of the event. `projectId` and
  `projectName` are `null` for workspace-level actions.
* `metadata` is a JSON object with extra context, or `null`.
* `id` is stable. Use it to drop repeats.

## Pull from the API

`GET /v1/workspaces/{workspaceId}/audit-events` returns the workspace's audit events oldest first,
one page at a time. Call it with an admin's [API key](https://docs.openlayer.com/workspace-and-projects/find-your-api-key.md).

```bash theme={null}
curl --request GET \
  --url 'https://api.openlayer.com/v1/workspaces/<workspace-id>/audit-events?start=1791936000&limit=500' \
  --header 'Authorization: Bearer <token>'
```

```json theme={null}
{
  "items": [
    {
      "id": "5b0c4c1e-1f7a-4f55-9a7e-2f1d3c9b8a01",
      "dateCreated": "2026-10-14T13:02:11.482913Z",
      "action": "api_key_created"
    }
  ],
  "nextCursor": "<opaque string>",
  "hasMore": true
}
```

The response includes every [event field](#event-format); the example above is shortened.

| Parameter | Description |
| - | - |
| `cursor` | The `nextCursor` from the previous response. Takes precedence over `start`. |
| `start` | Where the first page begins, as a Unix timestamp in **seconds**. Used only without `cursor`. With neither, reading starts at the oldest event. |
| `limit` | Events per page, from 1 to 1000. Defaults to 500. |

To read the log incrementally, store `nextCursor` from every response and send it back as `cursor`
on the next call. `nextCursor` is returned even when `items` is empty, and a cursor never returns the
same event twice. While `hasMore` is `true`, call again right away; once it is `false`, you are caught
up, so wait before polling again.

Keep in mind:

* **Events from the last 10 seconds are held back**, so an event that is committed a moment late still
  arrives in order. Expect a short lag behind real time.
* **`start` is in seconds, not milliseconds.** A millisecond value lies in the future and returns
  `400`.
* **Polling is not audited.** Reading the audit log does not add events to it.
* **The rate limit is 120 requests a minute per user.** Above it, the API returns `429`.
* **Only admins can read it.** Other members get `403`.

## Push to a SIEM

Instead of polling, you can have Openlayer push new events to a destination. Add a stream under
**Workspace settings** → **Security** → **Compliance & audit** → **Stream to SIEM**, or from the
**Google SecOps** and **HTTPS endpoint** cards in the **Security** category of the Integrations page.

### How delivery works

* **Timing.** Events usually reach the destination within about two minutes. They are sent in order,
  in batches of up to 500 events or 1 MB.
* **Where a stream starts.** A stream sends events from the moment you create it, unless you set
  **Send events from** to an earlier time to backfill. To resend events, open **Edit** and set
  **Replay from**; your destination receives those events again, so drop repeats by event `id`.
* **Retries.** When the destination fails or rejects a batch (any non-`2xx` response), Openlayer
  retries it after 1, 2, 5, 15, and 30 minutes, then every hour. It never gives up and never skips
  an event, so a destination that comes back catches up from where it stopped.
* **Large events.** An event bigger than 1 MB on its own is sent with its `metadata` replaced by
  `{"truncated": true, "originalBytes": N}`. The full event stays in the audit log.
* **Limit.** A workspace can have up to 5 streams, across both destination types.

Each stream shows a status:

| Status | Meaning |
| - | - |
| **Healthy** | The stream is delivering normally. |
| **Retrying** | The destination failed or rejected the last attempt. Hover the status to see the error it returned. |
| **Paused** | You switched the stream off. Events queue up and are sent from where it stopped when you resume it. |

**Send test event** in a stream's options menu sends one synthetic event, with the action
`audit_log_stream_test`, and reports whether the destination accepted it. The test event is not
recorded in the audit log. Deleting a stream stops delivery but leaves its events in the audit log.

### Google SecOps

The stream sends events to a Google SecOps webhook feed. Before adding it, set up the following in
Google SecOps and Google Cloud:

1. Create a **Webhook** feed in Google SecOps with the **split delimiter** set to `\n`. Openlayer
   sends each batch as newline-delimited JSON, one event per line.
2. Copy the feed's endpoint URL and generate its **secret key**.
3. Create a Google Cloud API key restricted to the Chronicle API.

Then add a **Google SecOps** stream with the endpoint URL, the API key, and the feed's secret key as
the **Access key**. The URL must be the `https://` endpoint on `googleapis.com` from the feed's
details. Leave the key and secret out of the URL itself; a URL with `key` or `secret` query
parameters is rejected.

> [!NOTE]
>
> Google SecOps has no parser for Openlayer events, so they arrive as raw logs.

When editing a SecOps stream, leave a key blank to keep the current one.

### HTTPS endpoint

The stream sends events to a collector you run. You need an endpoint that:

* uses an `https://` URL with a hostname, on any port. IP addresses, usernames and passwords in the
  URL are rejected, and the hostname must resolve to public IP addresses only;
* presents a valid TLS certificate for that hostname;
* answers with a `2xx` status without redirecting. Openlayer does not follow redirects.

When you add an **HTTPS endpoint** stream, Openlayer generates a **signing secret** and shows it
once. Copy it into your collector before you close the dialog.

> [!WARNING]
>
> The signing secret cannot be shown again. If you lose it, use **Rotate signing
> secret** in the stream's options menu. The old secret stops working
> immediately, so your collector rejects requests until you update it.

Each request is a `POST` with a JSON body holding one batch of [events](#event-format):

```json theme={null}
{
  "events": [
    {
      "id": "5b0c4c1e-1f7a-4f55-9a7e-2f1d3c9b8a01",
      "action": "api_key_created"
    }
  ]
}
```

Requests are signed with [Standard Webhooks](https://www.standardwebhooks.com/), the same scheme as
[Openlayer webhooks](https://docs.openlayer.com/security/webhooks/verify-signatures.md), and carry the `webhook-id`,
`webhook-timestamp`, and `webhook-signature` headers. Verify every request against the raw body
before you process it. With a Standard Webhooks library, the verified payload is the batch:

```python Python theme={null}
# pip install standardwebhooks
from standardwebhooks import Webhook

# The signing secret shown when you created the stream, e.g. "whsec_..."
wh = Webhook("YOUR_STREAM_SIGNING_SECRET")


def handle_request(raw_body: bytes, headers: dict):
    # Raises an error if the signature or timestamp is invalid.
    payload = wh.verify(raw_body, {
        "webhook-id": headers["webhook-id"],
        "webhook-timestamp": headers["webhook-timestamp"],
        "webhook-signature": headers["webhook-signature"],
    })
    for event in payload["events"]:
        ...  # forward to your SIEM, dropping events whose id you have seen
```

```javascript JavaScript theme={null}
// npm install standardwebhooks
import { Webhook } from "standardwebhooks";

// The signing secret shown when you created the stream, e.g. "whsec_..."
const wh = new Webhook("YOUR_STREAM_SIGNING_SECRET");

function handleRequest(rawBody, headers) {
  // Throws if the signature or timestamp is invalid.
  const payload = wh.verify(rawBody, {
    "webhook-id": headers["webhook-id"],
    "webhook-timestamp": headers["webhook-timestamp"],
    "webhook-signature": headers["webhook-signature"],
  });
  for (const event of payload.events) {
    // forward to your SIEM, dropping events whose id you have seen
  }
}
```

To verify without a library, see [Verify manually](https://docs.openlayer.com/security/webhooks/verify-signatures.md#verify-manually);
the computation is identical.


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