Skip to content

Export the audit log to your SIEM

Your SIEM wants every governance action — who approved a risk, who uploaded evidence, who changed a vendor’s criticality. Vendorica’s audit log is a cursor feed built for exactly this.

This guide builds a poller you can run every five minutes and leave alone.

You need: a key with audit:read (how to get one), and somewhere durable to store one string between runs.

GET /v1/audit-log is cursor-paginated, and the registers are not. That difference is the whole design of this integration.

A page number is a position in a list that is still growing. Ask for page 2 a minute after page 1 and new entries have pushed the old ones down — you re-read rows you already have and skip rows you never saw. On an append-only feed that is not a rare race, it is what happens every time.

A cursor is a position in the data. Hand back the cursor you finished on and you resume exactly there, however many entries arrived meanwhile.

  1. Read your stored cursor. On the very first run you have none — omit the parameter and the feed starts from the beginning.

  2. Fetch a page.

    Terminal window
    curl -sS "https://api.vendorica.com/v1/audit-log?limit=200&cursor=$CURSOR" \
    -H "Authorization: Bearer $VENDORICA_API_KEY"
  3. Ship the entries, then store the new cursor — in that order.

  4. Repeat while the response says there is more. Then sleep until the next run.

Storing the cursor only after a successful ship is what makes the loop crash-safe. Die mid-run and the next one re-reads the last page: your SIEM sees a handful of duplicates, which is recoverable, instead of a hole, which is not.

import os, time, json, pathlib, requests
BASE = "https://api.vendorica.com/v1"
KEY = os.environ["VENDORICA_API_KEY"]
STATE = pathlib.Path("/var/lib/vendorica/audit-cursor")
def fetch(cursor):
params = {"limit": 200}
if cursor:
params["cursor"] = cursor
r = requests.get(f"{BASE}/audit-log", params=params,
headers={"Authorization": f"Bearer {KEY}"}, timeout=30)
# Pace against the budget rather than waiting to be refused.
if int(r.headers.get("RateLimit-Remaining", 1)) < 5:
time.sleep(int(r.headers.get("RateLimit-Reset", 10)))
r.raise_for_status()
return r.json()
def run():
cursor = STATE.read_text().strip() if STATE.exists() else None
while True:
body = fetch(cursor)
entries = body["data"]
if not entries:
break
for entry in entries:
print(json.dumps(entry)) # your SIEM's ingest goes here
# Only after the entries are safely away.
meta = body["cursor"]
cursor = meta["nextCursor"]
STATE.write_text(cursor or "")
if not meta["hasMore"]:
break
run()

The cursor metadata is its own top-level key, cursor, not part of pagination — the offset-paginated registers use pagination, and the two feeds are shaped differently on purpose:

{
"success": true,
"data": [ ],
"cursor": { "nextCursor": "", "hasMore": true, "limit": 200 },
"timestamp": "2026-08-30T18:32:11.004Z",
"requestId": "req_01J…"
}

Trust the audit-log operation in the reference over this page for field names: that one is generated from the service, and this one is written by hand.

The feed takes filters, and applying them server-side beats filtering after transfer:

entityType, entityId one register, or one record’s whole history
operation create, update, delete
userId one person’s actions
severity the entries worth alerting on
createdFrom, createdTo a bounded window, for a backfill

For a genuine SIEM feed, take everything. You do not know today which field next year’s investigation turns on, and the volume is governance events, not application logs.

Five minutes is a good default. The audit log is not a latency-sensitive surface — nobody pages on a governance event within seconds — and a five-minute poll with limit=200 keeps you far inside any rate budget.

Check RateLimit-Remaining on every response and back off on it, as the sample does. See rate limits.

Evidence flagged sensitive is restricted to named people, and a key is nobody in particular. Those entries appear in the feed with their metadata, but the content behind them is not retrievable with a key. That is deliberate and will not be lifted — see Authentication.