Built to fit your stack
Plain REST with one header, a typed Node client, an MCP server, signed webhooks and custom domains. Nothing to install on your server unless you want to.
Five ways in
The same event, from a shell, your code, an assistant or a push.
curl "https://api.zeno.id/events/1792578564800.yU4Ety" \
-H "Auth-API-Key: sk_..."const event = await fetch(`https://api.zeno.id/events/${requestId}`, {
headers: { 'Auth-API-Key': process.env.FP_SECRET_KEY },
}).then(response => response.json())import { createClient } from '@zeno-id/server'
const fingerprint = createClient({ secretKey: process.env.FP_SECRET_KEY, endpoint: 'https://api.zeno.id' })
const event = await fingerprint.getEvent(requestId)
const page = await fingerprint.searchEvents({ externalId: user.id, signal: 'vpn' })
await fingerprint.updateEvent(requestId, { action: 'checkout' })
await fingerprint.forgetExternalId(user.id)# Your project's API as tools for Claude Code, Cursor or any MCP client.
claude mcp add zeno --env FP_SECRET_KEY=sk_... --env FP_ENDPOINT=https://api.zeno.id -- npx -y @zeno-id/mcpimport { verifyWebhook } from '@zeno-id/server'
app.post('/fingerprint', express.text({ type: 'application/json' }), (request, response) => {
if (!verifyWebhook(request.body, request.header('fingerprint-signature') ?? '', process.env.FP_WEBHOOK_SECRET)) {
return response.sendStatus(401)
}
const event = JSON.parse(request.body)
response.sendStatus(200)
})// Serve the agent and identify from a subdomain of your own site, so content
// blockers that list ours don't drop identifications. Add it in Settings ›
// Custom domain, create the one DNS record it shows, then:
import { start } from 'zeno-id'
const agent = start({ publicKey: 'pk_...', endpoint: 'https://metrics.example.com' })- Server API Read, search and erase events and visitors with one header.
- Server SDK A typed Node client with webhook checks and sealed result opening.
- MCP server Your project's API as tools for an AI assistant.
- Webhooks Every new event POSTed to your server, signed, with retries.
- Custom domain Serve the agent from a subdomain of yours with one DNS record, as every live site should.
- Sealed results An encrypted result your server opens without calling the API.
The agent in any page
Start it when the form opens and ask for a request ID once the user acts. It runs in the background and never blocks the page.
- No dependencies
- /agent.js as a script tag to try it, or a small npm loader that fetches the current agent through your own domain.
- Any framework
- React, Svelte, Vue, Next.js, Nuxt, Angular, or none at all.
- About a kilobyte
- The report it sends. It never reads WebRTC network addresses.
- Cached
- A result can be kept for the session, so a login and the checkout after it cost one identification.
<script src="https://zenocdn.net/agent.js"></script>
<script>
const agent = FingerprintAgent.start({ endpoint: 'https://zenocdn.net', publicKey: 'pk_...' })
// Once the user logs in, hand the request ID to your server:
agent.get({ externalId: user.id }).then(({ requestId }) => verifyOnServer(requestId))
</script>Your data, your rules
Built for the data requests you'll get.
Hosted in the EU
Events are stored in AWS eu-west-1 until you delete them, or for 1, 3 or 5 years if you pick that.
Erase an external ID in one call
Delete an external ID and every event and device link of it is gone, for a data request or a closed account.
Nothing for the browser to tune against
Flags never reach the browser. Zero trust mode goes further: the browser gets a request ID and nothing else.
Your first identification, in minutes
A public key in the browser, a secret key on your server.