Publishable pk_live_…

Goes in your page. Anyone can read it, so it only works from the domains you register, and all it can do is ask who a browser is.

Secret sk_live_…

Stays on your server. This is the one that gives you an answer worth acting on. Never put it in a page, an app bundle, or anything else a user receives.

Three steps

  1. Put the script on the page

    It runs when the browser is idle, so it never gets in the way of anything the visitor came for. Give it a field and it fills that field in for you.

    <script src="https://abusescore.com/js/fingerprint.js"
            data-key="pk_live_…"
            data-field="#visitor-id"
            defer></script>
    
    <form method="post" action="/checkout">
        <input type="hidden" id="visitor-id" name="visitor_id">
        …
    </form>

    Prefer to drive it yourself? new Fingerprint('pk_live_…') gives you .get(), .identify() and .field(). All three resolve, always, including when we are having a bad day. A fraud check is not worth somebody's checkout.

  2. Check it from your server

    This is the step that matters, and it is the one people skip.

    curl https://abusescore.com/v1/fingerprint/$VISITOR_ID \
      -H "Authorization: Bearer sk_live_…"
  3. Use the answer

    An account three people have signed into this week. A "new customer" whose browser you have seen forty times. A card that keeps being declined from the same machine under different names. You get the history, so you can write the rule that fits your fraud, not ours.

Never decide anything on what the page told you

The result your JavaScript receives came from the visitor's own browser and went back to the visitor's own browser. Someone who wants to get past you controls both ends of that, and changing a value in a console takes about four seconds.

Take the visitor id from the page, send it to your backend, and ask us about it with your secret key. That answer arrives on a connection the browser was never part of. It is the only one worth a decision.

Why it still works next month

Most fingerprinting stops working and nobody notices. Browsers update every four weeks, graphics drivers rewrite themselves, people plug in a monitor or get on a plane. Hash all of that together and one changed detail makes yesterday's customer a stranger. Four things are done about it.

Nothing is hashed together

Every signal is measured and compared on its own, and the result is weighed. A browser update moves one or two of them and the visitor survives it.

A browser that lies is caught lying

Some browsers deliberately return a slightly different answer every time. We measure twice, notice, and drop that signal instead of letting it split one person into a new visitor on every visit.

Storage first, fingerprint second

If the browser still has its id, that is the answer. The fingerprint is how we find somebody again after they clear it, which is exactly when you want it.

The confidence is real

A locked-down browser gets a low number, because that is the truth. Anything that answers 95 for everything has moved you from unsure to confidently wrong.

What gets measured

22 signals, each worth what it is actually worth rather than what it sounds like. The raw values never leave the browser: the page sends a hash of each one, so nothing we store can be turned back into your visitor's font list.

Signal Stability Weight
platform core 12
colorDepth core 6
hardwareConcurrency core 12
deviceMemory core 10
engine core 10
canvas stable 90
webgl stable 75
fonts stable 65
audio stable 60
fontPreferences stable 40
screen stable 35
speech stable 30
timezone stable 28
languages stable 25
cssFeatures stable 20
pixelRatio stable 18
touch stable 15
vendorFlavors stable 15
math stable 12
sensors stable 8
viewport volatile not counted
uaVersion volatile not counted

Signals marked not counted are collected because they are useful to look at and are worth nothing for recognition: a window gets resized, a browser gets updated.

Reference

Generated from the OpenAPI document, which is the same one your tooling reads. Paste that URL into Swagger Editor, Postman or Insomnia and you have a client.

GET /v1/lookup/{ip} Secret key

Score one address

Everything known about an address, and a risk score from 0 to 100 with the reason codes that produced it.

Which fields come back depends on your plan. Every tier is a superset of the one below it, so upgrading never makes a field disappear. GET /v1/me lists exactly what yours includes.

A score of 0 with no reasons is a real answer, not a miss. Most of the address space is on no list at all, and that is worth knowing.

Private, loopback, link-local and reserved addresses are refused rather than scored. One arriving here almost always means you are reading your own load balancer's address instead of your visitor's, and a confident "clean" would let you ship that mistake.

Path

NameTypeNotes
ip string

Responses

StatusCodeMeaning
200 not an error The answer, filtered to your plan.
401 invalid_key Send a secret key. Publishable keys cannot make lookups.
402 quota_exhausted The allowance is used up, or the subscription is not active.
422 invalid_address Not an address, or not a public one.
429 rate_limited Over the per-second rate limit. Slow down and retry.
503 unavailable Temporarily unavailable. Retry.

