Tools from your program: access key and API

How to reach the checks from your own program: key, request, answer and limits.

Every check in the Tools section works not only from the page but from your own program: the same computation and the same shape of answer. Everything you need to start in ten minutes is here.

How to get a key

  1. Sign in to the tools account with a code sent to your email.
  2. An access key comes with the Pro plan. The free and Basic levels have no key — an attempt to issue one answers TOOL_KEY_NOT_ALLOWED.
  3. In the “Access keys” section press “Create key” and give it a name — that is how you will recognise it in the list.
  4. The key value is shown once — at the moment of creation. We store only its fingerprint, so “show it again” is impossible for you and for us. Copy the key straight away. Lost it — revoke the old one and issue a new one.

A key looks like this: the prefix tlk_ and 64 hexadecimal characters. The prefix makes it recognisable in someone else's log — do not leave a key in page source and do not commit it to a code repository.

The Pro plan allows five live keys. A revoked key stops working the same instant.

How to pass the key

The key goes in the Authorization header with the word Bearer:

Authorization: Bearer tlk_YOUR_KEY

Other ways do not work on purpose: Authorization: Basic …, an empty Bearer with no value, or a key without the tlk_ prefix get 401 TOOL_KEY_INVALID. We will not quietly fall back to a guest — otherwise you would hit guest limits and read them as random refusals.

A key works only on the site that issued it. The same key sent to another site of the platform answers 401 TOOL_KEY_INVALID — the same code as a made-up key.

No header at all is not an error: the request goes through as a guest, with guest limits.

The first request

One check is one POST request. The address is built from the tool kind:

POST https://seo-gen.com.ua/api/public/tools/<kind>/run

A full example you can copy and run (put your own key in):

curl -X POST https://seo-gen.com.ua/api/public/tools/http-status-checker/run \
  -H "Authorization: Bearer tlk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target":"https://example.com/"}'

The request body has two fields: target — the address to check, and the optional options — the settings of that kind.

What the answer consists of

A successful answer is 200 and two fields: data with the result and limit with the remaining volume. The example below leaves out the headers section to keep the page short — a live answer has it.

{
  "data": {
    "kind": "http-status-checker",
    "target": "https://example.com/",
    "checkedAt": "2026-09-19T08:12:07.078Z",
    "cached": false,
    "verdict": "ok",
    "sections": [
      {
        "id": "summary",
        "rows": [
          { "status": 200, "final": "https://example.com/", "hops": 0, "time_ms": 95, "soft_404": "" }
        ],
        "columns": ["status", "final", "hops", "time_ms", "soft_404"]
      },
      { "id": "chain", "rows": [], "columns": ["step", "from", "to", "status", "scheme"] }
    ],
    "notes": []
  },
  "limit": { "remaining": null }
}
  • data.kind — the tool kind — the same one as in the address
  • data.target — the address that was checked; null when the kind has none
  • data.checkedAt — the time of the check, ISO-8601 with a time zone
  • data.cachedtrue — the answer came from our cache (it lives 10 minutes); the time stays the time of the actual check
  • data.verdict — one word of summary — the list is below
  • data.sections — result sections: each has an id, rows and columns — the column order
  • data.notes — codes of caveats (not ready-made text): why not everything was checked and what is worth knowing
  • limit.remaining — how many checks are left; null means “no limit”

If a section is shown in part, it carries capped with two numbers: how many rows are shown and how many were found.

Summary words

  • ok — checked, no trouble found
  • guess — determined roughly: the answer rests on one weak clue
  • partial — checked in part — some of it did not happen for OUR reason (our time budget, our size limit, we were slowed down). The “checked N of M” numbers are in the summary
  • warn — there are caveats
  • unknown — could not be determined: the site's answer could not be parsed
  • fail — a fault was found

Tool kinds and what to send them

Every kind below works with a key. The “Options” column is the content of the options field: round brackets hold the type and the default, square brackets the allowed values.

