Zeno dokumentáció
API-kulcsok lekérése

Szerver

Search events

Filter your project's events by visitor, external ID, IP, signal, score and date, and page through them.

GET /events searches your project's events, newest first. Every filter is optional, and filters combine (AND).

Shell
curl "https://api.zeno.id/events?signal=vpn&minRiskScore=6&limit=50" \
	-H "Auth-API-Key: sk_..."
JSON
{
	"events": [
		{
			"requestId": "1792578564800.yU4Ety",
			"apiKeyId": "7d0c5f0e-3b6a-4c1e-9a52-1f4e8b2d6c90",
			"visitorId": "q1yLOPpzvOstEQCFQQxE",
			"externalId": "user-2077",
			"action": "login",
			"origin": "https://example.com",
			"ip": "185.65.134.10",
			"country": "NL",
			"city": "Amsterdam",
			"asn": "AS39351",
			"company": "31173 Services AB",
			"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...",
			"browser": "Chrome 156",
			"os": "Windows 11",
			"detected": ["vpn", "abusive-ip"],
			"bot": "not_detected",
			"riskScore": 6,
			"confidence": 0.99,
			"createdAt": "2026-10-21T12:58:49.476Z"
		}
	],
	"cursor": "WyIyMDI2LTEwLTIxVDEyOjU4OjQ5LjQ3NloiLCIxNzkyNTc4NTY0ODAwLnlVNEV0eSJd",
	"found": 128
}

Filters

FilterExampleDescription
visitorIdq1yLOPpzvOstEQCFQQxEOne visitor.
externalIduser-2077The ID you attached, like an account in your app.
ip185.65.134.10An exact IP; no CIDR ranges.
requestId1792578564800.yU4EtyOne event.
apiKeyId7d0c5f0e-…The public key that identified, by its id (the event's apiKeyId).
countryNLTwo letters, in any case.
originhttps://example.comThe page's origin, exactly.
asnAS9009The network, with its AS.
browser, osChrome 156, Chrome, macOSA name with its version exactly as the search item shows it, or the name alone for every version. A browser has its major version (Chrome 156), an OS its version as the event's browserDetails has it (macOS 27.0.1, Windows 11).
actionsignupThe action passed to get() or set later.
signalvpnEvents that detected this signal.
botbadgood, bad or not_detected.
minRiskScore9A risk score of at least this.
start, end2026-10-01ISO 8601 dates or times, or epoch milliseconds; both included. A date alone is a UTC day: 00:00 UTC for start, the whole day for end. A time is UTC unless it carries an offset: Z, or +02:00 URL-encoded as %2B02:00 (an unencoded + reads as a space and answers 400). A start after end answers 200 with nothing found.
limit501 to 100, 20 by default.
cursorThe cursor of the previous page, unchanged.

apiKeyId, country, origin, asn, browser, os, action, signal and bot also take several values, up to 20, matching any of them: repeat the parameter, as in ?country=DE&country=NL. Python's requests does that for a list (params={"country": ["DE", "NL"]}), Go's url.Values with Add, and the SDK for an array. PHP's http_build_query, and so Laravel's Http::get($url, $array) and Guzzle's array query option, write country[0]=DE, which is refused as an unknown parameter: build the query with GuzzleHttp\Psr7\Query::build($filters), or join the name=value pairs yourself.

Names are case-sensitive. A parameter that isn't a filter, like externalID, signals or key (it's apiKeyId), answers 400 invalid_request naming it and the filter you probably meant, rather than being ignored and matching every event.

Results

found is the exact number of events matching the filters, on every page. A search item is flat and smaller than the event:

FieldDescription
requestId, apiKeyId, visitorIdAs in the event.
externalId, action, originAs in the event; absent when it has none. Search results leave metadata out; read the event for it.
ip, countryAs in the event.
city, asn, companyThe event's ipInfo.city, ipInfo.asn and ipInfo.company.
userAgentAs in the event.
browser, osName and version in one string, like Chrome 156 and macOS 27.0.1, from the event's browserDetails.
detected, bot, riskScore, confidence, createdAtAs in the event.

Search items leave out visitorMatch, returning, updatedAt, expiresAt, url, referrer, browserDetails, incognito, ipTimeZone, ipInfo, ja4, ja3, socketProof, riskScoreBreakdown, signals, detectorVersion, the signals' evidence (automationMarkers, gpuRenderer, vpn, proxy, tampering, tlsMismatch, virtualMachine) and velocity; get the event for those.

Paging

Pass the cursor of each page to get the next one. The last page has no cursor field at all. A cursor is opaque: pass it back unchanged with the same filters. One that isn't a page's answers 400 invalid_request, so a loop with a broken cursor stops instead of starting over.

const FP_SECRET_KEY = process.env.FP_SECRET_KEY!
let cursor: string | undefined

do {
	const query = new URLSearchParams({ externalId: 'user-2077' })
	if (cursor) query.set('cursor', cursor)

	const response = await fetch(`https://api.zeno.id/events?${query}`, {
		headers: { 'Auth-API-Key': FP_SECRET_KEY },
	})

	if (!response.ok) {
		throw new Error((await response.json()).message)
	}

	const page = await response.json()
	handle(page.events)
	cursor = page.cursor
} while (cursor)

Recipes

Risky signups this week

Shell
# start: a week before today
curl "https://api.zeno.id/events?action=signup&minRiskScore=9&start=2026-10-18" \
	-H "Auth-API-Key: sk_..."

On the dashboard: Events, filter Action is signup and Risk score at least 9, period 7d.

Everything one browser did

?visitorId=q1yLOPpzvOstEQCFQQxE lists its events with every external ID (here user-2077 and user-5120); the visitor API answers the same in one call, for its 20 latest.

Bad bots from one network

?bot=bad&asn=AS14061, or several networks with asn repeated.

Accounts sharing a browser

Easier with Related than by search: GET /fingerprints/shared?type=visitor-id lists the visitor IDs more than one external ID was seen with.