Example response

{
    "ip": "45.66.1.9",
    "version": 4,
    "score": 60,
    "band": "medium",
    "is_bogon": false,
    "country": "SC",
    "continent": "AF",
    "flags": [
        "tor_exit",
        "hosting"
    ],
    "asn": 64496,
    "as_name": "Example Networks",
    "as_domain": "example.net",
    "network": "hosting",
    "prefix": "45.66.0.0/16",
    "reasons": [
        {
            "code": "TOR_EXIT",
            "weight": 55,
            "category": "anonymity",
            "detail": "Traffic leaves the Tor network here. Whoever sent it is deliberately anonymous."
        },
        {
            "code": "HOSTING_ASN",
            "weight": 30,
            "category": "anonymity",
            "detail": "A datacentre, not a home or an office. People browse from homes."
        }
    ],
    "categories": {
        "anonymity": {
            "raw": 85,
            "cap": 60,
            "applied": 60
        }
    },
    "sources": 2,
    "data_age_seconds": 3421
}
FieldTypeWhat it is
ip string
version integer 4 · 6
score integer Higher is riskier. Built from the reason codes below, capped per category so three blocklists describing one botnet do not count three times.
band string The score bucketed, for when you want a label rather than a number. clean · low · medium · high
is_bogon boolean An address that should never appear on the public internet.
country string or null ISO 3166-1 alpha-2.
continent string or null
flags string[] What this address is. Tor exit, Tor relay, VPN and privacy relay are four different things and never collapse into one. tor_exit · tor_relay · vpn · proxy · hosting · cloud · privacy_relay · abuse · abuse_l1 · scanner · bogon · mobile · business · residential · education · government · anycast · geo_disagree
asn integer or null
as_name string or null
as_domain string or null
network string or null What kind of network it is, where that is known.
prefix string or null The range this answer covers. CIDR where it is exactly one prefix, start-end otherwise.
reasons Reason[] Why the score is what it is, biggest mover first. This is the part worth building a rule on.
categories object The arithmetic: each category's raw total, its cap, and what was actually applied.
sources integer How many independent sources described this range.
data_age_seconds integer How old the compiled data is.
POST /v1/lookup Secret key

Score several addresses

One round trip instead of five hundred. The limit per request is your plan's bulk_max; the Free plan has none.

The whole batch is charged against your allowance before any of it is answered, so you never get a partial result you have already paid for. A malformed entry does not fail the batch: it comes back as an object with an error instead of a score.

Body

FieldTypeWhat it is
ips required string[] Up to your plan's bulk_max.

Example request

{
    "ips": [
        "45.66.1.9",
        "8.8.8.8",
        "2001:4860:4860::8888"
    ]
}

Responses

StatusCodeMeaning
200 not an error The answers, in the order they were sent.
400 invalid_body The body is not {"ips": [...]}.
401 invalid_key Send a secret key.
402 quota_exhausted The allowance is used up.
403 bulk_not_included This plan does not include bulk lookups.
422 bulk_too_large More addresses than the plan accepts in one request.
429 rate_limited Over the per-second rate limit.

Example response

{
    "results": [
        {
            "ip": "45.66.1.9",
            "version": 4,
            "score": 60,
            "band": "medium",
            "is_bogon": false,
            "country": "SC",
            "continent": "AF",
            "flags": [
                "tor_exit",
                "hosting"
            ],
            "asn": 64496,
            "as_name": "Example Networks",
            "as_domain": "example.net",
            "network": "hosting",
            "prefix": "45.66.0.0/16",
            "reasons": [
                {
                    "code": "TOR_EXIT",
                    "weight": 55,
                    "category": "anonymity",
                    "detail": "Traffic leaves the Tor network here. Whoever sent it is deliberately anonymous."
                },
                {
                    "code": "HOSTING_ASN",
                    "weight": 30,
                    "category": "anonymity",
                    "detail": "A datacentre, not a home or an office. People browse from homes."
                }
            ],
            "categories": {
                "anonymity": {
                    "raw": 85,
                    "cap": 60,
                    "applied": 60
                }
            },
            "sources": 2,
            "data_age_seconds": 3421
        },
        {
            "ip": "nonsense",
            "error": "\"nonsense\" is not an IP address."
        }
    ],
    "count": 2
}
FieldTypeWhat it is
results Answer[]
count integer
GET /v1/me Secret key

