API reference
Base URL https://uat.turing-api.com. JSON in, JSON out,
bearer auth. There is nothing else to learn.
/v1/bot-check · /v1/email-check · /v1/usage · Errors · Limits
Authentication
Send your key as a bearer token. Keys are stored as SHA-256 hashes, so we cannot show you an existing key, only mint a new one.
Authorization: Bearer YOUR_API_KEY
Browser CORS is scoped to your own origins on purpose. A key that reaches the browser is a public key, so call us from your server.
POST /v1/bot-check
Scores an IP and User-Agent pair. A risk score above 50 sets is_bot. The IP half is deterministic: a merged range table over published datacenter CIDRs, with no heuristics.
Costs one credit. Refunded on any non-2xx response.
curl -X POST https://uat.turing-api.com/v1/bot-check \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ip":"52.1.2.3","user_agent":"curl/8.4.0"}'
import requests
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
response = requests.post(
"https://uat.turing-api.com/v1/bot-check",
headers=HEADERS,
json={"ip": "52.1.2.3", "user_agent": "curl/8.4.0"},
timeout=5,
)
response.raise_for_status()
print(response.json())
const response = await fetch("https://uat.turing-api.com/v1/bot-check", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({"ip":"52.1.2.3","user_agent":"curl/8.4.0"}),
signal: AbortSignal.timeout(5000),
});
if (!response.ok) throw new Error("turing: " + response.status);
console.log(await response.json());
$curl = curl_init("https://uat.turing-api.com/v1/bot-check");
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => '{"ip":"52.1.2.3","user_agent":"curl/8.4.0"}',
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_KEY",
"Content-Type: application/json",
],
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$verdict = $status === 200 ? json_decode($body, true) : null;
var_dump($verdict);
require "net/http"
require "json"
uri = URI("https://uat.turing-api.com/v1/bot-check")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer YOUR_API_KEY"
request["Content-Type"] = "application/json"
request.body = { ip: "52.1.2.3", user_agent: "curl/8.4.0" }.to_json
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: uri.scheme == "https",
open_timeout: 5, read_timeout: 5) do |http|
http.request(request)
end
puts JSON.parse(response.body)
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
ip | string | yes | IPv4 address of the client. |
user_agent | string | no | Raw User-Agent header. Absent or empty is itself a signal. |
headers | object | no | Request headers, up to 64 entries. Missing browser headers add risk. |
Response
{
"is_bot": true,
"risk_score": 100,
"reasons": [
"datacenter_ip",
"missing_sec_ch_ua",
"missing_accept_language",
"missing_sec_fetch_site",
"known_bot_user_agent"
]
}Try it
POST /v1/email-check
Scores an address on syntax, role-account shape, disposable-domain membership, free-provider status and a live MX lookup. A risk score above 40 sets is_risky. A DNS failure scores zero rather than inventing a verdict.
Costs one credit. Refunded on any non-2xx response.
curl -X POST https://uat.turing-api.com/v1/email-check \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"admin@mailinator.com"}'
import requests
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
response = requests.post(
"https://uat.turing-api.com/v1/email-check",
headers=HEADERS,
json={"email": "admin@mailinator.com"},
timeout=5,
)
response.raise_for_status()
print(response.json())
const response = await fetch("https://uat.turing-api.com/v1/email-check", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({"email":"admin@mailinator.com"}),
signal: AbortSignal.timeout(5000),
});
if (!response.ok) throw new Error("turing: " + response.status);
console.log(await response.json());
$curl = curl_init("https://uat.turing-api.com/v1/email-check");
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => '{"email":"admin@mailinator.com"}',
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_KEY",
"Content-Type: application/json",
],
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$verdict = $status === 200 ? json_decode($body, true) : null;
var_dump($verdict);
require "net/http"
require "json"
uri = URI("https://uat.turing-api.com/v1/email-check")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer YOUR_API_KEY"
request["Content-Type"] = "application/json"
request.body = { email: "admin@mailinator.com" }.to_json
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: uri.scheme == "https",
open_timeout: 5, read_timeout: 5) do |http|
http.request(request)
end
puts JSON.parse(response.body)
Parameters
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | yes | Address to score. Maximum 254 characters. |
Response
{
"email": "admin@mailinator.com",
"is_risky": true,
"is_role": true,
"is_disposable": true,
"is_free_provider": false,
"has_mx": true,
"mx_status": "has_mx",
"risk_score": 90,
"reasons": ["role_account", "disposable_domain"]
}Try it
GET /v1/usage
Current balance for the key. Authenticated but never metered: checking your balance is free.
Free. Never metered.
curl -X GET https://uat.turing-api.com/v1/usage \ -H "Authorization: Bearer YOUR_API_KEY"
import requests
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
response = requests.get(
"https://uat.turing-api.com/v1/usage",
headers=HEADERS,
timeout=5,
)
response.raise_for_status()
print(response.json())
const response = await fetch("https://uat.turing-api.com/v1/usage", {
method: "GET",
headers: {
Authorization: "Bearer YOUR_API_KEY",
},
signal: AbortSignal.timeout(5000),
});
if (!response.ok) throw new Error("turing: " + response.status);
console.log(await response.json());
$curl = curl_init("https://uat.turing-api.com/v1/usage");
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_KEY",
],
]);
$body = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
$verdict = $status === 200 ? json_decode($body, true) : null;
var_dump($verdict);
require "net/http"
require "json"
uri = URI("https://uat.turing-api.com/v1/usage")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer YOUR_API_KEY"
response = Net::HTTP.start(uri.hostname, uri.port,
use_ssl: uri.scheme == "https",
open_timeout: 5, read_timeout: 5) do |http|
http.request(request)
end
puts JSON.parse(response.body)
Parameters
No parameters.
Response
{
"period": "2026-08",
"free_used": 412,
"free_limit": 1000,
"free_remaining": 588,
"paid_remaining": 0,
"total_remaining": 588
}Try it
Errors
| Status | Meaning |
|---|---|
400 | Validation failed. details[] names the field. Not billed. |
401 | Missing, malformed or revoked key. |
402 | Credits exhausted. Top up on the dashboard. |
413 | Body over the size cap. |
429 | Rate limited. Retry after the header says. |
500 | Our fault. Not billed. Quote request_id. |
503 | Storage unreachable. Retry: this is not an auth failure. |
{
"error": "Invalid request body",
"request_id": "01J...",
"details": [{ "field": "ip", "message": "Must be a valid IPv4 address" }]
}Limits and stated trade-offs
- Credit metering settles asynchronously per colo, so a burst can overshoot your balance by a few calls for up to ten seconds.
- Key revocation propagates through the auth cache in up to sixty seconds, and the revoke response tells you so.
- Metering, rate limiting and DNS all fail open. If our ledger is unreachable we serve you rather than bill you.
- IPv4 only. IPv6 is rejected with a 400 rather than scored as unknown.
- Every response carries
X-Request-Id. Log it.