Device fingerprinting
Every verification submit is also the customer's acceptance of the terms of service. Seismic records that acceptance against the verification, together with three pieces of evidence about where it was made:
- the IP address the customer accepted from,
- the user agent of the browser they accepted in,
- a device fingerprint: an opaque identifier for the device they accepted on.
Reviewers use the three together to corroborate that the person who accepted is the person being verified. None of them is parsed or scored: they are recorded as sent and shown to a reviewer. That makes them only as useful as they are accurate — a value that belongs to your server, or changes on every request, is worse than none, because it contradicts the record instead of supporting it.
Who collects what
| Integration | IP address and user agent | Device fingerprint |
|---|---|---|
| Hosted verification link | Taken from the customer's own request | Computed by the hosted page |
| Seismic dashboard | Taken from the request | Computed by the dashboard |
| Your own frontend, submitting from your server with an API key | You send them | You send it |
If your customers complete verification on a hosted link, there is nothing to do. The rest of this page is for integrations that collect consent in their own UI and call the submit endpoint server-to-server.
The request fields
The evidence rides in an end_user block on:
POST /v0/customers/{id}/kyc/submit— the whole request body.POST /v0/org/kyb/submit— beside the profile keys, for your own organization's verification.
{
"end_user": {
"ip_address": "203.0.113.7",
"device_fingerprint": "3f9b8c1e5a7d2b4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d1e3f5a7b9c1d",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
}
}| Field | Format |
|---|---|
end_user.ip_address | IPv4 or IPv6 address. |
end_user.device_fingerprint | Printable ASCII, 1 to 128 characters. Any encoding. |
end_user.user_agent | Printable ASCII, 1 to 512 characters. |
All three are optional today. Starting November 1, 2026, a customer submit made with an API key will require all three.
A value outside these limits returns 400 InvalidRequest naming the field. Sending ip_address or user_agent on a request made from the customer's own browser — a hosted link, or the dashboard — also returns 400 InvalidRequest: there the request itself is the evidence, and a supplied value would be second-hand.
How Seismic computes the fingerprint
The dashboard and the hosted verification link do not use a third-party fingerprinting library. They take a SHA-256 digest, with the browser's built-in Web Crypto API, of signals that stay the same across sessions on one device:
navigator.userAgentnavigator.languageandnavigator.languagesnavigator.platformnavigator.hardwareConcurrency(CPU core count)screen.width,screen.heightandscreen.colorDepthwindow.devicePixelRationavigator.maxTouchPoints- the time zone name and current UTC offset
The result is a 64-character lowercase hex string. Only the digest leaves the browser.
Libraries usually add canvas, WebGL and audio probes for extra entropy. Those are left out on purpose: Safari, Firefox with fingerprinting protection, and privacy-focused browsers randomize them per session, so the same person accepting twice would produce two different fingerprints. Here a stable value is worth more than a unique one.
Use the same function
The TypeScript client exports the same computation as getDeviceFingerprint(), with no added dependencies. A customer who touches both your UI and a hosted link then reads as one device.
// In the customer's browser, when they accept the terms.
const = await ();It resolves to undefined outside a browser or outside a secure (HTTPS) context, where Web Crypto is unavailable. Send the submit without the field rather than inventing a value.
Bringing your own
Any identifier that meets the format above is accepted, so if you already fingerprint devices — for example with the open-source FingerprintJS library — send that. Send the library's stable visitor identifier, not its raw component output: a component payload is not an identifier and is too long to be accepted. Prefer configurations that avoid the per-session randomized probes described above.
Collecting the evidence
Device fingerprint
- Compute it in the customer's browser or app, at the moment they accept the terms, and send it to your server with the rest of the form.
- Never compute it on your server. A fingerprint of your server is the same for every customer.
For a native mobile app, which has no browser to fingerprint, send a stable per-installation identifier such as iOS's identifierForVendor or a random UUID generated on first launch and kept in app storage. Do not use advertising identifiers.
IP address
Send the public address the customer's device connected to you from — not your server's address, and not an address from inside your own network.
- Behind a load balancer or CDN, the connecting peer is your infrastructure. Read the client address from the header that infrastructure sets, such as
X-Forwarded-FororX-Real-IP. - The left-most
X-Forwarded-Forentry is whatever the client chose to send. Walk the list from the right and take the first address your own infrastructure did not add, or use the single-value header your edge overwrites on every request. - Many frameworks do this for you once told which proxies to trust — Express's
trust proxysetting, for example.
User agent
Send the User-Agent header the customer's browser sent to your server, or navigator.userAgent captured alongside the fingerprint. Not the user agent of the HTTP library your server uses to call the API.
From a native app, send the user agent your app sends on its own requests, naming the app and OS version.
Timing
Capture all three on the request where the customer accepted. If the submit happens later — after a review step on your side, or from a background job — store the three values with the acceptance and send those, not values from whatever request triggers the submit.
What not to send
- Personal data. No email address, phone number, name or account number, raw or hashed: a hash of an email address still identifies the person.
- A fresh value per request. A random UUID or a timestamp on every submit says nothing about the device.
- Constants.
"unknown","web","n/a", or one value shared by all your customers. Omit the field instead. - Your own infrastructure's values. Your server's IP address, a private-range address from an internal hop, or your HTTP library's user agent.
- Fabricated addresses.
0.0.0.0or127.0.0.1as a stand-in for an address you did not capture.
End to end
In the browser, alongside the consent checkbox:
await ("/api/onboarding/submit", {
: "POST",
: { "Content-Type": "application/json" },
: .({
,
: true,
: await (),
}),
});On your server (Express shown, with app.set("trust proxy", …) configured for your proxies so req.ip is the customer's address):
app.post("/api/onboarding/submit", async (req, res) => {
const state = await api.customers.submit(req.body.customerId, {
endUser: {
ipAddress: req.ip,
userAgent: req.get("user-agent"),
deviceFingerprint: req.body.deviceFingerprint,
},
});
res.json(state);
});The same submit over HTTP:
curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/customers/$CUSTOMER_ID/kyc/submit" \
-H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"end_user": {
"ip_address": "203.0.113.7",
"device_fingerprint": "3f9b8c1e5a7d2b4c6e8f0a1b3c5d7e9f2a4b6c8d0e1f3a5b7c9d1e3f5a7b9c1d",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
}
}'