# Quick start

> Create keys, identify a visitor in the browser, read the result with a secret key on your server, and act on it. The whole integration in five steps.

Last updated: 2026-09-17

This guide takes you from an empty account to a verified identification. It uses the browser SDK and Node.js; every step has the same shape in other languages. New accounts start with $3 of credit, and development keys are free.

### Step 1: Create an account and choose a region

Sign up for the Fingerly dashboard. The owner of a new organization chooses where its visitor data will live, and the choice is permanent. See [regions](https://docs.fingerly.io/docs/regions).

### Step 2: Create a public key

In **Integration > SDK keys**, create a **public** key in the **development** environment and add the origin your app runs on. A public key with no allowed origins refuses every request.

```text Allowed origin
http://localhost:3000
```

### Step 3: Identify a visitor in the browser

```bash npm
npm install @fingerly/web-js
```

```bash pnpm
pnpm add @fingerly/web-js
```

```ts login.ts
import { load } from '@fingerly/web-js'

const fingerly = await load({ apiKey: 'fly_pk_us_development_…' })

form.addEventListener('submit', async (event) => {
  event.preventDefault()
  const { requestId } = await fingerly.identify({ tag: 'login' })

  await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email: form.email.value, password: form.password.value, requestId }),
  })
})
```

> **Tip:** Using React, Vue, Next.js or a mobile app? The same step is in each [SDK's guide](https://docs.fingerly.io/docs/sdks).

### Step 4: Create a secret key and read the result on your server

Create a **secret** key in the same environment and store it in your server's environment. The secret is shown once.

When the login request reaches your backend, read the event its `requestId` names.

```ts Node.js
import { load } from '@fingerly/node'

const fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })
const event = await fingerly.events.get('01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4')
```

```python Python
import os
from fingerly import Fingerly

fingerly = Fingerly(secret_key=os.environ["FINGERLY_SECRET_KEY"])
event = fingerly.events.get("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4")
```

```python Python (async)
import os
from fingerly import AsyncFingerly

fingerly = AsyncFingerly(secret_key=os.environ["FINGERLY_SECRET_KEY"])
event = await fingerly.events.get("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4")
```

```go Go
import fingerly "github.com/fingerly-io/fingerly-go"

client := fingerly.New(os.Getenv("FINGERLY_SECRET_KEY"))
event, err := client.Events.Get(ctx, "01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4")
```

```java Java
import io.fingerly.server.FingerlyClient;

FingerlyClient fingerly = FingerlyClient.builder()
    .secretKey(System.getenv("FINGERLY_SECRET_KEY"))
    .build();

Event event = fingerly.events().get("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4");
```

```csharp .NET
using Fingerly;

var fingerly = new FingerlyClient(Environment.GetEnvironmentVariable("FINGERLY_SECRET_KEY"));
var ev = await fingerly.Events.GetAsync("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4");
```

```php PHP
<?php

$fingerly = new \Fingerly\Client(getenv('FINGERLY_SECRET_KEY'));
$event = $fingerly->events->get('01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4');
```

```ruby Ruby
require "fingerly"

fingerly = Fingerly::Client.new(secret_key: ENV.fetch("FINGERLY_SECRET_KEY"))
event = fingerly.events.get("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4")
```

```rust Rust
let fingerly = fingerly::Client::new(std::env::var("FINGERLY_SECRET_KEY")?);
let event = fingerly.events().get("01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4").await?;
```

```bash cURL
curl "https://us.api.fingerly.io/api/v1/events/01a0a84b-e6a2-7c09-9f51-0b3d7a26c8e4" \
  -H "x-api-key: $FINGERLY_SECRET_KEY"
```

### Step 5: Decide

Check the event belongs to this action and is recent, then use the level to choose what happens. Start by recording what you would have done before enforcing it.

```ts Node.js
import { load, FingerlyAPIError } from '@fingerly/node'

const fingerly = load({ secretKey: process.env.FINGERLY_SECRET_KEY! })
const MAX_AGE_MS = 2 * 60 * 1000

export async function decide(orderId: string, requestId: string) {
  let event
  try {
    event = await fingerly.events.get(requestId)
  } catch (error) {
    if (error instanceof FingerlyAPIError && error.status === 404) return 'refuse'
    throw error
  }

  if (event.tag !== 'checkout:' + orderId) return 'refuse'
  if (Date.now() - Date.parse(event.occurred_at) > MAX_AGE_MS) return 'refuse'

  if (event.suspect_level === 'high') return 'review'
  if (event.suspect_level === 'medium') return 'challenge'
  return 'allow'
}
```

```python Python
from datetime import datetime, timedelta, timezone
from fingerly import Fingerly, FingerlyAPIError

fingerly = Fingerly(secret_key=os.environ["FINGERLY_SECRET_KEY"])

def decide(order_id: str, request_id: str) -> str:
    try:
        event = fingerly.events.get(request_id)
    except FingerlyAPIError as error:
        if error.status == 404:
            return "refuse"
        raise

    if event.tag != f"checkout:{order_id}":
        return "refuse"
    if datetime.now(timezone.utc) - event.occurred_at > timedelta(minutes=2):
        return "refuse"

    if event.suspect_level == "high":
        return "review"
    if event.suspect_level == "medium":
        return "challenge"
    return "allow"
```

```python Python (async)
from datetime import datetime, timedelta, timezone
from fingerly import AsyncFingerly, FingerlyAPIError

fingerly = AsyncFingerly(secret_key=os.environ["FINGERLY_SECRET_KEY"])

async def decide(order_id: str, request_id: str) -> str:
    try:
        event = await fingerly.events.get(request_id)
    except FingerlyAPIError as error:
        if error.status == 404:
            return "refuse"
        raise

    if event.tag != f"checkout:{order_id}":
        return "refuse"
    if datetime.now(timezone.utc) - event.occurred_at > timedelta(minutes=2):
        return "refuse"

    return {"high": "review", "medium": "challenge"}.get(event.suspect_level, "allow")
```

```go Go
func decide(ctx context.Context, orderID, requestID string) (string, error) {
    event, err := client.Events.Get(ctx, requestID)
    var apiErr *fingerly.APIError
    if errors.As(err, &apiErr) && apiErr.Status == http.StatusNotFound {
        return "refuse", nil
    } else if err != nil {
        return "", err
    }

    if event.Tag != "checkout:"+orderID || time.Since(event.OccurredAt) > 2*time.Minute {
        return "refuse", nil
    }

    switch event.SuspectLevel {
    case "high":
        return "review", nil
    case "medium":
        return "challenge", nil
    }
    return "allow", nil
}
```

```java Java
public String decide(String orderId, String requestId) {
    Event event;
    try {
        event = fingerly.events().get(requestId);
    } catch (FingerlyApiException e) {
        if (e.getStatus() == 404) return "refuse";
        throw e;
    }

    if (!("checkout:" + orderId).equals(event.getTag())) return "refuse";
    if (event.getOccurredAt().isBefore(Instant.now().minus(Duration.ofMinutes(2)))) return "refuse";

    return switch (String.valueOf(event.getSuspectLevel())) {
        case "high" -> "review";
        case "medium" -> "challenge";
        default -> "allow";
    };
}
```

```csharp .NET
public async Task<string> DecideAsync(string orderId, string requestId, CancellationToken ct)
{
    Event ev;
    try
    {
        ev = await _fingerly.Events.GetAsync(requestId, ct);
    }
    catch (FingerlyApiException e) when (e.Status == 404)
    {
        return "refuse";
    }

    if (ev.Tag != $"checkout:{orderId}") return "refuse";
    if (DateTimeOffset.UtcNow - ev.OccurredAt > TimeSpan.FromMinutes(2)) return "refuse";

    return ev.SuspectLevel switch
    {
        "high" => "review",
        "medium" => "challenge",
        _ => "allow",
    };
}
```

```php PHP
<?php

use Fingerly\ApiException;

function decide(string $orderId, string $requestId): string
{
    global $fingerly;

    try {
        $event = $fingerly->events->get($requestId);
    } catch (ApiException $e) {
        if ($e->getStatus() === 404) {
            return 'refuse';
        }
        throw $e;
    }

    if ($event->tag !== "checkout:{$orderId}") {
        return 'refuse';
    }
    if ($event->occurredAt < new DateTimeImmutable('-2 minutes')) {
        return 'refuse';
    }

    return match ($event->suspectLevel) {
        'high' => 'review',
        'medium' => 'challenge',
        default => 'allow',
    };
}
```

```ruby Ruby
def decide(order_id, request_id)
  event = fingerly.events.get(request_id)

  return "refuse" unless event.tag == "checkout:#{order_id}"
  return "refuse" if event.occurred_at < Time.now - 120

  case event.suspect_level
  when "high" then "review"
  when "medium" then "challenge"
  else "allow"
  end
rescue Fingerly::APIError => e
  raise unless e.status == 404
  "refuse"
end
```

```rust Rust
async fn decide(fingerly: &fingerly::Client, order_id: &str, request_id: &str) -> Result<Decision, fingerly::Error> {
    let event = match fingerly.events().get(request_id).await {
        Ok(event) => event,
        Err(fingerly::Error::Api { status: 404, .. }) => return Ok(Decision::Refuse),
        Err(error) => return Err(error),
    };

    if event.tag.as_deref() != Some(&format!("checkout:{order_id}")) {
        return Ok(Decision::Refuse);
    }
    if chrono::Utc::now() - event.occurred_at > chrono::Duration::minutes(2) {
        return Ok(Decision::Refuse);
    }

    Ok(match event.suspect_level {
        Some(Level::High) => Decision::Review,
        Some(Level::Medium) => Decision::Challenge,
        _ => Decision::Allow,
    })
}
```

## Try it

Open your page, log in, then open **Identification > Events** in the dashboard. Your identification is there with its visitor, score and signals. Now try a private window, a VPN or an automated browser, and watch the score change.

## Go live

- Create **production** public and secret keys with your real origins, and deploy them.
- Production identifications cost $0.003 each. Add funds or turn on auto top-up in **Settings > Billing**.
- Review your [risk weights](https://docs.fingerly.io/docs/risk-weights) and threshold against staging traffic before you enforce decisions.
- Add [webhooks](https://docs.fingerly.io/docs/webhooks) if you want results pushed to you.

- [Server-side verification](https://docs.fingerly.io/docs/server-side-verification): The checks to make before you trust a result.
- [Plan your integration](https://docs.fingerly.io/docs/planning-your-integration): Where to identify, what to tag, what to decide.
