Skip to content
metryn

Docs

The free scan endpoints

Metryn has no public API product. What exists are the endpoints the website itself uses. They are described here as they behave today, so you know what you would be building on. They can change without notice and there is no service level agreement.

Limits

  • 10 scans per hour per IP address, and 3 scans per hour per target domain across all visitors.
  • One scan running and two queued per IP at a time.
  • The scan endpoint requires a Cloudflare Turnstile token. That token is produced by the widget in a browser, so a plain script or server cannot call it. This is deliberate, not an oversight.
  • Cross-origin browser calls are only allowed from an exact-origin list kept by hand: Metryn's own Shopify companion store and the sites that asked to be listed for the embed snippet. Never from arbitrary sites.

Start a scan

Send the URL and the Turnstile token as JSON; other content types get 415. The scan is queued and runs in the background, so the response carries an id, not a result.

utmSource is optional and only labels where the scan came from, for counting. It is a short plain token (letters, digits, dot, dash, underscore, up to 40 characters); anything else is ignored. On a call from another website's origin, which is what the embed snippet makes, only the platform labels the snippet itself sends are kept: wordpress, magento, wix, squarespace, webflow, shopify and embed. Anything else from such an origin is dropped and the scan runs unlabelled.

utmMedium and utmCampaign are optional too, with the same rules (campaign up to 60 characters). From another website's origin they are kept only together with an accepted source, and the medium only if it is widget or embed. A request without them works exactly as before.

POST /api/scan
Content-Type: application/json

{ "url": "https://example.com", "turnstileToken": "<token from the Turnstile widget>", "utmSource": "shopify", "utmMedium": "widget", "utmCampaign": "embed" }
HTTP/1.1 202 Accepted

{ "id": "b1f0c0de-0000-4000-8000-000000000000", "status": "queued" }

If the same URL was scanned in the last 15 minutes you get 201 with { "id": "...", "status": "done", "reused": true }. If that URL is already queued or running you get 200 with the existing id. Other answers: 400 for an invalid or non-public address (or a url that is not a string), 415 when the content type is not JSON, 403 when the security check fails, 429 with Retry-After when a limit above is reached, 503 with Retry-After when the queue is full.

Check status

Poll until status is done or failed. The score is null until then. While the scan runs, progress lists the stages reached so far (s is the stage, t its start in epoch milliseconds) and startedAt is when the worker picked it up. The report token is never returned here.

When status is failed, error is a message you can show, errorCode says why, and outcome classifies it: "refused" when the target said no or showed nothing to read (errorCode is ROBOTS_DISALLOWED, BOT_CHALLENGE, PASSWORD_PROTECTED or TARGET_REFUSED; retrying does not help), "failed" for anything else (for example TIMEOUT, when the site was too slow to answer; those scans are retried once before they fail). outcome is null while the scan has not failed. The preview returns the same errorCode and outcome fields.

GET /api/scan/{id}

{
  "id": "b1f0c0de-0000-4000-8000-000000000000",
  "url": "https://example.com/",
  "domain": "example.com",
  "status": "done",
  "score": 72,
  "paid": false,
  "createdAt": "2026-10-14T09:30:00.000Z",
  "error": null,
  "errorCode": null,
  "outcome": null,
  "startedAt": "2026-10-14T09:30:01.000Z",
  "progress": [
    { "s": "robots", "t": 1760434201100 },
    { "s": "page", "t": 1760434201650 }
  ]
}

Free preview

The same free result the scan page shows: score, counts and the top three findings with title, severity, confidence and explanation. Evidence, fix steps and the remaining findings are part of the paid report and are not returned.

issueCount and counts count issues only: findings that cost score points. Findings that cost nothing (what already works, optional notes) still carry the severity recommendation and appear in totalFindings and the preview list, but not in counts.recommendation. informationalCount is how many there are (totalFindings - issueCount), so a site scoring 100 can show recommendation findings with counts.recommendation at 0.

When the scanned address looks like a product page, the report also reads its Product structured data. Those notes cost nothing and are part of informationalCount, but they are never in the preview list. productDataCount says how many there are. The field is present only when the number is above zero.

GET /api/scan/{id}/preview

{
  "id": "b1f0c0de-0000-4000-8000-000000000000",
  "domain": "example.com",
  "status": "done",
  "error": null,
  "errorCode": null,
  "outcome": null,
  "scanUrl": "https://getmetryn.com/scan/b1f0c0de-0000-4000-8000-000000000000",
  "score": 72,
  "label": "Needs attention",
  "totalFindings": 9,
  "hiddenFindings": 6,
  "issueCount": 5,
  "informationalCount": 4,
  "counts": { "critical": 0, "important": 2, "recommendation": 3 },
  "findings": [
    { "severity": "important", "confidence": "...", "title": "...", "explanation": "..." }
  ]
}

Free tools

POST /api/tools/robots and POST /api/tools/llms back the free tools. Send pasted text with no network call, or a URL plus a Turnstile token. Up to 64 KB per request, 20 network checks per IP per hour per tool and 10 per target host per hour. Nothing you submit is stored.

POST /api/tools/robots
Content-Type: application/json

{ "text": "User-agent: GPTBot\nDisallow: /" }

Need programmatic access?

There are no accounts and no keys yet. If you would use an API with a key and higher limits, tell us what you would build. That is how we will decide whether to make one.

Request an API key (support@getmetryn.com)

See also the methodology for what each check measures and what it does not.