API documentation

Submit a URL, Genchi Scan fully analyzes the page and reports what's wrong - dead links and images, mixed content, TLS issues, slow responses, detected technologies, and more. Submit a scan, then poll for the findings.

Interactive API reference (Swagger UI) · or download the OpenAPI spec to import into Postman, Insomnia, or generate a client.

Authentication

Create an API key on your dashboard and send it as a bearer token on every request. Keys look like gk_live_….

Authorization: Bearer gk_live_…

Base URL: https://api.genchi.dk

Create a scan

POST /v1/scans - queues a scan and meters one invocation against your quota. Returns immediately with an id.

FieldTypeDefaultNotes
urlstring (URL)requiredThe page to scan. Must be a public http(s) URL.
waitMsinteger 0-300000Extra settle time after load before collecting findings.
viewport{ width, height }1280 × 800Render viewport in px (each 1-4000).
screenshotbooleanfalseCapture full-page screenshots (desktop/tablet/phone). Requires the Screenshots feature (Pro & Enterprise).
curl -X POST https://api.genchi.dk/v1/scans \
  -H "Authorization: Bearer gk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com"}'
HTTP/2 202
ratelimit-limit: 100
ratelimit-remaining: 97
ratelimit-reset: 2
x-concurrency-limit: 10
x-concurrency-remaining: 6
x-quota-limit: 15000
x-quota-remaining: 14988
x-quota-reset: 1209600

{
  "id": "7f3c…-uuid",
  "status": "pending",
  "usage": { "used": 12, "remaining": 14988, "limit": 15000 }
}

Bulk: POST /v1/scans/batch with { "urls": ["https://a.com", "https://b.com"] } (up to 100) queues many at once for enrichment/research. Each URL meters one invocation; it returns { accepted: [{ url, id }], rejected: [{ url, reason }], usage }, partial-accepting until your quota runs out (reason: "quota_exceeded") or you reach your plan's cap on unfinished scans in the queue (reason: "queue_full" - drain by polling/completing some, then resubmit the rest). Then poll each id as below.

Get a scan & findings

GET /v1/scans/:id - poll until status is completed or failed. A scan moves through pending → running → completed (or failed). Most scans finish in a few seconds; poll every 1-2s.

Result retention: finished results are kept for a limited window that depends on your plan (Free 5 minutes, Researcher 7 days, Basic 2 hours, Pro 24 hours). Fetch the findings within that window - afterwards the result is purged and GET /v1/scans/:id returns 404.

curl https://api.genchi.dk/v1/scans/7f3c…-uuid \
  -H "Authorization: Bearer gk_live_…"
{
  "id": "7f3c…-uuid",
  "url": "https://example.com",
  "status": "completed",
  "createdAt": "2026-06-03T10:00:00.000Z",
  "startedAt": "2026-06-03T10:00:01.000Z",
  "finishedAt": "2026-06-03T10:00:14.000Z",
  "findings": {
    "resultKind": "full",
    "http_status": 200,
    "finalUrl": "https://example.com/",
    "metrics": {
      "dead_links": 2,
      "dead_images": 0,
      "dead_resources": [ { "url": "https://…/missing.js", "status": 404 } ],
      "mixed_content": 0,
      "ttfb_ms": 180,
      "page_weight_bytes": 1843200
    },
    "cert": { "subject_cn": "example.com", "valid_to": "2026-09-01T…", "error": null },
    "securityHeaders": { "strict-transport-security": "max-age=…" },
    "technologies": { "nginx": { "version": "1.25" }, "WordPress": { "version": "6.5" } },
    "wordpress": { "theme": { "slug": "astra", "version": "4.6.1", "requiresWp": "5.3", "testedWp": "6.5", "requiresPhp": "5.6" }, "plugins": { "count": 7, "found": [ { "slug": "woocommerce", "version": "8.9.1", "requiresWp": "6.4", "testedWp": "6.5", "requiresPhp": "7.4" } ] }, "security": { "loginPublic": true, "xmlrpcExposed": false, "userEnumExposed": true, "authorEnumExposed": false, "installExposed": false, "readmeExposed": true, "debugEnabled": false } },
    "sitemap": { "count": 732, "checksum": "…", "locations": [ "https://example.com/", "https://example.com/about" ] },
    "dns": { "resolves": true, "resolveError": null, "email": { "spf": true, "dmarc": true, "dkim": false, "mx": true, "dnssec": false }, "detail": { "nameserverProvider": "cloudflare", "mailProvider": "google", "nsCount": 2, "aRecords": [ { "address": "203.0.113.10", "family": 4, "isoCode": "US", "asn": 13335, "rdns": "host.example.com", "rdns_forward_ip": [ "203.0.113.10" ] } ], "mxRecords": [ { "exchange": "aspmx.l.google.com", "priority": 1, "aRecords": [ { "address": "142.250.1.26", "family": 4, "isoCode": "US", "asn": 15169, "rdns": "…1e100.net", "rdns_forward_ip": [ "142.250.1.26" ] } ] } ], "whois": { "registrar": "Cloudflare", "expires": "2026-09-01T…" } } }
  },
  "error": null,
  "screenshots": [ { "viewport": "desktop", "url": "https://fsn1.your-objectstorage.com/…signed…" }, { "viewport": "tablet", "url": "…" }, { "viewport": "phone", "url": "…" } ]
}

