Calenzy API
APIs/For your website

IP API

New

Where a visitor is, and whether your website should let them in.

/v1/ipCounts toward your plan's quota2 endpointsFrom your server only

Send your visitor's address and get back their country, region, city and timezone, what follows from the country (currency, main language, calling code), and whether to let them in. We answer from data kept on our own servers, block lists refreshed daily and locations monthly, so a lookup takes a few milliseconds and depends on no third party.

Call it from your server with your visitor's address, never from the browser: your API key would be public. Cache each answer (an hour is a good default), and treat a timeout or any error as allowed. Your website must keep working when a lookup cannot be made. The PHP code does all three.

A website hosted by Calenzy checks its visitors for free: those lookups are not counted in your quota.

The country is reliable. The city and the coordinates are approximate, often the nearest large city or the network operator's location, and there is no postcode. Use them to preselect a country, a language or a currency, never to locate a person.

How allowed is decided

  1. An address on our allow list, or a Google or Bing crawler, is always allowed.
  2. An address on our block list is refused, with reason blacklist.
  3. An address on a public block list is refused, with reason threat, unless your website vouched for it in the last 30 days with Let a visitor through. It is then allowed for your key, and pass_until says until when.
  4. Every other address is allowed. Private and reserved addresses are allowed, with empty location fields.

A refused visitor can still be a person, on a shared office connection or a VPN. Show a check page with a CAPTCHA rather than a dead end, and when they solve it, tell us with Let a visitor through.

GET/v1/ip/{ip}

Look up an address

Location, country details and access decision for one IPv4 or IPv6 address.

Path
ipRequiredstringThe visitor's IPv4 or IPv6 address, as is or percent encoded
Response
statusstringok
ipstringThe address, in its canonical form
allowedbooleanfalse when the visitor should be refused or checked. See How allowed is decided
reasonstring or nullWhy allowed is false: threat (on a public block list) or blacklist (on ours). null when allowed
pass_untilstring or nullWhen your website vouched for the address after a CAPTCHA: the end of that pass, UTC, ISO 8601
crawlerbooleantrue for Google and Bing crawlers, which are always allowed
threat_listsarrayThe public block lists the address is on: firehol_level1, firehol_level2, firehol_level3, firehol_abusers_30d, tor_exit. IPv4 only, as the lists are
country_codestring or nullISO 3166-1 alpha-2, such as FR
country_name, continent, regionstring or nullIn English
citystring or nullNearest city in English, approximate
latitude, longitudenumber or nullApproximate, 4 decimals
timezonestring or nullIANA timezone such as Europe/Paris. In a country with several, the one nearest the coordinates
currency_code, currency_namestring or nullThe country's currency, such as EUR
languagestring or nullThe country's main language, ISO 639-1, such as fr
calling_codestring or nullSuch as +33
attributionstringThe credit owed to the location data provider (see Data sources)
GETapi.calenzy.app/v1/ip/171.25.193.25
Response · 200 OK
{
  "status": "ok",
  "ip": "171.25.193.25",
  "allowed": false,
  "reason": "threat",
  "crawler": false,
  "threat_lists": ["firehol_level2", "firehol_abusers_30d", "tor_exit"],
  "country_code": "SE",
  "country_name": "Sweden",
  "continent": "Europe",
  "region": "Stockholm",
  "city": "Stockholm",
  "latitude": 59.3327,
  "longitude": 18.0656,
  "timezone": "Europe/Stockholm",
  "currency_code": "SEK",
  "currency_name": "Swedish Krona",
  "language": "sv",
  "calling_code": "+46",
  "attribution": "IP Geolocation by DB-IP (https://db-ip.com)"
}
POST/v1/ip/{ip}/pass

Let a visitor throughNew

Vouch for a refused visitor who solved your CAPTCHA.

Send it from your server once your CAPTCHA says the visitor is a person. For 30 days, lookups of that address with your key answer allowed: true with pass_until set, and sending it again starts a new 30 days. It applies to your key only, it never lets in an address on our block list, and it is not counted in your quota.

