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.
Why a cursor, not a page number
Section titled “Why a cursor, not a page number”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.
The loop
Section titled “The loop”-
Read your stored cursor. On the very first run you have none — omit the parameter and the feed starts from the beginning.
-
Fetch a page.
Terminal window curl -sS "https://api.vendorica.com/v1/audit-log?limit=200&cursor=$CURSOR" \-H "Authorization: Bearer $VENDORICA_API_KEY" -
Ship the entries, then store the new cursor — in that order.
-
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.
A worked poller
Section titled “A worked poller”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.
Narrowing what you ship
Section titled “Narrowing what you ship”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.
Scheduling
Section titled “Scheduling”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.
What you will not see
Section titled “What you will not see”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.