List recent scans

GET /v1/scans - your 50 most recent scans (summaries, no findings).

curl https://api.genchi.dk/v1/scans \
  -H "Authorization: Bearer gk_live_…"

Content vs. provider checks (v2)

/v1/scans/:id keeps returning the full merged record, unchanged, forever. v2 splits page content from hosting/DNS provider data into two independent resources, so you can refresh provider info far more often than a full content scan without re-scanning the page.

Get a scan's content (v2)

GET /v2/scans/:id - identical to GET /v1/scans/:id above, except findings.dns is omitted. Use /v2/providers below for hosting/DNS data instead.

curl https://api.genchi.dk/v2/scans/7f3c…-uuid \
  -H "Authorization: Bearer gk_live_…"

Provider checks (v2)

POST /v2/providers - runs DNS/hosting-provider detection directly, with no browser involved. Responds synchronously (no polling needed) and meters against its own provider-check quota/rate limit, separate from your scan quota - built for checking a domain's hosting/DNS/mail provider far more often than you'd run a full content scan. Responses carry the same RateLimit-*/X-Quota-* headers as scans below (no concurrency headers - there's no concurrency gate for provider checks).

curl -X POST https://api.genchi.dk/v2/providers \
  -H "Authorization: Bearer gk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com"}'
{
  "id": "9c1e…-uuid",
  "url": "https://example.com",
  "createdAt": "2026-07-06T09:15:55.331Z",
  "dns": {
    "resolves": true,
    "resolveError": null,
    "email": { "spf": true, "dmarc": true, "dkim": false, "mx": true, "dnssec": false },
    "detail": { "nameserverProvider": "cloudflare", "mailProvider": "google", "nsCount": 2, "whois": { "registrar": "Cloudflare", "expires": "2026-09-01T…" } }
  },
  "error": null,
  "usage": { "used": 1, "remaining": 149999, "limit": 150000 }
}

dns has the same shape as findings.dns (see Findings reference below). error is set (with dns: null) when the target isn't a valid registrable domain - this is still a 200, not a failure, and still counts against quota.

History: GET /v2/providers (optionally ?url= to filter to one domain) returns your 50 most recent checks, newest first; GET /v2/providers/:id fetches one by id - track when a customer's provider changes over time.

curl "https://api.genchi.dk/v2/providers?url=https://example.com" \
  -H "Authorization: Bearer gk_live_…"

