REST Countries

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.type answer, 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 Ultra rather than SM-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/json on every response. Successful payloads sit under a top-level data key with the parse record in data.objects; failures come back under errors as an array of objects carrying a message.
  • 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 is null; a whole block that resolved nothing collapses to a single null rather than a tree of them, so os on a bot is null, 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-Agent header is parsed, so /user-agent/v1 alone 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();

Run this request →

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

GET /user-agent/v1

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.

A normal browser — Chrome on macOS
GET /user-agent/v1?ua=Chrome on macOS Run →
Mobile — Safari on iPhone
GET /user-agent/v1?ua=Mobile Safari on Apple iPhone Run →
Android TV — Chrome on a Sony Bravia (smart_tv)
GET /user-agent/v1?ua=Chrome on Sony Bravia 4K Run →
Instagram — the app's built-in browser (mobile_app)
GET /user-agent/v1?ua=Instagram on Apple iPhone 13 Pro Max Run →
GPTBot — OpenAI's crawler (is_ai_crawler)
GET /user-agent/v1?ua=GPTBot Run →
ChatGPT-User — an AI assistant browsing for a user (is_ai_assistant)
GET /user-agent/v1?ua=ChatGPT-User Run →
ClaudeBot — Anthropic's crawler
GET /user-agent/v1?ua=ClaudeBot Run →
Googlebot — a search-engine crawler
GET /user-agent/v1?ua=Googlebot Run →
AhrefsBot — an SEO crawler
GET /user-agent/v1?ua=AhrefsBot Run →
Twitterbot — a link-preview fetcher
GET /user-agent/v1?ua=Twitterbot Run →
curl — a command-line tool
GET /user-agent/v1?ua=curl Run →
python-requests — an HTTP library
GET /user-agent/v1?ua=python-requests Run →
Your own user agent (nothing supplied)
GET /user-agent/v1 Run →

Query parameters

ua string
User-agent string to parse. Omit it to parse the request's own User-Agent header. Values may be sent raw, percent-encoded, or with + for spaces; a + is always read as a space.
response_fields string
Allowlist of properties to include in the returned object. When set, the response carries only those properties and nothing else (e.g. 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
Blocklist of properties to drop from the returned object (e.g. 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-print the JSON response. The bare flag (?pretty) and truthy values (true, 1) enable; anything else (including false and 0) leaves the response compact.

Example

Request
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)
Response
{
  "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
The string that was parsed: the ua parameter or the request's own User-Agent header, whichever the request resolved to.
label string
Display-ready agent summary: the client's name plus the one context that best identifies the traffic. Recognized hardware takes that slot when the device is something other than a desktop (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 labelclient, 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
Display-ready client summary, maker-first with the major version (e.g. 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
The client's name (e.g. Chrome, Mobile Safari, Googlebot, curl).
type string
What kind of agent this is. One of: 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 (ChromeGoogle, FirefoxMozilla), 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
The rendering engine, as { 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
Family flags for the client — always an object, every leaf a definite boolean. Flags match across variants (Mobile Safari counts as 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 (GPTBotOpenAI, GooglebotGoogle, InstagramMeta), 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
The canonical organization name (e.g. 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
The producer exactly as declared, un-normalized (e.g. OpenAI OpCo, LLC). Prefer name for grouping.
email_address string
The contact address the string itself declares (e.g. [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
Display-ready OS summary, maker-first with the major version (e.g. Apple macOS (v10)).
name string
The operating system (e.g. 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 (macOSApple).

device

The hardware. Always an object — never collapsed — though for an agent that identifies no hardware every leaf but attributes is null.

label string
Display-ready device summary, maker-first (e.g. 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
The hardware model by marketing name wherever one is known — so an Android handset resolves to 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
The device class. One of: 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
Form-factor flags — always an object, every leaf a definite boolean: 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
The agent is automated rather than a human's browser. Deliberately broad: it covers crawlers, fetchers, command-line tools, and HTTP libraries alike, so it's the one flag to gate on when you mean "not a person".
is_crawler boolean
The agent crawls in bulk rather than fetching on demand. Narrower than 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
The agent is a known AI training or indexing crawler (e.g. GPTBot, ClaudeBot) — the kind that reads pages in bulk.
is_ai_assistant boolean
The agent is a known AI assistant fetching one page live on a user's behalf (e.g. 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.

StatusMeaning
200OK.
400Malformed request: no user agent could be resolved (see above).
401Missing, unrecognized, revoked, or expired API key, across both Authorization header and ?api-key= query-param forms.
403Forbidden. 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).
404Path doesn't match a known endpoint.
405HTTP method not allowed. User Agent endpoints are GET-only.
410API version is no longer active.
429Too Many Requests. Returned by the edge rate limiter when sustained traffic exceeds 20 requests per 10 seconds; retry shortly.
500Unhandled 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.

Clean to use. The data you receive is yours to use directly in your own product; there's no upstream attribution chain to maintain. (Use of the REST Countries API itself is governed by the Terms.)

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.