Skip to content
Logo

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

IntegrationIP address and user agentDevice fingerprint
Hosted verification linkTaken from the customer's own requestComputed by the hosted page
Seismic dashboardTaken from the requestComputed by the dashboard
Your own frontend, submitting from your server with an API keyYou send themYou 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:

{
  "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"
  }
}
FieldFormat
end_user.ip_addressIPv4 or IPv6 address.
end_user.device_fingerprintPrintable ASCII, 1 to 128 characters. Any encoding.
end_user.user_agentPrintable 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.userAgent
  • navigator.language and navigator.languages
  • navigator.platform
  • navigator.hardwareConcurrency (CPU core count)
  • screen.width, screen.height and screen.colorDepth
  • window.devicePixelRatio
  • navigator.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-For or X-Real-IP.
  • The left-most X-Forwarded-For entry 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 proxy setting, 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.0 or 127.0.0.1 as 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"
    }
  }'