# Troubleshooting

> Fix the problems integrations run into: 401 and CORS errors, content blockers, events that cannot be read, visitor IDs that change, high scores and slow identification.

Last updated: 2026-09-17

Find the symptom, then work down its causes, most likely first. If you are still stuck, [ask for help](#get-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. |

> **Tip:** Subscribe a webhook endpoint to [`identification.refused`](https://docs.fingerly.io/reference/webhooks/identification-refused). Its `reason` names the cause: `origin_not_allowed`, `revoked_key` or `expired_key`. A key Fingerly does not recognise at all, or one sent to another region's API, cannot be attributed to you and sends no webhook.

### 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 `OPTIONS` and allow the `Content-Type`, `x-api-key` and `Idempotency-Key` headers. See [proxy integrations](https://docs.fingerly.io/docs/proxy-integrations#build-your-own-proxy).
- **Browser code calls the server API.** `GET /events` never 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](https://docs.fingerly.io/docs/proxy-integrations). 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](https://docs.fingerly.io/docs/billing).

### 429 Too Many Requests

Your organization is over its [rate limit](https://docs.fingerly.io/docs/rate-limits), 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](https://docs.fingerly.io/docs/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](https://docs.fingerly.io/docs/data-retention#keeping-your-own-copy). |
| 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 an `Origin` header. Read events from your backend.
- `422 invalid_request_id`: the value is not a Fingerly request ID. Pass `request_id` exactly 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 })` or `refresh()` 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](https://docs.fingerly.io/docs/testing#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](https://docs.fingerly.io/docs/suspect-score#when-a-request-is-not-scored).

### 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](https://docs.fingerly.io/docs/risk-weights) 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](https://docs.fingerly.io/docs/signals#behaviour-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 `AbortSignal` to 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_changed` is 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](https://docs.fingerly.io/docs/webhooks).

## Get help

Email [support@fingerly.io](mailto:support@fingerly.io) with:

- The `request_id` of an affected identification, or the `X-Request-Id` header 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.
