HTML Validation API
Validate a deployed URL from a script or a build. Every check runs off a single fetch of the page, and the answer carries one verdict your pipeline can act on.
What this is, and what it is not
It is not an API in front of html-validate. That library is free, it is the same engine this runs on, and if all you need is markup validation of files you already have on disk, install it and skip this page. Nothing here beats a local dependency at that job.
What this does instead is the orchestration. One deployed URL goes in; the page is fetched once and five checks read that same response, so accessibility, markup validity, meta tags, social cards and page weight cost one request rather than five. The findings come back already filtered through the false-positive work this validator exists for, and the verdict is a single field so a build can act on it without parsing prose.
Get a key
Keys are created in your account, under your sites. An account is free and needs no password. A key is shown once when it is created, because only a hash of it is stored, so it cannot be recovered afterwards by you or by us.
Every account gets 100 calls a month at no cost. That is enough to wire up a pipeline and see whether this belongs in yours.
A key can be given an expiry when it is created, or none at all, which is the default. An expiry cannot be changed afterwards, since a deadline that moves is not one, and we email you 7 days before a key stops working so it never fails a build without warning. Revoking a key takes effect on the very next call.
Make a call
One endpoint, POST /api/v1/validate, and the key travels as a bearer token.
curl -X POST https://validatehtml.com/api/v1/validate \
-H "Authorization: Bearer vh_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'{
"api_version": 1,
"url": "https://example.com/",
"checked_at": "2026-09-14T18:14:44.083Z",
"status": "passed",
"fail_on": "error",
"accessibility_score": 100,
"checks": ["accessibility", "html", "meta", "opengraph", "pageweight"],
"totals": {
"errors": 0,
"warnings": 13,
"info": 1,
"findings": 14,
"accessibility_issues": 0
},
"by_check": { "meta": 3, "opengraph": 10, "pageweight": 1 },
"findings": [
{
"check": "meta",
"rule": "meta/meta-description",
"severity": "warning",
"message": "meta description: No meta description found.",
"line": 0,
"column": 0
}
],
"findings_truncated": false,
"usage": { "calls": 1, "limit": 100 }
}That is a real answer for that URL, with one of its fourteen findings kept so the block fits on a page. It is also the clearest illustration of the one decision that matters here: thirteen warnings, no errors, and a verdict of passed. The single field a build reads is status, and everything under it is for the person who then goes looking.
What you can ask for
url
Required. An absolute http or https address of a page that is already deployed. Pasted markup is not accepted, deliberately: that request is answered better by a local library.
checks
Optional, and all five run by default. An unknown id is refused rather than quietly dropped, so a typo can never return a verdict over checks you did not ask for.
accessibilityWCAG failures a machine can see: missing names, labels, alt text, language.htmlSpec violations in the markup, at the same bar as the free validator.metaTitle, description, canonical, viewport, and whether a page is indexable.opengraphWhat a link to this page looks like when somebody shares it.pageweightHow heavy each page is, and whether the server is compressing it.
CSS validation and robots.txt are not on this list. Both are facts about a site rather than about a page: one stylesheet serves five hundred pages, so running them per call would report the same error five hundred times. They run in the site audit instead.
fail_on
Optional, and error by default. An error is a spec violation; a warning is a best practice that is still valid HTML. The default verdict counts errors only, because a build that goes red on opinion is a build somebody switches off by Friday. Raise the bar when you actually want it:
{
"url": "https://example.com",
"checks": ["html", "accessibility"],
"fail_on": "warning"
}When something goes wrong
Every failure carries a stable error.code so you can branch on it without matching on the message. The split between 4xx and 5xx is the useful one: a 4xx is something to fix in the request, a 5xx is worth retrying.
| Status | Code | What it means |
|---|---|---|
| 400 | invalid_url | The url field is missing, or is not an absolute http or https address. |
| 400 | invalid_request | An unknown check id, or a fail_on that is neither error nor warning. |
| 401 | missing_key | No Authorization header, or one that is not a Bearer token. |
| 401 | invalid_key | The key is unknown or has been revoked. The two answer identically. |
| 401 | key_expired | The key was valid and has reached the expiry you chose for it. Named separately, so a build that worked yesterday does not send you hunting. |
| 429 | quota_exceeded | This key has spent its calls for the month. The allowance resets on the 1st. |
| 502 | unreachable | The request was fine and the page did not answer. Worth retrying. |
| 503 | unavailable | The API is temporarily unavailable. Worth retrying. |
Limits, stated plainly
- One page per call. This validates the URL you send, not the site behind it. Reading every page a site publishes is what the site audit does.
- 100 calls a month. Counted per account, reset on the 1st. A refused call still counts, because the page has usually already been fetched by then.
- At most 200 findings in one response. Past that the list is cut and
findings_truncatedsays so, rather than the list quietly ending. - The score measures accessibility only. It is named
accessibility_scorefor that reason, so it can never be read as a grade for everything that ran.
The response is a contract
Renaming a field breaks a pipeline inside a company that will never read a changelog, so within version 1 fields may be added and will never be renamed or removed. Anything that has to break goes to a new version under a new path, and the version travels in the body as well as in the URL so a stored response can always say what produced it.