Find the symptom, then work down its causes, most likely first. If you are still stuck, ask for help.
Identification fails
401 Unauthorized in the browser
Fingerly answers every authentication failure with the same 401, so the response does not say which of these it was.
| Cause | How to check | Fix |
|---|---|---|
| The page's origin is not in the key's allowed origins | Run location.origin in the browser console and compare it, character for character, with the key's origins in Integration > SDK keys. | Add the exact origin. https://example.com does not cover https://www.example.com, and ports count. |
| The key has no allowed origins | The key's row lists none. | Add at least one. A public key with none refuses every browser request. |
| The key was revoked or has expired | The key's status in SDK keys. | Deploy an active key. |
| A secret key is in front-end code | The key starts with fly_sk_. | Use a public key, fly_pk_, in browsers and apps. Revoke the exposed secret key. |
| A proxy forwards a mismatched key | The proxy key and the public key differ in organization, environment or region. | Issue a proxy key for the same environment and region. |
CORS errors in the console
Fingerly answers the browser's preflight for every origin, so a CORS error is rarely a Fingerly setting.
- The request never reached Fingerly. An extension, a corporate proxy or a captive portal answered instead. Check the Network panel for the request's real response.
- Your own proxy does not answer preflights. It must answer
OPTIONSand allow theContent-Type,x-api-keyandIdempotency-Keyheaders. See proxy integrations. - Browser code calls the server API.
GET /eventsnever answers browsers. Read events from your backend with a secret key.
Response headers such as RateLimit-Remaining and Fingerly-Balance-Micros are readable from browser JavaScript, so they need no change.
Requests blocked by content blockers
The request is missing from the Network panel, or fails with net::ERR_BLOCKED_BY_CLIENT, and identify() rejects with a TransportError that has no status.
- Serve the SDK's requests from a path on your own site with a proxy integration. Choose a neutral path.
- Keep your server working without a request ID. A visitor whose blocker stops identification should get more friction at most, never a broken page.
402 Payment Required
no_credit: the balance is used up; add funds or turn on auto top-up. billing_blocked: the organization is not accepting traffic; resolve it in Settings > Billing. Development and staging keys keep working either way. See billing.
429 Too Many Requests
Your organization is over its rate limit, across all keys and environments. The SDKs already retry. Identify at meaningful moments rather than on every page view, and look for a load test or an attack sending identifications.
Other errors from the SDK
| Error | Cause | Fix |
|---|---|---|
ConsentError, code consent_required | The SDK was loaded with consent pending or denied, and consent was never granted. | Call setConsent('granted') when your consent tool records consent. See consent tools. |
FingerlyServerError | identify() ran during server rendering. | Call it from an effect or an event handler, or use immediate, which waits for the browser. |
load() throws | A configuration mistake: an empty apiKey, both endpoint and endpoints, or an endpoint that is neither a URL nor a path. | Fix the option. load() throws synchronously, so call it inside try. |
TransportError with status 404 or 405 | An endpoints path with no proxy behind it. | Deploy the proxy, or remove endpoints. |
| React Native: native module missing | The app was not rebuilt after installing, or runs in Expo Go. | Rebuild the app, in a development build. |
| Flutter: missing plugin | Hot reload after installing. | Run flutter clean and rebuild. |
Reading events
404 event_not_found
| Cause | Fix |
|---|---|
| The event is not readable yet. It usually is within a few seconds. | Retry a 404 a few times over a few seconds before giving up. |
| The secret key is from another environment than the public key that identified. | A development identification is read with a development secret key, a production one with a production key. |
| The identification is older than 30 days. | Keep your own copy. See data retention. |
The client never submitted. submit: false returns an empty request ID. | Identify with submit left on. |
401 or 422 when reading
401: the key is not a secret key, or the request was sent from a browser with anOriginheader. Read events from your backend.422 invalid_request_id: the value is not a Fingerly request ID. Passrequest_idexactly as the SDK returned it, and check your frontend actually sent it.
The tag does not match
- A tag passed to
identify({ tag })overrides the tag set on a framework hook. - A tag function is read when the request is sent. If the value it reads, such as an order ID, is set after that, the tag is stale.
- Framework SDKs share one identification per page. If one page protects two actions, call
identify({ force: true })orrefresh()for the second.
Visitor IDs
The visitor ID changes between visits
| Cause | How to tell |
|---|---|
| The browser or device gives too little to identify anyone, such as a hardened privacy browser. It gets a new visitor ID every time. | identifiable is false, and fingerprint_suppressed is in the triggers. |
| The device changed too much to be recognised, for example after a reset. | visitor_confidence is 0 and visitor_is_new is true. Smaller changes are recognised with a confidence from 85 to 99. |
| The device was not seen for 180 days. | The previous visit is older than that. |
| The two visits were identified by different organizations, such as one per region. | Visitor IDs are scoped to an organization, so each organization has its own. |
A device is returning the first time it reaches production
Visitor IDs belong to the organization, not to an environment. A device that was identified with a development or staging key is already known when it is identified with a production key.
Different people share a visitor ID
A visitor ID identifies a device, not a person, so everyone using a shared device shares one. Devices set up identically, such as a fleet of managed laptops, can also be hard to tell apart. Combine the visitor ID with your own account data before acting on it alone.
Scores
Every test session scores high
- Automated browsers are detected as automation. See end-to-end tests.
- VPNs, Tor and datacenter networks on development machines raise network signals.
- The iOS Simulator, Android emulators, rooted test devices and debug builds with developer tooling attached raise device signals with heavy weights.
Test on a physical device over an ordinary connection when you need a realistic score.
The score is missing
state is unavailable: the network lookup could not run, so the request was answered but not scored, and not billed. This is not a score of 0. Your server needs a rule for it. See suspect score.
A signal fires on real customers
vpn, incognito_mode and privacy_settings are common among ordinary, privacy-minded people, which is why their default weights are low. If one pushes real customers over your threshold, lower its weight rather than removing the check.
high_activity never fires
Behaviour signals learn your traffic first. Until an environment has at least 1,000 identifications and 7 days of history on a platform, they are reported without adding to the score. See signals.
Slow identification
- Load once, early. Call
load()when your application starts. Loading collects and sends nothing. - Identify before the click. For a form that has to feel instant, identify when the visitor starts filling it in, and send the request ID with the submission. Allow for the time the form takes in your server's age check.
- Collection is capped. The initial collection in browsers stops at 300 ms, and the slower signals are collected after the answer, without delaying it. What remains is the network.
- Retries add up. On a failing connection, the SDK makes up to three attempts of 5 seconds each. Pass an
AbortSignalto stop sooner. - On iOS and Android,
identify(tiers: [.fast])skips the slower collection tier for screens that cannot wait.
Webhooks
Signature verification fails
- The signature is computed over the raw body. Verify the bytes you received, before any JSON parsing.
- Use the whole signing secret as the key,
whsec_included, and the secret of this endpoint. - Your server's clock is more than five minutes out. Synchronise it with NTP.
- During a secret rotation the header holds two signatures separated by a comma. Accept the delivery if either verifies.
No deliveries arrive
- The endpoint listens to the wrong environment. Live receives production traffic only; Test receives development and staging.
- The endpoint is paused, or not subscribed to that event type.
billing.status_changedis for live endpoints only. - The URL redirects. Redirects are not followed; register the final URL.
- The URL is not public HTTPS. Use a tunnel for a local server.
Open the endpoint in Integration > Webhooks for its delivery history, and use Send test event to check the URL and your verification in one step. See webhooks.
Get help
Email support@fingerly.io with:
- The
request_idof an affected identification, or theX-Request-Idheader of a failed response. - The key's prefix, such as
fly_pk_us_production_. Never send a whole secret key. - The SDK and its version, and the browser or device.
- When it happened, with the time zone.