Remove the address from your own cache afterwards, or the visitor keeps the cached refusal until it expires. calenzy_ip_pass() in the PHP code does both.

Path
ipRequiredstringThe visitor's IPv4 or IPv6 address, as is or percent encoded
Response
statusstringok
ipstringThe address
pass_untilstringThe end of the pass, UTC, ISO 8601
Request
POSTapi.calenzy.app/v1/ip/171.25.193.25/pass
Response · 200 OK
{
  "status": "ok",
  "ip": "171.25.193.25",
  "pass_until": "2026-11-04T09:30:00Z"
}

Add it to your website

Copy these two functions into your website. They cache each answer for an hour, give up after 3 seconds, wait 30 seconds before trying again after a failure, and let the visitor in whenever we cannot answer.

PHP
<?php
/* Look up a visitor with the Calenzy IP API, cache the answer for an hour,
   and never hold a page or block a visitor when the API cannot answer. */
function calenzy_ip(string $ip, string $api_key): ?array {
    /* One cache per key, so sites with different keys on one server never share
       answers, usage or a pause */
    $dir  = sys_get_temp_dir() . '/calenzy-ip-' . substr(hash('sha256', $api_key), 0, 16);
    $file = $dir . '/' . hash('sha256', $ip) . '.json';
    $down = $dir . '/.down';

    if (is_file($file) && filemtime($file) > time() - 3600) {
        $cached = json_decode((string) file_get_contents($file), true);
        if (is_array($cached)) { return $cached; }
    }
    /* The API failed less than 30 seconds ago: do not wait on it again */
    if (is_file($down) && filemtime($down) > time() - 30) { return null; }

    $ch = curl_init('https://api.calenzy.app/v1/ip/' . rawurlencode($ip));
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 2,
        CURLOPT_TIMEOUT        => 3,
        CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $api_key],
    ]);
    $body = curl_exec($ch);
    $data = curl_getinfo($ch, CURLINFO_HTTP_CODE) === 200 ? json_decode((string) $body, true) : null;

    @mkdir($dir, 0700, true);
    if (!is_array($data) || ($data['status'] ?? '') !== 'ok') { @touch($down); return null; }

    $tmp = $file . '.' . getmypid();
    if (@file_put_contents($tmp, json_encode($data)) !== false) { @rename($tmp, $file); }
    return $data;
}

/* Your CAPTCHA said a refused visitor is a person: tell the API (allowed for your key
   for 30 days) and drop the cached refusal so their next page lets them in. */
function calenzy_ip_pass(string $ip, string $api_key): bool {
    $ch = curl_init('https://api.calenzy.app/v1/ip/' . rawurlencode($ip) . '/pass');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => '',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 2,
        CURLOPT_TIMEOUT        => 3,
        CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $api_key],
    ]);
    $ok = curl_exec($ch) !== false && curl_getinfo($ch, CURLINFO_HTTP_CODE) === 200;

    $dir = sys_get_temp_dir() . '/calenzy-ip-' . substr(hash('sha256', $api_key), 0, 16);
    @unlink($dir . '/' . hash('sha256', $ip) . '.json');
    return $ok;
}

$visitor = calenzy_ip($_SERVER['REMOTE_ADDR'], 'YOUR_API_KEY');

/* No answer (timeout, error, quota) means: let the visitor in */
if ($visitor !== null && $visitor['allowed'] === false) {
    http_response_code(403);
    exit('Access denied');   /* or your own check page, with a CAPTCHA that ends in calenzy_ip_pass() */
}

$language = $visitor['language'] ?? 'en';
$currency = $visitor['currency_code'] ?? 'EUR';
?>

Data sources

Locations: IP Geolocation by DB-IP, under CC BY 4.0. If you show location data from this API to your visitors, credit DB-IP with that link.

Threats: the FireHOL block lists (levels 1 to 3, and abusers over 30 days) and the Tor Project's list of exit nodes.

Errors

Besides these, any call can answer one of the common errors.

CodeHTTPWhen
invalid_ip400The path does not hold a valid IPv4 or IPv6 address
ip_data_unavailable503We are replacing the location data. Retry later, and let the visitor in meanwhile