Device data collection
View as MarkdownThree 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.
| Signal | Where it comes from | Charge field |
|---|---|---|
| Browser fingerprint | wp.getFingerprint() | device.fingerprint |
| Kount device session | wp.getKountSessionId(), from Kount's collector | device.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.
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:
{ "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:
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:
{ "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:
{ "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:
const deviceSession = await wp.getDeviceSession(); // "ds_K7Q2…", or null
// POST it to your server with the order
Your server sends it as device_session:
{ "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_sessionswith a secret key is a403: 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
400withparam: "device_session"(resource_missingordevice_session_expired), and that refusal does not consume theIdempotency-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_sessiontoo, 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