Your plan, your quota, your fields

What this key can do and how much of it is left. Costs nothing against your allowance, so an integration can check it on boot and a human can check it while debugging a quota problem without spending quota to find out what their quota is.

Responses

StatusCodeMeaning
200 not an error Your account.
401 invalid_key Send a secret key.

Example response

{
    "plan": {
        "id": "basic",
        "name": "Basic",
        "status": "active",
        "active": true,
        "field_tier": "basic",
        "fields": [
            "ip",
            "version",
            "score",
            "band",
            "is_bogon",
            "country",
            "continent",
            "flags",
            "asn",
            "as_name",
            "as_domain",
            "network",
            "prefix",
            "reasons"
        ],
        "bulk_max": 500,
        "rate_limit_per_second": 100,
        "attribution_required": false,
        "current_period_end": "2026-10-14T00:00:00+00:00"
    },
    "usage": {
        "period": "month",
        "used": 84210,
        "allowance": 1000000,
        "remaining": 915790,
        "resets_at": "2026-10-01T00:00:00+00:00"
    },
    "credits": 0,
    "data": {
        "age_seconds": 3421,
        "ranges": {
            "v4": 4182993,
            "v6": 221044
        }
    }
}
FieldTypeWhat it is
plan object
usage object
credits integer Prepaid lookups left. Spent before the subscription allowance, because credits expire and an allowance does not carry over.
data object or null How old the compiled data is, and how much of it there is.
POST /v1/fingerprint Publishable key

Identify a browser

Called by the browser library. You will not normally call this yourself.

Takes the signals collected on the page and returns the visitor id they belong to, creating one if this browser has not been seen before. Signals the browser could not measure reliably are listed in unstable and excluded from the comparison on both sides, which is what keeps a browser that randomises its canvas from becoming a new visitor every session.

Do not make decisions on this response. It was produced from data the browser sent and delivered to the browser, and anyone can edit both. Use it to obtain the visitor id, send that to your own server, and verify it with GET /v1/fingerprint/{visitorId}.

Body

FieldTypeWhat it is
key string Your publishable key, when it is not in the Authorization header.
components required object Signal name to a 32-character hash of its value. The raw values never leave the browser: nothing here can reconstruct which fonts somebody has installed. Unknown names are discarded.
unstable string[] Signals the browser would not reproduce twice in a row, or blanked outright. Excluded from the comparison on both sides. Sending a randomised value instead of listing it here is worse than sending nothing: it pushes a returning visitor below the threshold and splits one person into a stranger every session.
visitorId string The id already in the browser's storage, if any. Believed unless the fingerprint contradicts it outright.
incognito boolean Best effort. Every browser has deliberately broken the reliable detections.
bot string Automation the page could see. Empty means nothing obvious, not that nothing is there.
privacy string An anti-fingerprinting browser, where one was recognised.
tag string Your own reference. Stored and echoed back, never interpreted.

Example request

{
    "key": "pk_live_7QZ3M8XKD2VN0BHR5TJWY4FC61PSAE9G",
    "components": {
        "canvas": "e3b0c44298fc1c149afbf4c8996fb924",
        "webgl": "9f86d081884c7d659a2feaa0c55ad015",
        "fonts": "2c26b46b68ffc68ff99b453c1d304134",
        "audio": "fcde2b2edba56bf408601fb721fe9b5c",
        "screen": "486ea46224d1bb4fb680f34f7c9ad96a",
        "timezone": "6b51d431df5d7f141cbececcf79edf3d",
        "platform": "d4735e3a265e16eee03f59718b9b5d03"
    },
    "unstable": [],
    "visitorId": "5f2a9c1e7b4d3086af51cc92e0b7d143",
    "incognito": false,
    "bot": "",
    "privacy": "",
    "tag": "checkout"
}

Responses

StatusCodeMeaning
200 not an error The browser was identified.
401 invalid_key No active publishable key matches the one presented.
403 origin_not_allowed The key is not registered for this origin.
413 body_too_large The body is larger than the endpoint accepts.
422 insufficient_signals The browser revealed too little to identify anything.
429 rate_limited Over the rate limit. See Retry-After.
503 unavailable Temporarily unavailable. Retry.

Example response

