IP API
NewWhere a visitor is, and whether your website should let them in.
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.
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
- An address on our allow list, or a Google or Bing crawler, is always allowed.
- An address on our block list is refused, with
reasonblacklist. - An address on a public block list is refused, with
reasonthreat, unless your website vouched for it in the last 30 days with Let a visitor through. It is then allowed for your key, andpass_untilsays until when. - 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.
Look up an address
Location, country details and access decision for one IPv4 or IPv6 address.
| ipRequiredstring | The visitor's IPv4 or IPv6 address, as is or percent encoded |
| statusstring | ok |
| ipstring | The address, in its canonical form |
| allowedboolean | false when the visitor should be refused or checked. See How allowed is decided |
| reasonstring or null | Why allowed is false: threat (on a public block list) or blacklist (on ours). null when allowed |
| pass_untilstring or null | When your website vouched for the address after a CAPTCHA: the end of that pass, UTC, ISO 8601 |
| crawlerboolean | true for Google and Bing crawlers, which are always allowed |
| threat_listsarray | The 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 null | ISO 3166-1 alpha-2, such as FR |
| country_name, continent, regionstring or null | In English |
| citystring or null | Nearest city in English, approximate |
| latitude, longitudenumber or null | Approximate, 4 decimals |
| timezonestring or null | IANA timezone such as Europe/Paris. In a country with several, the one nearest the coordinates |
| currency_code, currency_namestring or null | The country's currency, such as EUR |
| languagestring or null | The country's main language, ISO 639-1, such as fr |
| calling_codestring or null | Such as +33 |
| attributionstring | The credit owed to the location data provider (see Data sources) |
curl "https://api.calenzy.app/v1/ip/171.25.193.25" \ -H "Authorization: Bearer YOUR_API_KEY"
curl "https://api.calenzy.app/v1/ip/2a01:cb00::1" \ -H "Authorization: Bearer YOUR_API_KEY"
{ "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)" }
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.
| ipRequiredstring | The visitor's IPv4 or IPv6 address, as is or percent encoded |
| statusstring | ok |
| ipstring | The address |
| pass_untilstring | The end of the pass, UTC, ISO 8601 |
curl -X POST "https://api.calenzy.app/v1/ip/171.25.193.25/pass" \ -H "Authorization: Bearer YOUR_API_KEY"
{ "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 /* 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.
| Code | HTTP | When |
|---|---|---|
invalid_ip | 400 | The path does not hold a valid IPv4 or IPv6 address |
ip_data_unavailable | 503 | We are replacing the location data. Retry later, and let the visitor in meanwhile |