Quickstart

A working call in about two minutes, then the three things worth getting right before you ship.

1. Get your key

Create an account, open the API key page and generate your key. It is shown once, at that moment, and never again. It is stored hashed, so nobody can recover it for you. If you lose it, regenerate.

2. Make a call

Send the key in the Authorization header and the address in the path.

curl -H "Authorization: Bearer nv_live_..." \
     https://novpn.fr/v1/check/203.0.113.7

3. Decide what to do with the answer

Most integrations only need the risk field, which buckets the score so you do not have to pick a threshold.

# Block anything the API is confident about
if [ "$(jq -r .risk <<< "$response")" = "high" ]; then
  echo "blocked"
fi

If you would rather set your own line, use the raw score. The buckets are documented alongside it in the endpoint reference, so you can start with a bucket and move to a number later without changing anything else.

Handle the unknown address

This is the case most integrations get wrong. An address the detection database has never seen returns 404, because "we have no opinion" is genuinely different from "this address is clean". A scan is queued automatically, so the same address usually resolves within minutes.

Treat it as "no opinion", not as "clean": retry shortly, or let the request through if a blocked login costs you more than a missed one. There is deliberately no way to turn the 404 into a success. When the address is absent there is no country, operator or ASN to return either, so such a response would carry nothing you could act on.

Before you ship

A complete integration

The four rules above in one function, in four languages. Standard library only, apart from Python's requests. Each one gives up after two seconds, treats an unknown address and any failure alike as a reason to let the request through, and asks for a challenge only when risk is high. Copy it and change the threshold, not the structure: the timeout, the unknown address and the failure path are the parts that are easy to leave out and expensive to leave out.

JavaScript

async function screen(ip) {
  try {
    const response = await fetch(`https://novpn.fr/v1/check/${ip}`, {
      headers: { Authorization: `Bearer ${process.env.NOVPN_API_KEY}` },
      signal: AbortSignal.timeout(2000),
    });

    if (response.status === 404) return "allow";
    if (!response.ok) return "allow";

    const { risk } = await response.json();

    return risk === "high" ? "challenge" : "allow";
  } catch {
    return "allow";
  }
}

Python

import os
import requests

def screen(ip: str) -> str:
    try:
        response = requests.get(
            f"https://novpn.fr/v1/check/{ip}",
            headers={"Authorization": f"Bearer {os.environ['NOVPN_API_KEY']}"},
            timeout=2,
        )
    except requests.RequestException:
        return "allow"

    if response.status_code == 404:
        return "allow"

    if not response.ok:
        return "allow"

    return "challenge" if response.json()["risk"] == "high" else "allow"

PHP

function screen(string $ip): string
{
    $curl = curl_init('https://novpn.fr/v1/check/' . rawurlencode($ip));

    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 2,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NOVPN_API_KEY')],
    ]);

    $body = curl_exec($curl);
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    curl_close($curl);

    if ($body === false || $status !== 200) {
        return 'allow';
    }

    return json_decode($body, true)['risk'] === 'high' ? 'challenge' : 'allow';
}

Go

func Screen(ctx context.Context, ip string) string {
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    request, err := http.NewRequestWithContext(
        ctx, http.MethodGet, "https://novpn.fr/v1/check/"+url.PathEscape(ip), nil)
    if err != nil {
        return "allow"
    }

    request.Header.Set("Authorization", "Bearer "+os.Getenv("NOVPN_API_KEY"))

    response, err := http.DefaultClient.Do(request)
    if err != nil {
        return "allow"
    }
    defer response.Body.Close()

    if response.StatusCode != http.StatusOK {
        return "allow"
    }

    var check struct {
        Risk string `json:"risk"`
    }
    if err := json.NewDecoder(response.Body).Decode(&check); err != nil {
        return "allow"
    }

    if check.Risk == "high" {
        return "challenge"
    }

    return "allow"
}