Measure Core Web Vitals
Field data from real visitors tells you there's a performance problem weeks after it started hurting rankings and conversions; a lab test tells you before you ship. This endpoint loads a URL in a controlled environment and measures Largest Contentful Paint, Cumulative Layout Shift and Interaction to Next Paint the same way a real visit would experience them, on demand, without waiting for enough real traffic to accumulate a signal.
Run it online
Run this on our servers with your account. Free tools run in your browser; this one bills your KIT balance per the price above.
Why a lab test earns its own place next to field data
Real-user performance data is the ground truth, but it's necessarily historical — it tells you how a page performed for the visitors it already had, which means a regression only shows up in the data after it's already affected them. A lab test flips that order: it measures a page under controlled, repeatable conditions before it reaches real visitors at all, which is exactly the moment a performance regression is cheapest to catch, right before a deploy rather than weeks into it.
The three metrics and what each one actually measures
POST /web/vitals with a URL, get a task_id back immediately, and receive the scores by signed webhook or a signed link once the async run finishes. Largest Contentful Paint measures how long the biggest visible element takes to render, standing in for how fast a page feels like it's loaded; Cumulative Layout Shift measures how much content jumps around unexpectedly as a page settles, standing in for visual stability; Interaction to Next Paint measures how responsive a page feels when someone actually clicks or taps something, standing in for interactivity. Together they cover loading, stability and responsiveness — three different ways a page can feel slow even when the others are fine.
From an internal metric to a ranking signal
These three metrics became a formal part of how search engines evaluate page experience because they were chosen to reflect what users actually notice and complain about, rather than technical numbers that correlate poorly with real frustration — a page that scores well on all three tends to feel fast and stable to an actual visitor, not just fast on a stopwatch. That's part of why they're worth testing before launch rather than only after: a regression that would eventually surface as a ranking or conversion problem is visible in a lab score long before enough field data exists to confirm it.
Running it as part of a release process
Teams that treat performance as a release gate run this on a staging URL before every deploy, and again on production right after, to catch regressions introduced by a new image, an added script or a font change before real visitors ever experience the slowdown. Priced per request plus per URL, testing a batch of key pages before each release costs in direct proportion to how many pages you check, which makes it practical to gate an entire release on vitals rather than spot-checking one homepage and hoping the rest of the site held up.
What you can do with it
Pre-deploy performance gate
A team runs vitals on a staging URL before every deploy and blocks the release if LCP or CLS regresses past an agreed threshold.
Post-launch verification
A team re-checks production vitals right after a release to confirm the deploy didn't introduce a layout shift or responsiveness regression that testing missed.
Third-party script impact testing
A team measures vitals before and after adding a new analytics or ad script to quantify its exact impact on loading and interactivity.
Competitive benchmarking
A marketing team tests vitals on competitor pages alongside their own to see where a redesign might close a real performance gap.
FAQ
How do I test Core Web Vitals with the API?
POST the page URL to /web/vitals, keep the returned task_id, and collect the LCP, CLS and INP scores by webhook or a signed link valid for 24 hours.
Is the Core Web Vitals API free?
No, there is no free tier or trial; it costs $0.040 per request plus $0.001 per URL, and a failed task is never charged.
Is this lab data or real-user field data?
It's a lab test run in a controlled environment, which measures a page under consistent conditions rather than aggregating real visitor sessions.
Why does a lab score sometimes differ from field data?
Lab tests run under fixed conditions while field data reflects the real mix of devices, connections and locations actual visitors use, so the two are complementary rather than interchangeable.
What does INP measure compared to the older FID metric?
INP measures the responsiveness of interactions throughout an entire page visit rather than just the first one, giving a fuller picture of how a page feels to use.
Can I test multiple pages before a release?
Yes, it's priced per request plus per URL, so testing a full set of key pages before shipping scales in direct proportion to the pages checked.
Is this endpoint live yet?
Yes, it's live and accepting requests now.
Is the tested page data kept afterward?
No, page content and results are deleted after the retention window and are never used for training.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, and soon from our app, email and Telegram.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/web/vitals \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://ejemplo.com"}'const res = await fetch("https://api.kit.forhosting.com/web/vitals", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"url": "https://ejemplo.com"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/web/vitals",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"url": "https://ejemplo.com"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/web/vitals", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"url":"https://ejemplo.com"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"url":"https://ejemplo.com"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/web/vitals", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"url": "https://ejemplo.com"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "web.vitals",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
timeout_sec | 30 |
max_crawl_pages | 25 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |
422 | task_failed | The task failed after 3 retries. You are never charged for it. |