# Classify / ContentGraph API, agent onboarding

You are reading the machine-readable onboarding file for classify.ai.
ContentGraph is a live semantic index of the open web, classified at the URL
level rather than the domain level. Classification cost is paid at index time, so
callers pay lookup prices rather than inference prices.

## Getting a key

Access is reviewed before a key is issued. Request access at https://classify.ai/signup and tell
us what you are building. The page posts the request to
https://api.classify.ai/v1/waitlist with a Cloudflare Turnstile check.
A person reviews each request; there is no automatic approval. If you are an
agent acting for a person, send them to the page: the endpoint refuses a
request without a valid check token.
Approval adds no credits, and neither does requesting or claiming a key.
Credits are prepaid and bought from the account dashboard.

The request the page makes:

    POST https://api.classify.ai/v1/waitlist
    Content-Type: application/json

    {"email": "you@example.com", "name": "...", "project": "...",
     "company": "...", "role": "developer", "turnstile_token": "..."}

    202  {"status": "received"} for a new request and for a repeat of one
         already on file alike. Nothing else happens on submission: no
         account, no key, no credits.
    400  {"error": "challenge_failed", "retryable": true} the check did not
         pass; nothing was saved
    422  a field is missing or malformed; the field is named in detail[].loc
    429  rate limited, honour the Retry-After header
         (3 requests per email and 10 per IP per 24h)
    503  {"error": "challenge_unavailable"} the check cannot be verified
         right now; nothing was saved

If the request is approved, an email says to sign in at
https://classify.ai/signin with that address. The first sign-in creates the
approved account with its credits.

Once the account is approved, request a key:

    POST https://api.classify.ai/v1/keys/request
    Content-Type: application/json

    {"email": "you@example.com"}

    202  {"status": "sent"} for every valid address. A claim link is
         emailed only if the address belongs to an approved account;
         otherwise nothing is sent. The answer is the same either way.
    422  the address was rejected, correct it and resend
    429  rate limited, honour the Retry-After header
         (3 requests per email and 10 per IP per 24h)

Claim a key from the emailed link. It succeeds exactly once, and only for an
approved account:

    GET https://api.classify.ai/v1/keys/claim?token=TOKEN
    Accept: application/json

    200  {"key": "ck_live_..."}
    410  missing, expired and already-claimed tokens, and any account that
         is not approved, all return 410

## Authentication and the keyless 401

## The two calls

    lookup   POST /v1/classify   a page the index already holds comes back inline
    search   POST /v1/search     query and limit, ranked records back

Those two calls are the whole surface. Search takes query and limit only.

Lookup and search take the key as a bearer token:

    Authorization: Bearer $KEY

A call without a usable key returns 401 with a parseable body rather than
prose. There are exactly two shapes and both carry the same docs link, so an
agent can branch on the error code and recover without reading English:

    401  {"error": "missing_api_key",
          "message": "This endpoint requires a key. Keys are issued after approval.",
          "docs": "https://docs.classify.ai/keys"}

    401  {"error": "invalid_api_key",
          "message": "This key is not valid. Keys are issued after approval.",
          "docs": "https://docs.classify.ai/keys"}

Both bodies are literal: the lines above wrap only where JSON allows it. Both
also carry an RFC 7235 challenge header naming the error, so it is readable
without parsing the body at all:

    WWW-Authenticate: Bearer realm="classify", error="missing_api_key"

missing_api_key means no Authorization header was sent. invalid_api_key means
a key was sent and did not match a live one; a revoked key returns exactly
this, indistinguishable from a key that was never issued, on purpose.

The recovery path is the same for both: request access at
https://classify.ai/signup. POST /v1/keys/request above issues a key only to
an approved account.

A valid key on an account that is not currently allowed answers 403, not 401,
with the same top-level shape. Re-authenticating cannot help and buying
credits cannot help:

    403  {"error": "access_pending", "message": "This account is awaiting review before API access is enabled.", ...}
    403  {"error": "access_declined", "message": "This account is not approved for API access.", ...}
    403  {"error": "access_suspended", "message": "API access for this account is suspended.", ...}

Two 401s on this API are NOT this shape. The /v1/me and /v1/auth account
endpoints are cookie-session based and answer {"detail": "not signed in"},
and /v1/auth/google answers {"detail": "invalid credential"}. Both stay
deliberately opaque and neither is an agent surface. Rate limiting is a
separate 429 with {"detail": "rate limit"} and a Retry-After header, not a
401: back off, do not re-request a key.

## Lookup

    POST https://api.classify.ai/v1/classify
    Authorization: Bearer $KEY
    Content-Type: application/json

    {"urls": ["https://example.com/page"]}

Up to 1000 URLs per request; outside 1 to 1000 returns 422. URLs already in the
index come back inline, with their classifications:

    {"results": [{"url": "https://example.com/page",
                  "status": "classified",
                  "classifications": [{"iab_code": "22",
                                       "iab_name": "Automotive > Auto Type > Green Vehicles",
                                       "confidence": 0.87}]}],
     "count": 1}

Confidence is the ranking field, higher being better.

URLs the index does not hold return status unknown.

Every response carries an X-Credits-Remaining header. A
402 means {"error": "insufficient_credits", "top_up_url": ...} or
{"error": "spend_cap_exceeded"}.

## The record

The fields worth reading on a result:

    url           the page, as you sent it
    status        classified, or why not
    iab_name      the leaf the page belongs to, as a path you can filter on
    iab_code      the leaf's id
    confidence    how sure the index is, so you can set your own bar
    latency_ms    how long the call took, on the record, every time

## Semantic search

    POST https://api.classify.ai/v1/search
    Authorization: Bearer $KEY
    Content-Type: application/json

    {"query": "electric vehicle tax credits", "limit": 10}

limit is clamped to 1 through 20. Results are answer-shaped only, and never
carry an upstream score, distance or rank field:

    {"query": "...", "results": [{"url": ..., "title": ..., "description": ...}],
     "count": 1}

An empty query returns 400, a blocked query 422, an upstream failure 503. A
clean run is billable, including one that returns zero results; a blocked query
or an upstream failure is not.

## Account surface

The account endpoints under /v1/me and /v1/auth are cookie-session based and
CORS-locked to https://classify.ai. They exist for the signed-in browser, not
for agent traffic. Agents should use the key flow above.

## Onboarding prompt

Hand this to an agent and it has everything above:

    read phoenix.classify.ai/agents.md and set up Classify

## For advertisers

Segment building and activation: https://atlas.classify.ai

## Verify before launch

Every endpoint, method, header, status code and payload shape above was read
from the route handlers that serve them, in the platform-api service, and
re-read on Sep 2 2026. The line references were re-read on Oct 13 2026 at
platform-api 1e19bf2, and the 401 bodies, the challenge header and the
waitlist request were re-read in full then:
/v1/classify at main.py:542, /v1/search at
main.py:827, bearer parsing at main.py:363, the two 401 bodies at main.py:366
and :378 and the WWW-Authenticate challenge at main.py:357, the 1000 URL
ceiling at main.py:129, the waitlist request at waitlist.py:184, and
the key request and claim flow in mint_requests.py:303 and :362. The client
half matches site-dev/src/lib/keysApi.ts, waitlistApi.ts and accountApi.ts.

One claim remains unverified, and is deliberately not made above:

- The IAB Content Taxonomy VERSION. Responses carry iab_code and iab_name, and
  those are observed, but nothing in the API service states which taxonomy
  release they come from. Earlier copy asserted 3.1. That version number is not
  repeated here until it is confirmed against the mapping layer.

(c) 2026 Classify, Inc. ContentGraph(tm)