{
    "visitorId": "5f2a9c1e7b4d3086af51cc92e0b7d143",
    "confidence": 94,
    "matchedBy": "storage",
    "similarity": 98,
    "returning": true,
    "firstSeen": "2026-06-02T09:14:07+00:00",
    "lastSeen": "2026-09-14T11:02:55+00:00",
    "sightings": 37,
    "incognito": false,
    "bot": "",
    "privacy": "",
    "ip": "203.0.113.42",
    "ipVersion": 4,
    "signals": {
        "collected": 19,
        "weight": 541,
        "maximum": 586,
        "unstable": []
    }
}
FieldTypeWhat it is
visitorId string Stable for this browser, and unique to your account. The same browser at another AbuseScore customer has a different id and there is no way to connect the two.
confidence integer How much to believe this. Never 100: there is no measurement here that justifies certainty. Under about 50 means treat it as a hint.
matchedBy string How the browser was recognised. storage is the strongest, fuzzy means the fingerprint had drifted and was matched on weight. new · storage · strict · fuzzy
similarity integer
returning boolean
firstSeen string
lastSeen string
sightings integer
incognito boolean
bot string
privacy string
ip string
ipVersion integer or null 4 · 6 · null
signals object
GET /v1/fingerprint/{visitorId} Secret key

Read a verdict back, server side

This is the call that means something. Your page hands your server a visitor id; your server asks here with a key the page never had. A browser can claim any id it likes and it cannot make this request.

Returns what we know about the visitor, including the history of sightings, so you can see whether an id that claims to be three months old actually is.

Path

NameTypeNotes
visitorId string ^[a-f0-9]{32}$

Responses

StatusCodeMeaning
200 not an error The visitor.
401 invalid_key This endpoint needs a secret key.
404 unknown_visitor No visitor with that id. Also the answer when the id belongs to another account.

Example response

{
    "visitorId": "5f2a9c1e7b4d3086af51cc92e0b7d143",
    "confidence": 94,
    "firstSeen": "2026-06-02T09:14:07+00:00",
    "lastSeen": "2026-09-14T11:02:55+00:00",
    "sightings": 37,
    "incognito": false,
    "bot": "",
    "privacy": "",
    "history": [
        {
            "at": "2026-09-14T11:02:55+00:00",
            "matchedBy": "storage",
            "similarity": 98,
            "confidence": 94,
            "ip": "203.0.113.42",
            "origin": "https://shop.example.com",
            "incognito": false,
            "bot": "",
            "privacy": "",
            "tag": "checkout"
        },
        {
            "at": "2026-09-01T18:40:11+00:00",
            "matchedBy": "fuzzy",
            "similarity": 86,
            "confidence": 79,
            "ip": "198.51.100.7",
            "origin": "https://shop.example.com",
            "incognito": false,
            "bot": "",
            "privacy": "",
            "tag": "login"
        }
    ]
}
FieldTypeWhat it is
visitorId string
confidence integer
firstSeen string
lastSeen string
sightings integer
incognito boolean
bot string
privacy string
history Sighting[] Most recent first. An id claiming to be months old with one sighting is worth a second look.
DELETE /v1/fingerprint/{visitorId} Secret key

Erase a visitor

Deletes the visitor and every sighting of it. Immediate and irreversible.

Here because erasure requests reach you, not us: for the browsers you identify you are the controller and we are your processor. This is how you action one.

Path

NameTypeNotes
visitorId string ^[a-f0-9]{32}$

Responses

StatusCodeMeaning
200 not an error Deleted.
401 invalid_key This endpoint needs a secret key.
404 unknown_visitor No visitor with that id.

Example response

{
    "deleted": true,
    "visitorId": "5f2a9c1e7b4d3086af51cc92e0b7d143"
}
FieldTypeWhat it is
deleted boolean
visitorId string

Before you switch it on

Recognising a browser is regulated in Europe and in the UK, whether or not it uses a cookie, and the obligation lands on you rather than on us. For the visitors you identify you are the controller and we act on your instructions.

  • Get consent where your own advice says you need it, before the script runs.
  • Say what you are doing in your privacy notice. You may link ours.
  • Erasure requests come to you. DELETE /v1/fingerprint/{visitorId} actions one.

What we do on our side: a visitor id belongs to your account alone. The same browser visiting another AbuseScore customer is a different id, and there is no query that connects them, including for us. That is the difference between fraud prevention and an advertising network, and it is enforced in the database rather than promised in a paragraph.

Visitor records are deleted 180 days after the last sighting. The full picture is in the privacy policy, and a data processing agreement is yours for the asking at [email protected].

Something not working, or not covered here? Write to [email protected] and quote the requestId from the response. A person reads it.