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 (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 /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": "…" } ]
}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.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). | Plan | Scans / mo | Provider checks / mo | Rate | Concurrency | Features |
|---|---|---|---|---|---|
| Free | 25 | 500 | 1/s · burst 5 | 1 | - |
| Researcher | 1,000 | 10,000 | 2/s · burst 10 | 2 | - |
| Basic | 5,000 | 50,000 | 5/s · burst 20 | 3 | - |
| Pro | 15,000 | 150,000 | 25/s · burst 100 | 10 | Deep scan, Priority queue, WordPress checks, Full-page screenshots |
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. |