Kind (kind)NameAddressOptions (options)Answer sections
auditWebsite auditrequireduser_agent (select, "*") [*|Googlebot|Bingbot|YandexBot|GPTBot|ClaudeBot|PerplexityBot|Google-Extended]summary, issues, robots_issues, sitemap_issues, hreflang_issues, sources, conflicts, plan, chain, checks, directives, meta, headings
seo-checkerPage SEO analysisrequiredcheck_links (boolean, true)summary, issues, indexability, meta, headings, content, images, links
cms-detectorCMS Detector / Detect a site CMSrequiredcms, signals, tech
indexability-checkerIndexability checkrequireduser_agent (select, "*") [*|Googlebot|Bingbot|YandexBot|GPTBot|ClaudeBot|PerplexityBot|Google-Extended]; check_targets (boolean, true)verdict, conflicts, sources, targets, plan
http-status-checkerHTTP Status Checkerrequiredcheck_domain_variants (boolean, false)summary, chain, variants, headers
redirect-checkerRedirect Checkerrequiredcheck_domain_variants (boolean, false)summary, chain, variants, headers
robots-checkerRobots.txt Checkerrequireduser_agent (select, "*") [*|Googlebot|Bingbot|YandexBot|GPTBot|ClaudeBot|PerplexityBot|Google-Extended]; paths (list); custom_file (textarea)verdict, paths, ai_bots, issues, file, health
sitemap-checkerSitemap Checkerrequiredcheck_urls (boolean, true); sample_size (number, 20)summary, robots, issues, tree, sample, urls, searched
robots-meta-checkerRobots Meta Checkerrequireduser_agent (select, "*") [*|Googlebot|Bingbot|YandexBot|GPTBot|ClaudeBot|PerplexityBot|Google-Extended]; with_robots_txt (boolean, true)verdict, issues, sources, directives
canonical-checkerCanonical Checkerrequiredcheck_target (boolean, true)summary, checks, chain
link-extractorLink Extractorrequiredcheck_status (boolean, true); unique_only (boolean, false)summary, links, domains, special
link-attributesNofollow / Sponsored / UGC Checkerrequiredmy_domain (domain); check_status (boolean, true)page_block, summary, links, mine
anchor-analysisLink anchor analysisrequiredcommercial_words (textarea)types, anchors, words, conflicts
meta-extractorMeta tag extractionrequiredsummary, tags, cards, code
hreflang-checkerHreflang Checkerrequiredcheck_return (boolean, true)summary, alternates, reciprocal, issues
headings-extractorH1-H6 Extractornot neededhtml (textarea)tiles, outline
pagespeed-checkerPageSpeed / Core Web Vitals Checkerrequiredstrategy (select, "mobile") [mobile|desktop]summary, vitals, field, opportunities
broken-linksBroken Link Checkerrequiredmode (select, "page") [page|site]; max_pages (number, 100); check_status (boolean, true); urls (textarea)summary, broken, redirects, skipped
internal-linksInternal links checkerrequiredmode (select, "page") [page|site]; max_pages (number, 100); check_status (boolean, true); subdomains_internal (boolean, false); urls (textarea)summary, links, dead_ends, orphans
external-linksExternal links checkerrequiredmode (select, "page") [page|site]; max_pages (number, 100); check_status (boolean, true); subdomains_internal (boolean, false); urls (textarea)summary, links, skipped
sitemap-generatorsitemap.xml generatornot neededurls (textarea); alternates (boolean, true); locales (text, "uk, ru, en"); lastmod (date); changefreq (select) [|always|hourly|daily|weekly|monthly|yearly|never]; priority (number)numbers, xml, notes, next
json-ld-validatorJSON-LD Validatornot neededcode (textarea)summary, blocks, issues
hreflang-generatorhreflang generatornot neededversions (rows); x_default (select); format (select, "html") [html|xml|http]; site_country (country, "UA"); comments (boolean); heads (textarea)numbers, code, checks, map

Address lists and files

A list of addresses and a site crawl go into a queue: the request answers at once, and the result is picked up by a link.

curl -X POST https://seo-gen.com.ua/api/public/tools/http-status-checker/batch \
  -H "Authorization: Bearer tlk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"list":"https://example.com/\nhttps://example.com/nope\nне адрес"}'

The 202 answer carries the job id, a link to it, how many addresses were accepted and the list of rejected lines right away — with the line number and the reason.

{
  "data": {
    "id": "537f4024-0537-4abc-b564-56dc55569538",
    "url": "/api/public/tools/jobs/537f4024-0537-4abc-b564-56dc55569538",
    "accepted": 2,
    "rejected": [ { "code": "TARGET_INVALID", "line": 3, "value": "не адрес" } ],
    "status": "queued"
  },
  "limit": { "remaining": 4997 }
}

A list can also be sent as a file — multipart/form-data, field file; CSV, XLSX and TXT up to 5 MB:

