Zeno Doku
API-Keys abrufen

Server

Related

Link your external IDs to the visitors and IPs they were seen with, find duplicates and the external IDs related to one.

An external ID is whatever you attach to an identification: an account, a customer or an order in your own system. Each one is linked to the visitors it was seen with (type visitor-id) and its IP addresses (type ip). The API counts how many external IDs share each value, so you can tell whether a new one belongs to someone you already know. The related IDs answer calls external IDs accounts, like the dashboard; it's the same thing.

An event with an externalId, sent by the agent or attached later with PATCH /events/{requestId}, links its visitor id and IP by itself. An event without one is identified but links nothing, so pass your own ID whenever you have it. Values seen elsewhere, like the IP of a payment or an API call, you link from your server.

The examples follow the browser from the Visitors sample, q1yLOPpzvOstEQCFQQxE, and three of your users:

  • user-2077 logs in on that browser, from her home IP 198.51.100.7 and once through a VPN exit, 185.65.134.10;
  • user-5120 signed up on the same browser, from the same home IP, the next day;
  • user-1042, a housemate, uses his own laptop on that home IP.

Your shop saw user-2077 pay from 203.0.113.9, outside the browser. Link it:

Shell
BODY='{"externalId":"user-2077","type":"ip","value":"203.0.113.9"}'

curl -X POST "https://api.zeno.id/fingerprints" \
	-H "Auth-API-Key: sk_..." \
	-H "Content-Type: application/json" \
	-H "x-amz-content-sha256: $(printf '%s' "$BODY" | openssl dgst -sha256 -r | cut -d' ' -f1)" \
	--data-binary "$BODY"

Answers 201 with the link it stored, the same JSON you sent, every time: linking the same external ID to the same value again changes nothing and answers 201 again. Only type: "ip" links by hand, and value must be an IPv4 or IPv6 address: a visitor id links only from an event with an external ID, since only that shows the user was seen in that browser. A call with a body sends its SHA-256 in x-amz-content-sha256, see Server API; the SDK's linkFingerprint does it for you.

Check an external ID

Shell
curl "https://api.zeno.id/fingerprints?externalId=user-2077" \
	-H "Auth-API-Key: sk_..."
JSON
{
	"externalId": "user-2077",
	"identified": true,
	"duplicate": true,
	"fingerprints": [
		{ "type": "visitor-id", "value": "q1yLOPpzvOstEQCFQQxE", "createdAt": "2026-10-21T12:58:49.476Z" },
		{ "type": "ip", "value": "185.65.134.10", "createdAt": "2026-10-21T12:58:49.476Z" },
		{ "type": "ip", "value": "198.51.100.7", "createdAt": "2026-10-20T10:10:23.117Z" },
		{ "type": "ip", "value": "203.0.113.9", "createdAt": "2026-10-23T08:41:17.902Z" }
	]
}
  • duplicate is true when the external ID shares a visitor id with another one: here user-5120's browser. A shared IP doesn't count, so user-1042 on the home IP doesn't make one.
  • duplicate doesn't look at confidence: every event with an external ID links its visitor id, a low-confidence match too. Before acting on a duplicate, read the events behind it (search with visitorId) and weigh those with confidence below 0.8 less. And a duplicate is a lead, not proof: see Shared visitor ids.
  • identified is true once an event with this external ID linked a visitor id, so it was seen in a browser. False means it never was: treat that as unverified rather than clear, since a duplicate: false then proves nothing.

An external ID you never sent answers 200 with identified: false, duplicate: false and no fingerprints, not a 404. Add &type=visitor-id for one type only.

External IDs of a value

Shell
curl "https://api.zeno.id/fingerprints?type=ip&value=198.51.100.7&limit=20" \
	-H "Auth-API-Key: sk_..."
JSON
{
	"items": [
		{ "externalId": "user-1042", "createdAt": "2026-10-19T19:02:11.000Z" },
		{ "externalId": "user-2077", "createdAt": "2026-10-20T10:10:23.117Z" },
		{ "externalId": "user-5120", "createdAt": "2026-10-22T09:14:02.310Z" }
	]
}

limit is 1 to 100, 20 by default. A page with more after it has a cursor: pass it back unchanged for the next one; the last page has none. A cursor that isn't the previous page's answers 400, so a broken paging loop stops. GET /fingerprints takes either externalId or type and value; neither answers 400. A value nobody was seen with answers 200 with no items.

Most shared values

