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, the organization the traffic acts for, the operating system, the
device, and bot signals. Give it a string in the query string, or give it nothing at all
and it parses the agent the request itself came from.
-
Coverage. Crawlers, AI agents, link-preview fetchers, command-line
tools, HTTP libraries, email clients, in-app webviews, and media players are
classified alongside browsers, each with its own
client.type. 100+ browser families, 70+ operating systems (down to the Linux distribution), 50,000+ device models by marketing name (so an Android handset reads asGalaxy S24 Ultra, notSM-S928B), and 190+ named bot operators. -
AI traffic. Training crawlers
(
is_ai_crawler) and live user-triggered assistant fetches (is_ai_assistant) are flagged separately, since most sites treat them differently. Every known bot also names its operator (operator.name), so traffic can be filtered by company. -
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 response shape. Every response is the same
structure, and unknown always reads as
null, never a missing key, never an empty string. A block that resolved nothing collapses to a singlenull, sooson a bot isnullrather than a tree of nulls. See Response objects. -
The user agent is optional. 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. 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
Every parameter below is optional unless it carries a required badge.
ua
string
required
User-Agent header.
Values may be sent raw,
percent-encoded, or with + for spaces; a
+ is always read as a space.
sec-ch-ua-*
string
ua to sharpen the
parse where the user-agent string alone is vague. Modern Chromium browsers
withhold the device model and the exact platform version from the string
itself (an Android phone reports its model as
K), and these carry what it left out.
Nine are accepted, each named exactly as the browser's own header:
sec-ch-ua,
sec-ch-ua-arch,
sec-ch-ua-bitness,
sec-ch-ua-form-factors,
sec-ch-ua-full-version-list,
sec-ch-ua-mobile,
sec-ch-ua-model,
sec-ch-ua-platform, and
sec-ch-ua-platform-version.
Values may be sent quoted the way a browser writes them (
sec-ch-ua-model="Pixel 8") or bare
(sec-ch-ua-model=Pixel 8), raw or
percent-encoded, with + read as a space. Send
sec-ch-ua,
sec-ch-ua-full-version-list and
sec-ch-ua-form-factors in the browser's own
format, since each carries a list.
sec-ch-ua-platform is only used when
sec-ch-ua-platform-version comes with it;
supplied alone it would name an operating system without its version, so
it's ignored instead. Values far longer than a real hint are ignored too.
Only Chromium-family browsers produce these; Safari and Firefox send none,
and requests without them parse exactly as before.
client-hints
boolean
sec-ch-ua-* parameters above take effect as soon
as they're sent. Set it to false
(or 0, no,
off) to have them accepted but ignored, which
returns the same object the user-agent string alone would produce. Useful
for seeing exactly what the hints are contributing.
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 },
"logo": { "url_svg": "https://brands.restcountries.com/v1/svg/chrome/logo.svg" },
"wordmark": { "url_svg": "https://brands.restcountries.com/v1/svg/chrome/wordmark.svg" },
"maker": {
"name": "Google",
"logo": { "url_svg": "https://brands.restcountries.com/v1/svg/google/logo.svg" },
"wordmark": { "url_svg": "https://brands.restcountries.com/v1/svg/google/wordmark.svg" },
"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 },
"logo": { "url_svg": "https://brands.restcountries.com/v1/svg/blink/logo.svg" },
"wordmark": null,
"maker": {
"name": "Google",
"logo": { "url_svg": "https://brands.restcountries.com/v1/svg/google/logo.svg" },
"wordmark": { "url_svg": "https://brands.restcountries.com/v1/svg/google/wordmark.svg" },
"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 },
"logo": { "url_svg": "https://brands.restcountries.com/v1/svg/macos/logo.svg" },
"wordmark": { "url_svg": "https://brands.restcountries.com/v1/svg/macos/wordmark.svg" },
"maker": {
"name": "Apple",
"logo": { "url_svg": "https://brands.restcountries.com/v1/svg/apple/logo.svg" },
"wordmark": { "url_svg": "https://brands.restcountries.com/v1/svg/apple/wordmark.svg" },
"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",
"logo": { "url_svg": "https://brands.restcountries.com/v1/svg/apple/logo.svg" },
"wordmark": { "url_svg": "https://brands.restcountries.com/v1/svg/apple/wordmark.svg" },
"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 },
"logo": { "url_svg": "https://brands.restcountries.com/v1/svg/openai/logo.svg" },
"wordmark": { "url_svg": "https://brands.restcountries.com/v1/svg/openai/wordmark.svg" },
"maker": {
"name": "OpenAI",
"logo": { "url_svg": "https://brands.restcountries.com/v1/svg/openai/logo.svg" },
"wordmark": { "url_svg": "https://brands.restcountries.com/v1/svg/openai/wordmark.svg" },
"links": { "wikipedia": "https://en.wikipedia.org/wiki/OpenAI", "official": "https://openai.com/" }
}
},
"operator": {
"name": "OpenAI",
"name_legal": "OpenAI OpCo, LLC",
"email_address": null,
"logo": { "url_svg": "https://brands.restcountries.com/v1/svg/openai/logo.svg" },
"wordmark": { "url_svg": "https://brands.restcountries.com/v1/svg/openai/wordmark.svg" },
"links": { "info": "https://openai.com/gptbot", "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, so 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).
logo
object
{ url_svg }: the brand mark for this name, as a
hotlinkable SVG on the free brand CDN. The same pair of
logo and
wordmark blocks sits beside most names the
response resolves (the device itself is the exception, carrying imagery
only on device.maker), so
client.maker.logo.url_svg is Google's mark where
client.logo.url_svg is Chrome's.
null when no image exists for that name. See
Free brand CDN for the terms and the full list
of blocks that carry them.
wordmark
object
{ url_svg }: the brand's name set as a logotype,
for places a bare mark reads as unlabelled. Same rules as
logo, and independent of it: a brand may publish
one, both, or neither, and each missing block is
null on its own.
maker
object
{ name, logo, wordmark, 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, logo, wordmark, maker }: the same
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, covering
Chrome, Chromium, Edge 79+, and 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, and deliberately
so: 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, logo, wordmark, 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, logo, wordmark, 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.
Free brand CDN
The User Agent API is backed by a free global CDN for brand imagery: logo and wordmark SVGs for the browsers, engines, operating systems, device makers, and bot operators the API names. No account is needed, no API key is required, and the CDN can be used entirely anonymously.
Every URL is free to hotlink. Point an <img> straight
at it rather than copying files into your own project, and the images stay current as
brands are added and refreshed. Everything is served as SVG, so a single URL renders
crisply at any size.
<img src="https://brands.restcountries.com/v1/svg/chrome/logo.svg" alt="Chrome logo">
You don't have to build the URLs yourself. Where a brand image exists, the parse
response carries it as logo.url_svg and
wordmark.url_svg beside the name it belongs to:
client, client.maker,
client.engine,
client.engine.maker,
operator, os,
os.maker, and
device.maker. A name with no brand image, or a brand with
only one of the two files, leaves the missing block
null.
The device itself carries no imagery. Device names are model names
(Galaxy S24 Ultra,
Redmi Note 13) and no brand publishes a mark for an
individual handset, so device branding lives on
device.maker instead.
Brand requests are delivered from Cloudflare edge nodes with cache rules for stable image URLs, with CloudFront as the fallback source and S3 behind that as the durable origin. The brand CDN is cookie free and GDPR compliant, and it is designed for direct browser use with no tracking.
The images are the trademarks of the companies they identify. The CDN is free to use, but it doesn't grant any right in the marks themselves. Use them to identify the brand, not to imply it endorses you.
Gotchas
Names & labels are display strings
label, client.name,
os.name, device.name, and the
per-block labels are meant to be read, not compared. They change more often than
anything else in the record: products get renamed, new agents arrive under names close
to existing ones, and the canonical spelling for an agent gets revised as detection
improves. A client reported as Chrome Mobile today can come
back spelled differently after a refresh, without the traffic itself having changed.
Anything that drives logic should read flags,
client.attributes, and
device.attributes instead. Those are a fixed set of
booleans, always present and always a definite true or
false, and they are maintained so their meaning holds
while the names underneath them move.
client.attributes.is_chrome keeps covering Chrome Mobile
and Chrome Headless whatever those clients end up being called, and
flags.is_bot keeps covering a crawler that gets renamed.
client.type and device.type
are closed sets and are similarly safe to branch on. Keep the names and labels for
display, logging, and analytics, and treat an exact string match against one as
something that will need revisiting.
CORS & browser origins
Calling this API from frontend JavaScript is blocked unless 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.
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. It is refreshed regularly as new agents appear.
The following is currently suported: 100+ browser families, 70+ operating systems, 50,000+ device models (reconciled against manufacturer databases), and 190+ bot operators.
User agents are processed by multiple independent parsers, with the concensus being determined before responding. All of it is first-party, meaning the user agents are never forwarded to a third party.