# Identify a visitor

> Submit an SDK signal report and receive the visitor ID, the suspect score and the signals behind it in the same response.

Last updated: 2026-09-17

```http
POST /api/v1/identify
```

Authentication: Public key (`x-api-key: fly_pk_…`)

Submits a signal report collected by a client SDK. The response carries the complete verdict. There is no second call to make.

> **Note:** The client SDKs build and send this request for you. Call it directly only if you are writing your own transport, for example a proxy.

**Headers**

- `x-api-key` (string, required): A public key, or a secret key from a server.
- `Idempotency-Key` (string): Up to 255 characters. Makes retries safe. See [Idempotency](https://docs.fingerly.io/reference/idempotency).
- `Content-Encoding` (string): `gzip` to send a compressed body.

**Body**

- `sdk` (object, required): The SDK that collected the report.
  - `platform` (string): `web`, `ios`, `android`, or a bridge such as `react-native-ios`.
  - `version` (string): The SDK version.
- `signals` (object, required): The signal report the SDK collected. Its contents are produced by the SDKs and are not a public contract.
- `tag` (string): Your own reference for this identification, echoed on the event and in webhooks.

## Response

- `request_id` (string): Identifies this identification. A UUIDv7.
- `deferred_token` (string): A capability for sending this request's [deferred report](https://docs.fingerly.io/reference/deferred-report) within 24 hours. Omitted on duplicates.
- `duplicate` (boolean): `true` when this `Idempotency-Key` was already answered. Only `request_id` is meaningful then.
- `visitor_id` (string): The stable visitor identifier: 20 letters and digits. Always present on a new identification.
- `visitor_is_new` (boolean): Whether your organization is seeing this visitor for the first time.
- `identifiable` (boolean): `false` when the report carried too little to identify anyone. The request is still scored.
- `visitor_confidence` (integer): From 0 to 100. `100`: seen before exactly. `85` to `99`: recognised after the device changed. `0`: a new visitor ID.
- `state` (string): `enriched`, or `unavailable` when the network lookup could not run.
- `reason` (string): A short token explaining an `unavailable` state. Omitted otherwise.
- `suspect_score` (integer): The weighted sum of the signals that fired. Omitted when the request was not scored, which is different from `0`.
- `suspect_level` (string): `low`, `medium` or `high`, from your [threshold](https://docs.fingerly.io/docs/suspect-score#levels).
- `triggers` (array): Each signal that fired, heaviest first. Omitted when none did.
  - `signal` (string): The signal, such as `tor` or `automation`.
  - `group` (string): The signal's group, such as `bot` for `automation`.
  - `weight` (integer): The weight the signal added, from the weights in force.
  - `confidence` (string): `low`, `medium` or `high`.

**Response headers**

- `Fingerly-Balance-Micros` (integer): Your remaining balance in millionths of a US dollar. Reflects production traffic.
- `RateLimit-Limit` (integer): Your organization's rate limit, per second.
- `RateLimit-Remaining` (integer): What remained of the limit when the request was admitted.
- `Retry-After` (integer): On `429`, seconds to wait before retrying.

These headers can be read from browser JavaScript as well as from servers and proxies.

## Billing

A production identification costs $0.003, or $0.0005 when `identifiable` is `false`. Development and staging keys, duplicates, refused requests and requests whose `state` is `unavailable` are free. See [billing](https://docs.fingerly.io/docs/billing).

## Errors

| Status | Code | When |
| --- | --- | --- |
| `401` | `unauthorized` | The key did not authenticate from this origin or platform. |
| `402` | `billing_blocked` | The organization is not accepting traffic. |
| `402` | `no_credit` | The balance is used up. |
| `413` | `payload_too_large` | The body is over 1 MiB. |
| `422` | `idempotency_key_too_long` | The key is over 255 characters. |
| `422` | `invalid_visitor_metadata` | A proxy forwarded malformed visitor details. |
| `429` | `rate_limited` | Over the organization's rate limit. |
| `503` | `service_unavailable` | Retry with the same `Idempotency-Key`. |

## Example request

```bash cURL
curl -X POST "https://us.api.fingerly.io/api/v1/identify" \
  -H "Content-Type: application/json" \
  -H "x-api-key: fly_pk_us_production_…" \
  -H "Idempotency-Key: 7d5e1f2a-3c4b-4d6e-8f90-a1b2c3d4e5f6" \
  -H "Origin: https://shop.example.com" \
  -d '{
    "sdk": { "platform": "web", "version": "0.1.0" },
    "signals": { "schema": 1, "…": "…" },
    "tag": "checkout:8412"
  }'
```

```ts JavaScript
const result = await fingerly.identify({ tag: 'checkout:8412' })
```

```swift Swift
let result = try await fingerly.identify(tag: "checkout:8412")
```

```kotlin Kotlin
val result = fingerly.identify(tag = "checkout:8412")
```

## Example response

```json 200
{
  "request_id": "01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4",
  "deferred_token": "v1.eyJyZXF1ZXN0X2lkIjoi….…",
  "visitor_id": "X9pL2mRc7KvT4bQw8NdF",
  "visitor_is_new": false,
  "identifiable": true,
  "visitor_confidence": 100,
  "state": "enriched",
  "suspect_score": 37,
  "suspect_level": "high",
  "triggers": [
    {
      "signal": "tor",
      "group": "tor",
      "weight": 14,
      "confidence": "high"
    },
    {
      "signal": "automation",
      "group": "bot",
      "weight": 9,
      "confidence": "medium"
    },
    {
      "signal": "tampering",
      "group": "browser_tampering",
      "weight": 8,
      "confidence": "medium"
    },
    {
      "signal": "high_activity",
      "group": "high_activity",
      "weight": 6,
      "confidence": "low"
    }
  ]
}
```

```json 200 duplicate
{
  "request_id": "01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4",
  "duplicate": true
}
```

```json 429
{
  "error": {
    "code": "rate_limited",
    "message": "this organisation is sending faster than its rate limit allows",
    "status": 429
  },
  "request_id": "01a0a84c-0f11-7a3e-9c2d-4b5e6f708192"
}
```