Shell
curl "https://api.zeno.id/fingerprints/shared?type=visitor-id" \
	-H "Auth-API-Key: sk_..."
JSON
{
	"items": [{ "type": "visitor-id", "value": "q1yLOPpzvOstEQCFQQxE", "count": 2 }]
}

The values of a type that more than one external ID shares, most shared first. type is required; limit and cursor work as above. With type=ip, this project would list 198.51.100.7 with a count of 3.

Shell
curl "https://api.zeno.id/fingerprints/related?externalId=user-2077" \
	-H "Auth-API-Key: sk_..."

Follows shared values of one type (visitor-id by default) up to depth hops (1 to 5, 1 by default). At depth 1 you get the external IDs that share a value with user-2077; at depth 2 also those that share a value with one of them, and so on.

JSON
{
	"externalId": "user-2077",
	"type": "visitor-id",
	"connections": [{ "externalId": "user-5120", "value": "q1yLOPpzvOstEQCFQQxE", "createdAt": "2026-10-22T09:14:02.310Z" }],
	"accounts": [
		{
			"externalId": "user-2077",
			"connections": [{ "externalId": "user-5120", "value": "q1yLOPpzvOstEQCFQQxE", "createdAt": "2026-10-22T09:14:02.310Z" }]
		},
		{
			"externalId": "user-5120",
			"connections": [{ "externalId": "user-2077", "value": "q1yLOPpzvOstEQCFQQxE", "createdAt": "2026-10-21T12:58:49.476Z" }]
		}
	]
}

connections are the external IDs user-2077 itself shares a value with, each with the other side's link time. accounts is every external ID reached, each with every external ID it shares a value with among the links the walk read, so a link shows on both sides: that's the graph to draw. An external ID with no links answers 200 with empty lists.

The same for user-5120 by IP, ?externalId=user-5120&type=ip, reaches the housemate through the home IP:

JSON
{
	"externalId": "user-5120",
	"type": "ip",
	"connections": [
		{ "externalId": "user-1042", "value": "198.51.100.7", "createdAt": "2026-10-19T19:02:11.000Z" },
		{ "externalId": "user-2077", "value": "198.51.100.7", "createdAt": "2026-10-20T10:10:23.117Z" }
	],
	"accounts": [
		{ "externalId": "user-5120", "connections": [{ "externalId": "user-1042", "value": "198.51.100.7", "createdAt": "2026-10-19T19:02:11.000Z" }, { "externalId": "user-2077", "value": "198.51.100.7", "createdAt": "2026-10-20T10:10:23.117Z" }] },
		{ "externalId": "user-1042", "connections": [{ "externalId": "user-2077", "value": "198.51.100.7", "createdAt": "2026-10-20T10:10:23.117Z" }, { "externalId": "user-5120", "value": "198.51.100.7", "createdAt": "2026-10-22T09:14:02.310Z" }] },
		{ "externalId": "user-2077", "connections": [{ "externalId": "user-1042", "value": "198.51.100.7", "createdAt": "2026-10-19T19:02:11.000Z" }, { "externalId": "user-5120", "value": "198.51.100.7", "createdAt": "2026-10-22T09:14:02.310Z" }] }
	]
}

The walk never goes through a value more than 50 external IDs share (a NAT, office or VPN exit, say), so one busy IP doesn't pull in half your users: if many of your users share the VPN exit 185.65.134.10, it never shows here.

  • A shared visitor id is the strong link: the same browser was used for both, like user-2077 and user-5120. Still check the rest: identical phones and a shared family computer look the same.
  • A shared IP is weak on its own. Offices, schools, mobile networks and VPN exits put many unrelated people on one IP, like the housemate user-1042 here, so a VPN exit can show up as "3 external IDs share this IP" right next to a real ring. Look at the visitor ids first, and treat an IP link as supporting detail.
  • Depth widens the net fast. Depth 1 or 2 on visitor ids finds most rings; deeper or on IPs, expect strangers.

In the dashboard

The dashboard's Related page does the same without code. Search for an external ID to see its visitor ids, IPs and whether it's a duplicate, then its related external IDs as a tree (three hops at most), a network graph (drag to pin, zoom, filter by type) or a table. Most shared lists the values more than one external ID shares.

Erasure

Forgetting an external ID or deleting a visitor also drops their links and uncounts them. Deleting a visitor also drops the links to IPs that only its events made. Otherwise a link expires with the project's retention, counted from the latest event that made it, and is uncounted then.