Docs › User Agent API
The User Agent API is a read-only user-agent parsing API. Every endpoint returns JSON, authenticates via a single bearer token, and answers with one structured record per request. No SDK is required.
Overview
The User Agent API answers the questions a user-agent string raises: what is it, what is
it running on, and is it human? It's a deliberately small surface: a single
GET parse endpoint that returns the
agent's classification (a display-ready label plus the
client's type, name, version, and rendering engine), the organization the traffic acts
for, the operating system, the device class and hardware it identifies, and bot signals
— including whether the agent is an AI crawler or an AI assistant. Give it a string in
the query string, or give it nothing at all and it parses the agent the request itself
came from.
-
Every kind of agent. Browsers are the easy part: the API also
classifies search-engine crawlers, AI crawlers and assistants, link-preview fetchers,
command-line tools, HTTP libraries, email clients, in-app webviews, and media players
— one
client.typeanswer, every time. -
Breadth is the point. 100+ browser families, 70+ operating systems
(down to the Linux distribution), and 50,000+ device models by marketing name, so an
obscure Android handset resolves to
Galaxy S24 Ultrarather thanSM-S928B, and 190+ bot operators are named outright. The strings that defeat a hand-rolled regex are the ones this is for. -
AI traffic, split two ways. Training crawlers
(
is_ai_crawler: GPTBot, ClaudeBot) and live user-triggered assistant fetches (is_ai_assistant: ChatGPT-User, Perplexity-User) are flagged separately, because most sites want to treat them differently — and every known bot names its operator (operator.name: OpenAI, Anthropic, Google), so per-company traffic questions are one comparison. -
Read-only. All endpoints are
GET. There is no POST/PUT/DELETE surface. -
JSON-only responses.
Content-Type: application/jsonon every response. Successful payloads sit under a top-leveldatakey with the parse record indata.objects; failures come back undererrorsas an array of objects carrying amessage. -
One shape, one rule for absence. Every response is the same
structure, and unknown always reads as
null— never a missing key, never an empty string. A leaf the string doesn't reveal isnull; a whole block that resolved nothing collapses to a singlenullrather than a tree of them, sooson a bot isnull, not{ "name": null, … }. Null-check a block before reading into it — see Response objects for which blocks collapse and which never do. -
Bring a user agent, or don't. With no string supplied, the request's
own
User-Agentheader is parsed, so/user-agent/v1alone is a "what browser am I" call. - Private. Parsing is first-party end to end: the strings you submit are never forwarded to a third-party service, and nothing about them is shared.
Try it now
Two ways to try the User Agent API right now. Neither requires an account.
1. Fire a live request from this page. Click the button below. The response slides open in the Explorer panel.
// Demo key: no account required.
const response = await fetch(
'https://api.restcountries.com/user-agent/v1',
{ headers: { 'Authorization': 'Bearer rc_live_demo' } }
);
const data = await response.json();
2. Run it from your terminal.
Click the copy button below, then paste anywhere
curl lives.
curl "https://api.restcountries.com/user-agent/v1?pretty=1" \
-H "Authorization: Bearer rc_live_demo"
The demo key returns a real, correctly-shaped sample record (Chrome on macOS) plus a
data._demo notice block; it never touches your account or
quota.
Quick start
Sign up to get an API key, then paste this into your terminal (swap out the API key placeholder):
curl "https://api.restcountries.com/user-agent/v1?pretty=1" \
-H "Authorization: Bearer YOUR_API_KEY"
You'll get a single-object data.objects array back with the
full parse record — and since no string was supplied, the record describes the agent that
made the request: curl itself, classified as a command-line
tool. Pass any string via ?ua= to parse that instead. From
there, browse the Parse section for the full surface, or the
Response objects reference to see every field.
Base URL & versioning
User Agent API endpoints live under:
https://api.restcountries.com/user-agent/v1
The v1 path segment is the API version. It's the current, stable
version; backwards-compatible additions (new fields, new detections) ship inside it without
a bump. See Versioning.
Append ?pretty to any request to pretty-print the JSON response,
which is handy while exploring from a browser or terminal.
Business-plan customers also get a dedicated EU-region endpoint at a separate
hostname (eu-west-1) for in-region routing.
Parse
Returns the parse record for one user-agent string. The string to parse is resolved
in a fixed precedence order: the ua query parameter
(/user-agent/v1?ua=curl/8.7.1) wins, and when it isn't
supplied, the request's own User-Agent header is parsed
instead. User-agent strings never go in the path — they carry slashes, spaces, and
parentheses, so the query string (or the header) is their home. When neither a
parameter nor a header is present, the request is rejected with a
400.
Any agent string is accepted — there is no "invalid" user agent. A string the
classification data can't name comes back as a correctly-shaped record with
null values, never an error.
Path examples
Long agent strings below are collapsed into short named chips — hover one to see the full string it stands for. The Run button always fires the real, fully-encoded request.
smart_tv)mobile_app)is_ai_crawler)is_ai_assistant)Query parameters
ua
string
User-Agent header.
Values may be sent raw,
percent-encoded, or with + for spaces; a
+ is always read as a space.
response_fields
string
response_fields=label,attributes.is_bot returns an object
with exactly those two keys). Comma-separated dot-paths; ordering doesn't matter. Omit
the parameter to return every field.
response_fields_omit
string
response_fields_omit=ua returns the full object minus
that key). Comma-separated dot-paths; ordering doesn't matter. Composes with
response_fields: when both are present, the response is
first projected to response_fields, then
response_fields_omit prunes from that subset (so the omit
list wins on conflict).
Useful for trimming heavy nested branches like
client.attributes without listing every other field
you want.
pretty
boolean
?pretty) and truthy values
(true, 1)
enable; anything else (including
false and 0)
leaves the response compact.
Example
curl "https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36&pretty=1" \
-H "Authorization: Bearer YOUR_API_KEY"
const response = await fetch(
'https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36',
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
const data = await response.json();
fetch(
'https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36',
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
)
.then(function (response) { return response.json(); })
.then(function (data) { console.log(data); });
import requests
response = requests.get(
'https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36',
headers={'Authorization': 'Bearer YOUR_API_KEY'}
)
data = response.json()
$ch = curl_init('https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer YOUR_API_KEY']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = json_decode(curl_exec($ch), true);
require 'net/http'
require 'json'
uri = URI('https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36')
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer YOUR_API_KEY'
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
data = JSON.parse(response.body)
package main
import (
"encoding/json"
"io"
"net/http"
)
req, _ := http.NewRequest("GET", "https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36", nil)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data map[string]interface{}
json.Unmarshal(body, &data)
use reqwest::blocking::Client;
use serde_json::Value;
let client = Client::new();
let response = client
.get("https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36")
.header("Authorization", "Bearer YOUR_API_KEY")
.send()?;
let data: Value = response.json()?;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36"))
.header("Authorization", "Bearer YOUR_API_KEY")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String body = response.body();
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_API_KEY");
var data = await client.GetFromJsonAsync<JsonElement>(
"https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36");
import Foundation
var request = URLRequest(url: URL(string: "https://api.restcountries.com/user-agent/v1?ua=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F150.0.0.0%20Safari%2F537.36")!)
request.addValue("Bearer YOUR_API_KEY", forHTTPHeaderField: "Authorization")
let (data, _) = try await URLSession.shared.data(for: request)
let json = try JSONSerialization.jsonObject(with: data)
{
"data": {
"objects": [
{
"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
"label": "Chrome on macOS",
"client": {
"label": "Google Chrome (v150)",
"name": "Chrome",
"type": "browser",
"version": { "full": "150.0.0.0", "major": 150 },
"maker": {
"name": "Google",
"links": {
"wikipedia": "https://en.wikipedia.org/wiki/Google",
"official": "https://www.google.com/"
}
},
"engine": {
"label": "Google Blink (v150)",
"name": "Blink",
"version": { "full": "150.0.0.0", "major": 150 },
"maker": {
"name": "Google",
"links": {
"wikipedia": "https://en.wikipedia.org/wiki/Google",
"official": "https://www.google.com/"
}
}
},
"attributes": {
"is_android": false,
"is_chrome": true,
"is_chromium": false,
"is_chromium_based": true,
"is_edge": false,
"is_safari": false,
"is_firefox": false,
"is_headless": false,
"is_internet_explorer": false,
"is_opera": false,
"is_web_view": false
}
},
"operator": null,
"os": {
"label": "Apple macOS (v10)",
"name": "macOS",
"version": { "full": "10.15.7", "major": 10 },
"maker": {
"name": "Apple",
"links": {
"wikipedia": "https://en.wikipedia.org/wiki/Apple_Inc.",
"official": "https://www.apple.com/"
}
}
},
"device": {
"label": "Apple Macintosh",
"name": "Macintosh",
"type": "desktop",
"maker": {
"name": "Apple",
"links": {
"wikipedia": "https://en.wikipedia.org/wiki/Apple_Inc.",
"official": "https://www.apple.com/"
}
},
"attributes": {
"is_camera": false,
"is_desktop": true,
"is_media_player": false,
"is_mobile": false,
"is_phone": false,
"is_set_top_box": false,
"is_smart_tv": false,
"is_tablet": false,
"is_watch": false,
"is_wearable": false
}
},
"flags": {
"is_ai_assistant": false,
"is_ai_crawler": false,
"is_bot": false,
"is_crawler": false
}
}
]
}
}
data.objects always holds exactly one record — the resolved
string. Note operator:
null above. A browser's traffic acts for the human using it,
not for an organization, so that whole block collapses. See
Response objects for every field.
Example: a bot
The same request shape with an AI crawler's string
(?ua= GPTBot's agent) comes back typed and flagged —
abbreviated here to the fields that differ:
{
"data": {
"objects": [
{
"label": "GPTBot",
"client": {
"label": "OpenAI GPTBot (v1)",
"name": "GPTBot",
"type": "crawler",
"version": { "full": "1.0", "major": 1 },
"maker": {
"name": "OpenAI",
"links": {
"wikipedia": "https://en.wikipedia.org/wiki/OpenAI",
"official": "https://openai.com/"
}
}
},
"operator": {
"name": "OpenAI",
"name_legal": "OpenAI OpCo, LLC",
"email_address": null,
"links": {
"info": null,
"wikipedia": "https://en.wikipedia.org/wiki/OpenAI"
}
},
"os": null,
"device": {
"label": null,
"name": null,
"type": null,
"maker": null,
"attributes": { "is_desktop": false, "is_mobile": false, "…": false }
},
"flags": {
"is_ai_assistant": false,
"is_ai_crawler": true,
"is_bot": true,
"is_crawler": true
}
}
]
}
}
Two things to read off that. First, what doesn't happen: the bot is never
guessed to be a desktop just because its string borrows browser tokens — the
hardware leaves stay null, and
os collapses outright. Second, the
maker / operator split, which
coincides here and won't always: client.maker is who
builds the agent, operator is who its traffic
acts for. On an Instagram in-app browser the engine's maker is Google and the
operator is Meta.
Response objects
Every parse answers with a single record in data.objects,
and every record is the same shape — the keys below, in this order, on
every response. Unknown always reads as null, at one of two
granularities: a leaf the string doesn't reveal is
null, and a block whose every leaf came back
null collapses to a single
null instead of a tree of them.
So os, client.engine,
operator, every maker, and every
version may each arrive as a bare
null — null-check before reading into them. Three blocks
never collapse and are always objects: client,
device, and flags.
Two conventions worth knowing up front. Every
version is a two-key block:
full is the version string as the agent reported it, and
major is its leading number as an integer, for easy
comparisons. And every attributes block (on
client and device) plus
flags holds definite booleans: a flag
nothing could speak to answers false, never
null. Absence of evidence is reported as
false in those blocks, so a
false means "not established", not "proven negative".
Top level
ua
string
ua parameter or the
request's own User-Agent header, whichever the
request resolved to.
label
string
Mobile Safari on Apple iPad), otherwise the OS
(Chrome on macOS) — which spares desktops their
generic Macintosh and
Desktop hardware names. With neither resolved it's
the bare client name (GPTBot), and it's
null only when the client name itself didn't parse.
Note this is the agent label and carries no version. Each block has its own
label — client,
client.engine, os, and
device — and those lead with the maker and trail
with the major version
(Google Chrome (v150),
Apple Macintosh).
client
The agent itself. Always an object — never collapsed.
label
string
Google Chrome (v150)). A name that already leads
with its own maker isn't made to stutter, and a handful of names drop the maker
because it reads as trivia rather than recognition
(Ubuntu, not
Canonical Ubuntu).
name
string
Chrome,
Mobile Safari,
Googlebot, curl).
type
string
browser,
crawler (search-engine and AI indexers),
fetcher (link previewers and on-demand fetchers:
Twitterbot, Slackbot, AI assistants),
cli (command-line tools: curl, Wget),
library (HTTP client libraries: python-requests,
axios, okhttp),
email (email clients),
mobile_app (phone and tablet apps, including
in-app webviews: Instagram, Facebook),
desktop_app (apps on a desktop OS, Electron or
native: Slack, Cursor), and
media_player (VLC, iTunes). An ordinary browser
string carries no marker, so browser is the
default whenever a client is named without one.
version
object
{ full, major } (e.g.
150.0.0.0 / 150).
maker
object
{ name, links } — who builds this client
(Chrome →
Google,
Firefox →
Mozilla), with
links being
{ wikipedia, official } for that organization.
Not to be confused with operator, which is who the traffic acts for. The two diverge on in-app browsers: an Instagram webview has an engine maker of Google and an operator of Meta.
engine
object
{ label, name, version, maker } — the same four
keys the client itself carries.
name is e.g. Blink,
WebKit, Gecko,
Trident, reported as the string claims it: a bot
that borrows browser tokens will show the engine those tokens name.
Collapses to
null when nothing about the engine
parsed.
attributes
object
is_safari, Chrome Headless as
is_chrome) and follow brand semantics: Chrome on
iOS is is_chrome even though its engine is WebKit.
is_chrome — Chrome family (Chrome, Chrome Mobile,
Chrome Headless, Chrome WebView). Excludes Chromium, which has its own flag.
is_chromium — the client is Chromium itself.
is_chromium_based — built on Chromium: Chrome,
Chromium, Edge 79+, or any client whose engine reports Blink (Opera, Vivaldi,
Samsung Internet, Brave). The flag to gate on for engine-level behavior.
is_edge,
is_firefox,
is_opera,
is_safari,
is_internet_explorer — the remaining browser
families, each matching their mobile builds too.
is_android — the client is the stock
Android Browser. This names the client, not the OS: Chrome on
Android is is_chrome, not
is_android. For the OS, read
os.name.
is_headless — an automation-oriented build
(Headless Chrome, PhantomJS, SlimerJS, HtmlUnit, Splash).
is_web_view — an in-app browser (Instagram,
Facebook, Android WebView) rather than a standalone one. Useful when webviews
break OAuth flows or downloads.
operator
Who the traffic acts for, as opposed to who built the client. Populated
for bots and in-app browsers (GPTBot →
OpenAI, Googlebot →
Google, Instagram →
Meta), so "all of OpenAI's traffic" is one comparison
whatever the individual agent names.
Collapses to null for ordinary browsers — deliberately, since
a browser's traffic acts for the human using it, not for an organization. That makes a
non-null operator a useful signal in its own right.
name
string
OpenAI) — this is the one to compare on. Falls back
to name_legal verbatim when the producer isn't a
known organization.
name_legal
string
OpenAI OpCo, LLC). Prefer
name for grouping.
email_address
string
[email protected]);
null when it declares none.
links
object
{ info, wikipedia }.
info is the policy or documentation page the bot
declares in its own string (e.g.
http://www.google.com/bot.html) — handy for looking
up a crawler's policy straight from your logs.
wikipedia is the operator's article, resolved from
name.
os
The operating system. Collapses to null when the string
names no OS — the common case for bots, which is why this block needs a null-check
before you read os.name.
label
string
Apple macOS (v10)).
name
string
macOS,
Windows, iOS,
Android). Linux distributions resolve to their own
names (Ubuntu,
Fedora) rather than a generic
Linux.
version
object
{ full, major }. One caveat worth designing around:
modern Chrome-family browsers send "frozen" strings that pin the OS version to
a fixed value for privacy — macOS always reports
10.15.7 and Windows always
10.0, whatever the machine is actually running.
Treat a Chrome-family OS version as approximate, and don't gate features on it.
maker
object
{ name, links } for the organization behind the OS
(macOS →
Apple).
device
The hardware. Always an object — never collapsed — though for an agent that identifies
no hardware every leaf but attributes is
null.
label
string
Apple iPhone,
Samsung Galaxy S24 Ultra). This is what the
record's top-level label borrows when the device
is something other than a desktop.
name
string
Galaxy S24 Ultra rather than
SM-S928B. Falls back to the model code the string
carried when no marketing name is on file.
type
string
phone,
tablet,
desktop,
watch,
smart_tv,
set_top_box,
game_console,
handheld (handheld game consoles),
e_reader,
media_player,
smart_display,
voice (smart speakers),
camera,
car,
appliance,
augmented_reality,
virtual_reality, or
cloud.
Two naming notes: it's
phone, not
mobile (for the broader handheld question read
attributes.is_mobile), and
game_console, not
console. There is no
wearable type — a smartwatch is
watch, and
attributes.is_wearable rides alongside it.
desktop is inferred: desktop strings don't announce
themselves, so it's assigned only when a browser reports a desktop-class OS.
Bots therefore leave type
null rather than being guessed onto a desktop.
maker
object
{ name, links } for the hardware maker (e.g.
Apple, Samsung,
Tesla).
attributes
object
is_phone,
is_tablet,
is_desktop,
is_watch,
is_wearable,
is_smart_tv,
is_set_top_box,
is_media_player,
is_camera, and
is_mobile.
is_mobile is the broad handheld question and the
one most callers want: it covers phones and tablets, and also cameras and
portable media players. The rest are reconciled against the settled
type, so a record never claims two form factors at
once.
flags
Classification flags for the agent. Always an object, and every leaf is a definite
true or false — never
null. A false here means
the signal wasn't established, which is not quite the same as a proven negative:
an unrecognized agent reads as is_bot: false whether
it's a real browser or a bot nobody has catalogued yet. Gate accordingly.
is_bot
boolean
is_crawler
boolean
is_bot: a link previewer like
Twitterbot is a bot but not a crawler, since it
pulls the one URL somebody just shared.
is_ai_crawler
boolean
GPTBot, ClaudeBot) —
the kind that reads pages in bulk.
is_ai_assistant
boolean
ChatGPT-User,
Perplexity-User) — a person is behind the request,
unlike a crawler.
Looking for whether the agent is on a phone or tablet? That's
device.attributes.is_mobile, not a root flag.
Errors
Error responses use standard HTTP status codes plus a JSON body that names what went wrong.
Each entry in the errors array includes a
message describing the failure.
{
"errors": [
{
"message": "A user agent is required. Provide one via the ua parameter or send a User-Agent header."
}
]
}
The only API-specific failure is a 400 raised while
validating the request: no user agent could be resolved, because the
ua parameter is absent and the request carries no
User-Agent header. There is no "invalid user agent" error —
any string parses.
| Status | Meaning |
|---|---|
200 | OK. |
400 | Malformed request: no user agent could be resolved (see above). |
401 | Missing, unrecognized, revoked, or
expired API key, across both Authorization header and ?api-key=
query-param forms. |
403 | Forbidden. Returned when the account is frozen (e.g. monthly request limit exceeded) — frozen accounts clear on the monthly billing anniversary or when you upgrade — and when the request origin isn't listed on the key's allowed CORS origins (see Gotchas). |
404 | Path doesn't match a known endpoint. |
405 | HTTP method not allowed. User Agent
endpoints are GET-only. |
410 | API version is no longer active. |
429 | Too Many Requests. Returned by the edge rate limiter when sustained traffic exceeds 20 requests per 10 seconds; retry shortly. |
500 | Unhandled server error. |
There is no 502: parsing is first-party, so no request
ever depends on an outside service being reachable.
Data sources
Classification comes from curated detection data covering thousands of agents — browsers and their engines, search-engine and AI crawlers (including the newest AI agents), link fetchers, command-line tools, HTTP libraries, email clients, in-app webviews, consoles, TVs, and vehicles — refreshed regularly as new agents appear. Concretely: 100+ browser families, 70+ operating systems, 50,000+ device models reconciled against the manufacturers' own marketing names, and 190+ named bot operators. Keeping that current is the work you're outsourcing.
Every string is read by several independent parsers and the answer is the one they agree on, so a single parser's blind spot doesn't become your data. All of it is first-party: the strings you submit are never forwarded to a third party.
Gotchas
CORS & browser origins
Calling this API from frontend JavaScript is blocked until the page's
hostname is listed on the API key's allowed origins, so a key that works
from your server will fail in the browser until you add it. Requests that
carry no Origin header (server-to-server calls,
curl) are never affected. Add hostnames on the
API Keys page, and see
Browser usage & CORS for the
matching rules.