Findings reference

  • resultKind - full on success, otherwise unreachable, redirect, timeout, blocked, etc.
  • http_status, finalUrl, redirectType - response status and where the URL resolved to.
  • metrics - dead_links, dead_images, dead_resources[] ({ url, status }), mixed_content, ttfb_ms, page_weight_bytes, asset/timing breakdowns.
  • cert - TLS certificate (subject, issuer, validity, errors).
  • securityHeaders - notable response security headers.
  • technologies - detected stack, with a version when found.
  • wordpress - present only for WordPress sites: { theme, plugins: { count, found? }, security? }. The theme slug, the found plugin-slug list, and security require the WordPress feature (Pro & Enterprise); other plans receive only plugins.count. Each theme and plugin also carries a version when one can be resolved from the live site (best-effort; omitted when it can't be determined), plus its WP compatibility range (requiresWp / testedWp) and required PHP (requiresPhp) when present in the component's readme. security reports exposure surfaces probed in-page as boolean flags: loginPublic, xmlrpcExposed, userEnumExposed, authorEnumExposed, installExposed, readmeExposed, and debugEnabled.
  • dns - DNS / email / registration posture. resolves and the email booleans (spf, dmarc, dkim, mx, dnssec) are always present. The detail object requires the Deep scan feature (Pro & Enterprise): A/AAAA records with GeoIP + ASN + rDNS and rdns_forward_ip (forward-confirmed rDNS), per-MX aRecords with the same enrichment, mailProvider, nameserver/hostmaster provider, SPF eval, BIMI, and WHOIS registrar/expiry. Collected for every reachable and unreachable target, so an NXDOMAIN still reports resolves: false with a resolveError code.
  • sitemap - count + checksum always; the locations URL list requires Deep scan.
  • screenshots (top-level, not in findings) - presigned, time-limited image URLs per viewport (desktop/tablet/phone). Only captured when the request sets screenshot: true, and only on plans with the Screenshots feature (Pro & Enterprise).

Plans & limits

PlanScans / moProvider checks / moRateConcurrencyFeatures
Free255001/s · burst 51-
Researcher1,00010,0002/s · burst 102-
Basic5,00050,0005/s · burst 203-
Pro15,000150,00025/s · burst 10010Deep scan, Priority queue, WordPress checks, Full-page screenshots

Rate limits & backpressure

Every scan response carries your current limits and remaining budget as headers, so you can pace submissions instead of discovering limits by hitting them. Three independent gates apply, in order: a per-second rate (token bucket), a concurrency cap (scans in flight at once), and a monthly quota.

HeaderMeaning
RateLimit-LimitToken-bucket burst capacity (requests).
RateLimit-RemainingWhole tokens left right now.
RateLimit-ResetSeconds until the bucket refills to full.
X-Concurrency-LimitMax scans your plan may have in flight at once.
X-Concurrency-RemainingFree concurrency slots right now.
X-Quota-LimitMonthly scan quota (omitted on unlimited plans).
X-Quota-RemainingScans left this month.
X-Quota-ResetSeconds until the monthly quota rolls over.
Retry-AfterOn a 429: seconds to wait before retrying.

When a gate rejects a submission it returns 429 with a code and a Retry-After header. Distinguish transient from terminal by the code, not the status: rate_limited and concurrency_limited clear in seconds - back off for Retry-After and retry. quota_exceeded does not clear until the month rolls over (Retry-After is the seconds until then), so stop rather than retry. A healthy client watches X-Concurrency-Remaining and keeps that many scans in flight.

Errors

Failures return { "error": "…", "code": "…" } with one of:

codeHTTPMeaning
no_key401 / 400Missing or malformed API key.
bad_key401API key not recognised.
subscription_inactive402Paid plan without an active subscription.
validation400Invalid request body (e.g. bad URL).
blocked_target403Target refused (private/SSRF-protected address).
account_suspended403The account is suspended.
rate_limited429Request rate exceeded your plan's limit.
concurrency_limited429Too many scans in flight for your plan.
quota_exceeded429Monthly scan quota exhausted.
not_found404No scan with that id for your account.