curl -X POST https://seo-gen.com.ua/api/public/tools/http-status-checker/batch \
  -H "Authorization: Bearer tlk_YOUR_KEY" \
  -F "file=@spisok.csv"

Progress and result come from the same link. job.status goes queuedrunningdone, and job.done with job.total show the progress:

curl https://seo-gen.com.ua/api/public/tools/jobs/537f4024-0537-4abc-b564-56dc55569538 \
  -H "Authorization: Bearer tlk_YOUR_KEY"

A finished result is exported as a file — a table or JSON:

curl -OJ https://seo-gen.com.ua/api/public/tools/jobs/537f4024-0537-4abc-b564-56dc55569538/export.csv \
  -H "Authorization: Bearer tlk_YOUR_KEY"
curl -OJ https://seo-gen.com.ua/api/public/tools/jobs/537f4024-0537-4abc-b564-56dc55569538/export.json \
  -H "Authorization: Bearer tlk_YOUR_KEY"

A site crawl uses the same route, but instead of a list it takes one address and options.mode = "site". How many pages to walk is set by the plan.

What counts as spending

  • One check POST …/run is one unit of the plan's monthly volume and one key request.
  • A list of N addresses is N units: every address is a separate trip to someone else's site.
  • A site crawl is one unit at the moment it enters the queue; how many pages it walks is capped by the plan.
  • An answer from the cache (cached: true) still spends volume: the volume counts requests, not trips.
  • A refusal by limit (QUOTA_EXCEEDED, API_QUOTA_EXCEEDED) arrives BEFORE the work: we will not crawl someone else's site and only then say “not allowed”.
  • Looking at job progress and exporting the result spend nothing.

Refusals

A refusal is always a body with two fields: code as a machine word and error as a sentence in the language of the request (taken from the Accept-Language header). The same code is repeated in the X-CMS-Error-Code header.

CodeHTTPWhat it means
TOOL_KEY_INVALID401The key did not match: wrong, revoked, from another site, or passed the wrong way.
TOOL_KEY_NOT_ALLOWED403The key exists, but the plan no longer allows it — the subscription was downgraded or ended.
TOOL_KIND_UNKNOWN400No such tool kind.
TOOL_NOT_IMPLEMENTED501The kind is declared but still in development.
TARGET_INVALID422No address was passed, or it does not look like an address.
TARGET_NOT_ALLOWED400The address does not lead to the open internet: an internal or service network. We do not follow those.
TARGET_UNREACHABLE502The domain does not exist or does not answer. This is an answer ABOUT THE OTHER SITE, not our fault.
TARGET_TIMEOUT504The site did not answer within 10 seconds.
TARGET_TOO_BIG413The page is larger than 5 MB — we do not read further.
TARGET_TOO_MANY_REDIRECTS502The redirect chain is longer than five steps.
OUR_BUDGET504OUR time budget for the whole run ran out. This is not a fault of the site.
QUOTA_EXCEEDED429The plan's monthly volume is used up. The count starts over next month.
API_QUOTA_EXCEEDED429The monthly volume of KEY requests is used up — it is smaller than the check volume.
RATE_LIMIT_EXCEEDED429The guest hourly limit. A key on a paid plan has none — the monthly volume counts there.
BATCH_TOO_LARGE422The list holds more addresses than the plan allows. No volume was spent on it.
BATCH_BUSY429Two jobs of this site are already running — wait for one of them to finish.
FILE_UNREADABLE422The file could not be read. CSV, XLSX and TXT work.
JOB_NOT_FOUND404No such job: its term (seven days) expired or it belongs to another site.

Limits and politeness to other people's sites

Pro plan limits: 30,000 checks a month, 5,000 key requests a month, 5,000 addresses in one list, a crawl of up to 50,000 pages, five keys, 365 days of history. What is left is in the limit.remaining field of every answer.

Politeness limits towards SOMEONE ELSE'S site are lifted by no plan — they are not about who pays, they are about who is being checked:

  • no more than two simultaneous requests to one host and a 500 ms pause between them;
  • we wait 10 seconds for a page, read the first 5 MB, follow no more than five redirects;
  • a single check has 18 seconds in total: if we do not finish, the answer comes back honestly partial (partial), not cut off;
  • we introduce ourselves as SeoGenCMS-Tools — you can block us by that name in your own robots.txt;
  • two jobs per site run at the same time.

Check other people's sites with the same care you would want for your own.