Every change you might notice is recorded here: in the API, in webhook events, in signals and their default weights, and in each SDK. Entries are newest first. Deprecations are announced here too, under the versioning policy.
Current versions
| Component | Version |
|---|---|
| HTTP API | v1 |
| Webhook envelope | 1 |
| JavaScript, React, Next.js, Vue, Nuxt, Svelte, Angular | 0.1.0 |
| iOS, Android, React Native, Flutter, Lynx | 0.1.0 |
| Node.js, Python, Go, Java, .NET, PHP, Ruby, Rust | 0.1.0 |
| Cloudflare Worker proxy | 0.2.0 |
| Proxy templates: Vercel, Netlify, AWS CloudFront, Google Cloud Run, nginx, Caddy, Traefik, container | 1.0.0 |
Upgrading from a preview build
0.1.0 is the first release of every SDK. Builds of the SDKs and proxies from before 25 September 2026 differ from the current versions in the five ways below, and each of them can stop identifications until you act. If you start with the current versions, none of this applies: the guides already describe how they work.
| Change | Affects | What to do |
|---|---|---|
| Consent defaults to pending | Every client SDK, and anything that posts to the API itself | Grant consent once you have it, and send "consent": "granted" from your own code. |
| Endpoints are posted to exactly as given | The JavaScript SDK and its bindings, with custom endpoints | Set endpoints to the full path your proxy answers. |
| The script-tag build is renamed | Pages that load the script-tag build | Serve dist/browser.global.js instead of dist/fingerly.global.js. |
| Proxies answer one exact path | Server SDK proxies, the Cloudflare Worker and proxies of your own | Replace the prefix with the path the dashboard issued, and forward to /api/v1/ingest. |
| Forwarded visitor headers need a proxy key | Proxies that forward the visitor's address | Make sure every proxy sends its proxy key. |
Consent defaults to pending
Every client SDK now starts with its consent state pending instead of granted, and reads and sends nothing until it is granted: the JavaScript SDK and its script-tag build, the React, Next.js, Vue, Nuxt, Svelte and Angular bindings, iOS, Android, and the React Native, Flutter and Lynx packages. Every identification and every deferred report carries "consent": "granted", and the API answers a submission without it with 422 and the code consent_required, on POST /ingest, POST /identify and POST /events/{request_id}/supplement.
- Find every place your pages and apps load a Fingerly SDK.
- If you already have consent, or rely on another lawful basis, pass
grantedwhen loading, as in the table below. - Otherwise keep the default, and call
setConsentwithgrantedfrom your consent tool once the visitor agrees. See consent tools. - If you post to the API without an SDK, add
"consent": "granted"to every identification and deferred report body. - Expect the consent error from
identifywhile consent is not granted, and treat it as "not identified yet" rather than as a failure.
import { load } from '@fingerly/web-js'
// Consent used to default to granted, so this identified straight away.
const fingerly = await load({ apiKey: 'fly_pk_production_…' })
const result = await fingerly.identify()
| SDK | Grant consent | Error while pending |
|---|---|---|
| JavaScript and the script-tag build | load({ apiKey, consent: 'granted' }), or setConsent('granted') | ConsentError |
| React and Next.js | <FingerlyProvider consent="granted">, or setConsent from useFingerly() | ConsentError |
| Vue | createFingerly({ apiKey, consent: 'granted' }) | ConsentError |
| Nuxt | consent: 'granted' in the fingerly options, or NUXT_PUBLIC_FINGERLY_CONSENT=granted | ConsentError |
| Svelte | setupFingerly({ apiKey, consent: 'granted' }) | ConsentError |
| Angular | provideFingerly({ apiKey, consent: 'granted' }) | ConsentError |
| iOS | Fingerly.load(apiKey:consent: .granted), or setConsent(.granted) | ConsentError |
| Android | Fingerly.load(app, apiKey = …, consent = ConsentState.GRANTED), or setConsent(ConsentState.GRANTED) | ConsentException |
| React Native and Lynx | load({ apiKey, consent: 'granted' }), or setConsent('granted') | ConsentError |
| Flutter | Fingerly.load(apiKey: …, consent: ConsentState.granted), or setConsent(ConsentState.granted) | FingerlyConsentException |
| Your own HTTP client | "consent": "granted" in the body | 422 with consent_required |
In the JavaScript SDK, a 422 consent_required from the API is thrown as the same ConsentError. See privacy and consent.
Endpoints are posted to exactly as given
The JavaScript SDK used to treat each endpoints entry as a base: it appended /api/v1/identify for the identification and /api/v1/events/{request_id}/supplement for the deferred report. It now posts both calls to each entry exactly as given, with nothing appended. An entry that is a bare origin, such as http://localhost:8099, is read as an API and gets /api/v1/ingest. Without endpoints, both calls go to https://api.fingerly.io/api/v1/ingest.
- Replace each
endpointsentry with the exact URL your proxy answers: the path the dashboard issued, such as/k7x2qm4a, or an absolute URL for a proxy on another host. - Leave entries that name an API by its origin alone as they are.
- Rename
endpointtoendpoints.endpointstill works and means the same, but it is deprecated. - Stop importing
IDENTIFY_PATHfrom@fingerly/core. It is no longer exported. - Update your proxy in the same deployment, as described in proxies answer one exact path.
import { load } from '@fingerly/web-js'
// Posted to /api/fingerly/api/v1/identify,
// then /api/fingerly/api/v1/events/{request_id}/supplement
const fingerly = await load({ apiKey: 'fly_pk_production_…', endpoints: '/api/fingerly' })
On the API, POST /ingest is new. It takes both calls and tells them apart by the deferred report's token: an identification is answered 200 with the verdict, a deferred report 202. POST /identify and POST /events/{request_id}/supplement remain for direct callers. The mobile SDKs now post their identifications to /api/v1/ingest too, under the API origin they are configured with; in the iOS and Android SDKs, the IDENTIFY_PATH constant is replaced by INGEST_PATH.
The script-tag build is renamed
The script-tag build in @fingerly/web-js is now dist/browser.global.js, renamed from dist/fingerly.global.js, because filter lists match script file names too. It still defines window.Fingerly. Fingerly does not host the file for you: copy the new file to wherever you serve it, under a neutral name of your own. If you load it from a package CDN, change the file name in that URL as well.
cp node_modules/@fingerly/web-js/dist/fingerly.global.js public/assets/app-core.js
Proxies answer one exact path
Every proxy now answers exactly one path, the random one the dashboard issues when you create a proxy in Integration > Proxy keys, and forwards every request to /api/v1/ingest. There is no default path any more: the server SDKs' /api/fingerly default is gone. A proxy relays back only content-type, x-request-id, RateLimit-Limit, RateLimit-Remaining and Retry-After, and proxied responses no longer carry Fingerly-Balance-Micros.
- Open Integration > Proxy keys and note the proxy's path. A proxy key created before this change keeps working; edit it to record how it runs and its path, or create a new proxy, which issues a path.
- Upgrade the server SDK, the Worker or the template, and replace the prefix option with the path, as in the table below.
- Route that one path to the proxy. With a path set, it answers
404to every other path, including paths under it. - Set the browser SDK's
endpointsto the same path. - If you run a proxy of your own, forward the body unchanged to
https://api.fingerly.io/api/v1/ingestwhatever path it arrived on, and relay only the headers above. See proxy integrations. - If your pages read
Fingerly-Balance-Microsthrough a proxy, read the balance in the dashboard instead.
| SDK | Before | After |
|---|---|---|
| Node.js | createProxy({ prefix }) | createProxy({ path }), optional where the route already sends only that path |
| Python | FingerlyProxy(prefix=…) | FingerlyProxy(path=…); under Starlette, add_route rather than mount |
| Go | ProxyOptions{Prefix: …} | ProxyOptions{Path: …}; leave it empty behind http.StripPrefix |
| Java | .prefix(…) | .path(…), required by the WebFlux filter |
| .NET | options.Prefix | options.Path, required by UseFingerlyProxy; or give the path to MapFingerlyProxy |
| PHP | 'prefix' => … | 'path' => …, required by the PSR-15, Laravel and Symfony adapters. prefix is now refused as an unknown option. |
| Ruby | prefix: | path:, required when the proxy is middleware in front of an app |
| Rust | .prefix(…) | .path(…) with route_service; leave it unset under nest_service, which strips the path |
import { Fingerly } from '@fingerly/node'
export const POST = Fingerly.createProxy({
proxyKey: process.env.FINGERLY_PROXY_KEY!,
prefix: '/api/fingerly',
clientIp: (request) => request.headers.get('x-real-ip') ?? '',
})
The Cloudflare Worker proxy 0.2.0 takes the path as FINGERLY_PATH, which replaces FINGERLY_ROUTE_PREFIX, and its route is that one path. FINGERLY_INTEGRATION_ID is removed, and FINGERLY_ALLOWED_ORIGINS is needed only when the route is on another host than your pages.
routes = [{ pattern = "example.com/neutral-random-path/*", zone_name = "example.com" }]
[vars]
FINGERLY_ROUTE_PREFIX = "/neutral-random-path"
FINGERLY_ALLOWED_ORIGINS = "https://www.example.com,https://example.com"
Forwarded visitor headers need a proxy key
A request that carried x-fingerly-client-ip, x-fingerly-origin or x-fingerly-user-agent without a proxy key used to be answered as a direct call, which recorded the proxy's own address against every visitor. It is now refused with 401 and the code proxy_key_missing. A request authenticated with a secret key may still forward them.
- Check that every proxy sends its proxy key: the Worker's
FINGERLY_PROXY_KEYsecret, the server SDK's proxy key option, the key in a template's configuration, or the origin header on AWS CloudFront. - After deploying, look for
proxy_key_missingin your proxy's responses. See troubleshooting.
Other changes on 25 September 2026
These need no action, but you may notice them.
Changed
- Apps are recognised by the SDK's stored install ID only, never by device hardware, which is used for detections and scoring only. See visitor identification.
- Android keeps its install record in a file excluded from Auto Backup and device-to-device transfer instead of shared preferences, and moves an existing record there once. A reinstall, or a restore onto another phone, now starts a new install ID and so a new visitor ID.
- iOS no longer reads boot time, uptime or disk space, sends no DeviceCheck token, reports only a count of installed fonts and a summary of accessibility settings, and reports the container's age in whole days instead of the app's install date.
- Android reports other apps only as yes or no, never by package name.
- IP coordinates are stored rounded to one decimal place, so every event records an approximate location.
- Archived submissions expire after 90 days. See data retention.
- The JavaScript SDK no longer asks for a user-agent client hint that the specification deprecates.
endpointin the JavaScript SDK is deprecated in favour ofendpoints, which means the same.
Added
POST /ingest, the one URL the JavaScript SDK and every proxy post both calls to.deleteStoredIdentity()deletes the install record on the device, for account deletion and erasure:Fingerly.deleteStoredIdentity()on iOS,Fingerly.deleteStoredIdentity(context)on Android, anddeleteStoredIdentity()in the React Native, Flutter and Lynx packages.- The iOS SDK ships a privacy manifest,
PrivacyInfo.xcprivacy, that declares no tracking and file timestamps (C617.1) as its only required-reason API. See App Store and Google Play privacy. - Each proxy key records how the proxy runs, its path, and the version, origin and 30-day request count it last reported, and its name, kind and path can be changed.
- Ready-made proxy templates, and hosted subdomains for sites that can run no proxy.
Fixed
- Identifications from the iOS and Android SDKs no longer create a new visitor every time.
- Safari with its advanced fingerprinting protection on, as in a Private window, is recognised as one visitor across page loads instead of a new visitor on each.
API
v1
The first version of the API.
- Client API:
POST /ingest, the one URL the browser SDK and first-party proxies post both calls to, plusPOST /identify,POST /events/{request_id}/supplementandPOST /attestation/challenge, with public keys. - Every identification and deferred report carries
"consent": "granted"; anything else is answered422withconsent_required. - Server API:
GET /eventsandGET /events/{request_id}, with secret keys. - Proxy integrations: proxy keys with a kind and a random path each, forwarded visitor headers, and
x-fingerly-proxy-version, with the last version, origin and 30-day request count shown per proxy. - Proxy kinds for the ready-made templates:
nginx,caddy,vercel,netlify,aws_cloudfront,gcp_cloud_run,traefikandcontainer, besidecloudflare_worker,server_sdkandcustom. - Forwarded visitor headers sent without a proxy key are refused with
401andproxy_key_missing, rather than recorded with the proxy's own address. - Hosted subdomains, a fallback for sites that can run no proxy: one
CNAMEto Fingerly, a certificate issued automatically, and the management routes to list, add, check and remove them. Idempotency-Keyon identify, andRateLimit-Limit,RateLimit-Remaining,Retry-AfterandFingerly-Balance-Microsresponse headers, readable from browsers.- Management API: SDK keys, proxy keys, webhook endpoints and risk weights, with management keys acting as
adminordeveloper. - Secret keys are refused from browsers on event reads as on identify, except development keys from the Try it panel on these docs.
- An OpenAPI 3.1 document and a Postman collection.
Webhooks
Envelope version 1
The first version of webhook events.
- Events:
identification.completed,visitor.suspect,identification.refused,billing.status_changedandusage.daily_settled, andwebhook.testfrom Send test event. - HMAC-SHA256 signatures over the timestamp and the body, with secret rotation and an overlap window of up to seven days.
- Retries over about seven hours, redelivery from the delivery history, and pausing.
Signals
Initial signal groups
The signal groups in the signals reference, with their default weights for web, Android and iOS, and a default threshold of 30.
Web SDKs
0.1.0
The first version of @fingerly/web-js and the React, Next.js, Vue, Nuxt, Svelte and Angular bindings.
identify()with tags, a 300 ms initial collection and a deferred tier sent after the answer.- Local verdicts,
submit: falseandcollect(). - First-party
endpoints, posted to exactly as given, withfallbackToDefaultEndpoint. - A script-tag build,
dist/browser.global.js, definingwindow.Fingerly. - Consent states:
consent, defaulting topending,setConsent(),onConsentChange()andConsentError. - Retries with one idempotency key, and
TransportError.
Mobile SDKs
0.1.0
The first version of the iOS and Android SDKs, and of the React Native, Flutter and Lynx packages over them.
identifywith tags, local verdicts,submit: falseandcollect.- Hardware-backed attestation on Android 7.0 and newer.
- Consent states, defaulting to
pending, andsetConsent. - Recognition by the stored install ID only, and
deleteStoredIdentity(). - A privacy manifest in the iOS SDK. See App Store and Google Play privacy.
Server SDKs
0.1.0
The first version of the Node.js, Python, Go, Java, .NET, PHP, Ruby and Rust libraries.
- Read one event, and list events by window, visitor and level.
- Webhook signature verification, accepting any signature during a secret rotation.
- Node.js:
createProxy, a first-party proxy for webRequestandResponseruntimes. - Python: a synchronous and an asyncio client, and
FingerlyProxy, a first-party proxy serving both ASGI and WSGI. - Go: a context-aware client and
NewProxy, a first-party proxy as anhttp.Handler. - Java: a client for the JVM and
FingerlyProxy, a first-party proxy with Jakarta, javax and WebFlux adapters. - .NET: an async client and
FingerlyProxy, a first-party proxy for ASP.NET Core endpoints or middleware. - Ruby: a client for Ruby 3.2 and newer and
Fingerly::Proxy, a first-party proxy as Rack middleware. - PHP: a client with no Composer dependencies and
Fingerly\Proxy, a first-party proxy with Laravel, Symfony and PSR-15 adapters. - Rust: an async client and
FingerlyProxy, a first-party proxy as atower::Service. - Every proxy answers one path, the one the dashboard issued, set with an optional
pathoption, and relays back only an allowlist of response headers.
Cloudflare Worker proxy
0.2.0
- Answers exactly one path,
FINGERLY_PATH, the one the dashboard issued, which replacesFINGERLY_ROUTE_PREFIX.FINGERLY_INTEGRATION_IDis removed. - Forwards every request to
/api/v1/ingest, and relays back onlycontent-type,x-request-idand the rate limit headers. FINGERLY_ALLOWED_ORIGINSis needed only for a route on another host than your pages.- Reports itself upstream as
cloudflare-worker/0.2.0.
0.1.0
The first version of the Worker proxy: forwards identify and deferred-report requests for allowed origins, authenticated with a proxy key.
Proxy templates
1.0.0
The first ready-made proxies for sites that run neither the Cloudflare Worker nor a server SDK. The dashboard prints each one filled in with the proxy's own path and key.
- Vercel Routing Middleware, a Netlify Edge Function, and an AWS CloudFront Function with its cache behavior and a CloudFormation fragment.
- A Google Cloud Run service behind an external Application Load Balancer. Firebase Hosting rewrites are not supported.
- An nginx
locationblock and a Caddyhandleblock. - A container for Docker, Kubernetes, HAProxy, Envoy or any load balancer, with
FINGERLY_CLIENT_IP_SOURCEset toremote,header:<name>orxff:<n>, and Traefik routes to it. - Each answers exactly the issued path and only
POST, forwards to/api/v1/ingestalone, resolves the visitor's address from a source the visitor cannot write, and relays back onlycontent-type,x-request-idand the rate limit headers.