Device data collection

View as Markdown

Three independent device signals can travel with a charge. All are optional, all improve your approval rate, and none can block a payment if it fails. That means a broken CSP or blocked collector looks exactly like a healthy integration with empty device data. If scores look “soft” and device.* is empty, fix CSP first. See Integration hard rules §7.

SignalWhere it comes fromCharge field
Browser fingerprintwp.getFingerprint()device.fingerprint
Kount device sessionwp.getKountSessionId(), from Kount's collectordevice.kount_session_id
Device session (the buyer's IP and browser)wp.getDeviceSession()device_session

Using the Payment Element? You are already covered. The frame collects the first two signals and attaches them to the charge, and it confirms from the buyer's browser, so the charge already carries the buyer's IP. This page is for building the charge yourself: you read the signals in the browser, send them to your server, and put them on POST /charges.


Browser fingerprint#

wp.getFingerprint() returns a stable per-browser id, cached in localStorage for 24 hours. It resolves null rather than throwing, and it is safe to call repeatedly: the cached value is reused.

js
const wp = WorthyPayments('pk_live_xxx');

const fingerprint = await wp.getFingerprint();
// POST it to your server alongside the rest of the order

Your server then puts it on the charge:

json
{ "device": { "fingerprint": "tp_fp_9f2c..." } }

The fingerprint library is fetched lazily from a CDN the first time you call it, so a blocked CDN yields null. Charge anyway. A device signal is enrichment, and treating null as a hard failure would decline good customers over a browser extension.


Kount device session#

wp.getKountSessionId() runs Kount's Device Data Collector in the buyer's browser and returns the session id it produced:

js
const kountSessionId = await wp.getKountSessionId();  // null when DDC is off

Send it to your server with the fingerprint, and put both in the device block:

json
{ "device": { "fingerprint": "tp_fp_9f2c...", "kount_session_id": "abc123..." } }

Our fraud engine joins the profile Kount collected to the charge using that id. A charge with the field empty is scored without a device signal.

The collector has to run in the buyer's browser, so there is no server-side substitute. Collect it on the page, then charge from your server.

It never blocks a payment#

Every failure path resolves to a null session id and the charge proceeds: collection disabled for your account, an unreachable config endpoint, a collector iframe blocked by CSP or an extension. Collection is capped at 1.5 seconds, because the collect-end callback never fires when the frame is blocked, and a checkout that waits forever for a fraud signal has turned an optional input into a hard dependency.

The flip side: a missing device signal is silent by design. If you expect Kount data and see none, check your CSP first (below), then whether GET /v1/kount-config reports enabled: true for your account.

Configuration#

The SDK reads GET /v1/kount-config. The endpoint is scope-free, so a publishable key can call it:

json
{ "object": "kount_config", "mode": "test", "client_id": "900900",
  "environment": "sandbox", "enabled": true }

The Kount environment follows your key: a test key always collects against the sandbox tenant. enabled: false means your account is not provisioned for DDC, and the SDK skips collection entirely.

An absent client_id disables collection too, whatever enabled says. The SDK needs both, so {"enabled": true, "client_id": null} yields no session id and getKountSessionId() resolves null. The same outcome as enabled: false, reached a different way. If you expect device data and are getting null, read this endpoint before assuming the collector is at fault.


Device session: the buyer's IP and browser#

The IP a charge is scored on (per-IP velocity, the IP blocklist, the cascade) is always the connection we see. It is never read from the request body, because an IP a caller could name would let anyone rotate past those rules. When your server sends the charge with a secret key, the connection we see is your server, the same address for every one of your buyers. We record that address as your server's and never count it as a buyer IP, so it cannot get your whole store blocked, but the charge then has no buyer IP to score on.

wp.getDeviceSession() closes that gap, the way Stripe.js does with a Radar session. It calls POST /v1/device_sessions from the buyer's browser with your publishable key; we record the IP the request comes from, its user agent and the fingerprint, and answer an opaque id:

js
const deviceSession = await wp.getDeviceSession();  // "ds_K7Q2…", or null
// POST it to your server with the order

Your server sends it as device_session:

json
{ "amount": 4999, "currency": "usd",
  "payment_method": { "payment_method_id": "pm_..." },
  "initiated_by": "customer",
  "device_session": "ds_K7Q2M4ZJ3WXH5N6P8R2T4V6YB3" }

The charge then carries the buyer's IP and user agent, and the session's fingerprint when you sent no device.fingerprint.

  • Call it when the buyer pays. A session lasts 30 minutes. The SDK caches it and fetches a new one while the cached one still has ten minutes left, so a page left open still hands your server a live id. One request is shared by concurrent calls.
  • Reusable within those 30 minutes. The same id may go on an idempotent retry of the charge or on a second card after a decline. It only ever reports the connection that created it.
  • Scoped to your account and mode. A session created with pk_test_… is unknown to a live key, and another account's session is unknown to yours.
  • Publishable key only. POST /v1/device_sessions with a secret key is a 403: from your server it would record your server again.
  • A refused session never costs the payment, if you retry. An unknown or expired id is a 400 with param: "device_session" (resource_missing or device_session_expired), and that refusal does not consume the Idempotency-Key. Send the same charge again with a fresh session or without one. If our fraud service cannot read a session, the charge goes ahead without it.
  • A renewal has no buyer at a browser, so it has no session: charge it without one.
  • Confirming an intent from your server works the same way. A payment intent or setup intent confirmed with a secret or restricted key takes device_session too, and without one your server's address is never scored as the buyer's. A confirm from the buyer's browser (the Payment Element) needs none.

getDeviceSession() resolves null on any failure. Charge anyway: without a session the charge behaves exactly as before.


Content-Security-Policy#

If your checkout sets a CSP, the SDK needs these for fingerprinting and Kount's collector:

script-src  https://app.worthypayments.com
connect-src https://api.worthypayments.com
frame-src   https://*.kaptcha.com

app.worthypayments.com serves the fingerprint library (same host as the SDK; a page that loads the SDK from app.paygeyt.com gets it from there, with api.paygeyt.com), and *.kaptcha.com is for Kount's collector, which runs in a hidden iframe.

js.evervault.com (script-src) and *.evervault.com (connect-src, frame-src) are needed on your page only if you show Apple Pay / Google Pay through the Evervault wallet rail (GET /v1/wallet-config reports provider: "EVERVAULT"; the SDK then loads Evervault on your page to mount the buttons) or run a 3-D Secure challenge yourself on your page. The Payment Element encrypts the card and runs its challenge inside its own frame, whose own policy already allows Evervault, so card entry and 3-D Secure through it add nothing to your CSP.

Offering Apple Pay and GET /v1/wallet-config reports applePay: true? Add https://applepay.cdn-apple.com to script-src. Safari blocks Apple's own script without it while Google Pay keeps working, so it presents as an Apple-specific bug rather than a policy problem. Skip this directive when wallets are off (provider: "NONE" is the default).

Every one of these fails silently. A blocked fingerprint or a blocked collector costs a fraud signal rather than the sale, which is right in production and confusing while integrating. When a device signal is unexpectedly empty, look for CSP violations in the console before anything else.


All three signals are optional on POST /charges. The Payment Element needs none of them from you; build the charge yourself and they are yours to collect.

Related: Payment Element · Embedded SDK · Charges · Fraud checks