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.
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
POST /v1/scans - queues a scan and meters one invocation against your quota. Returns immediately with an id.
| Field | Type | Default | Notes |
|---|---|---|---|
| url | string (URL) | required | The page to scan. Must be a public http(s) URL. |
| waitMs | integer 0-30000 | 0 | Extra settle time after load before collecting findings. |
| viewport | { width, height } | 1280 × 800 | Render viewport in px (each 1-4000). |
| screenshot | boolean | false | Capture full-page screenshots (desktop/tablet/phone). Requires the Screenshots feature on your plan. |
| har | boolean | false | Capture a network trace (HAR) of the page load. Requires the Network trace feature on your plan. |
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 /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; see your dashboard for your plan). 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": "…" } ]
}GET /v1/scans - your 50 most recent scans (summaries, no findings).
curl https://api.genchi.dk/v1/scans \
-H "Authorization: Bearer gk_live_…"/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 /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_…"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_…"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. Load timings, page weight, css_used and uncached_assets are null without the Performance feature.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; 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 DNS and hosting detail feature: 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 DNS and hosting detail. 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. har (top-level, not in findings) - { url }, a presigned download link (valid 10 minutes) for the scan's network trace as a HAR file. Fetch the scan again for a fresh link. Only captured when the request sets har: true, and only on plans with the Network trace feature. Your plan sets your monthly scan and provider-check quota, rate limit, concurrency and which data a scan returns. Your dashboard shows your plan and usage, and every response carries your current limits in the headers described below.
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.
| Header | Meaning |
|---|---|
| RateLimit-Limit | Token-bucket burst capacity (requests). |
| RateLimit-Remaining | Whole tokens left right now. |
| RateLimit-Reset | Seconds until the bucket refills to full. |
| X-Concurrency-Limit | Max scans your plan may have in flight at once. |
| X-Concurrency-Remaining | Free concurrency slots right now. |
| X-Quota-Limit | Monthly scan quota (omitted on unlimited plans). |
| X-Quota-Remaining | Scans left this month. |
| X-Quota-Reset | Seconds until the monthly quota rolls over. |
| Retry-After | On 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.
Failures return { "error": "…", "code": "…" } with one of:
| code | HTTP | Meaning |
|---|---|---|
| no_key | 401 / 400 | Missing or malformed API key. |
| bad_key | 401 | API key not recognised. |
| subscription_inactive | 402 | Paid plan without an active subscription. |
| validation | 400 | Invalid request body (e.g. bad URL). |
| blocked_target | 403 | Target refused (private/SSRF-protected address). |
| account_suspended | 403 | The account is suspended. |
| rate_limited | 429 | Request rate exceeded your plan's limit. |
| concurrency_limited | 429 | Too many scans in flight for your plan. |
| quota_exceeded | 429 | Monthly scan quota exhausted. |
| not_found | 404 | No scan with that id for your account. |