FTP from 20-minute test calculator
Estimates functional threshold power from a 20-minute all-out effort: a 280 W average becomes an FTP of 266 W with the classic 95% factor. Use it to update power zones, workout targets and race pacing after a test. Free in your browser.
- Free
- Runs in your browser
- No sign-up
- Your data stays on your device
Run — free
Worked example
- Avg power 20min
- 280
- Ftp watts
- 266 W
- Factor
- 0.95
- Precision
- 2
Functional threshold power, often abbreviated FTP, is the highest power a cyclist or indoor rider can sustain for roughly one hour and is the anchor for power-based training zones. The classic field protocol is a maximal twenty-minute effort: ride as hard as you can for twenty minutes, take the average power from that bout, and multiply by ninety-five percent to estimate the hour-long threshold. This FTP from 20-minute test calculator applies that exact factor offline and deterministically so coaches, apps, and training notebooks get the same watts every time. Enter your twenty-minute average in watts; the tool returns estimated FTP, the factor used, and a short formula string you can show next to the answer.
What FTP means and why the 20-minute test uses 95%
Functional threshold power is the highest mean power an athlete can hold for about sixty minutes under controlled conditions. Direct one-hour tests are exhausting and hard to schedule, so coaches and platforms often use a shorter maximal effort and scale it. The widely taught twenty-minute protocol asks the rider for an all-out twenty-minute average, then multiplies that average by zero point nine five.
The five-percent reduction accounts for the fact that a rider can usually hold slightly more power for twenty minutes than for a full hour, so the scaled value approximates the hour-long threshold used for zone charts, workout targets, and race pacing. This calculator implements that single multiplication and nothing else: no fatigue models, no age adjustments, and no device-specific corrections.
If your head unit or training app already shows a twenty-minute average from a structured test or a hard race segment of that length, feed that number here to obtain FTP in watts. Remember that the quality of the estimate depends on a true maximal effort, adequate warm-up, and a calibrated power meter; the math itself is intentionally simple so every client can reproduce it offline without network calls or random variation.
What you can do with it
Post-test FTP update
After a structured twenty-minute all-out effort, convert the recorded average watts into estimated FTP for training zones and workout targets.
Coaching CRM and LMS plugins
Accept a client’s 20-minute average from a form or CSV and store a deterministic FTP without third-party sports APIs.
FAQ
Why multiply by 95% instead of using the raw average?
A true hour-long threshold is usually a bit lower than a maximal twenty-minute average. The classic protocol scales the 20-minute mean by 0.95 to approximate FTP.
What inputs are rejected?
Optional precision must be an integer from 0 to 15.
How do I run a 20-minute FTP test?
Warm up properly, then ride as hard as you can for twenty minutes and note the average power of that effort. The estimate is only as good as the test: it needs a true maximal effort and a calibrated power meter.
Can I use a hard 20-minute race segment instead of a test?
Yes. If your head unit or training app shows a 20-minute average from a structured test or from a hard race segment of that length, enter that number. Just make sure the effort really was all-out.
Does the calculator give me training zones?
No. It estimates the threshold only, and FTP is the anchor that power-based zones are built on. Take the result to your own zone table to set your training bands.
Related tools
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, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/health/ftp-from-20min \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"avg_power_20min":280}'const res = await fetch("https://api.kit.forhosting.com/health/ftp-from-20min", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"avg_power_20min": 280
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/health/ftp-from-20min",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"avg_power_20min": 280
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/health/ftp-from-20min", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"avg_power_20min":280}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"avg_power_20min":280}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/health/ftp-from-20min", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"avg_power_20min": 280
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "health.ftp_from_20min",
"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.
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. |
Technical notes
Invalid or non-positive inputs fail with invalid_input instead of inventing a threshold.
Input contract, response shape, and error behaviour
An optional precision integer from zero to fifteen (default two) sets decimal places for the rounded power fields in the response. Example: two hundred eighty watts average over twenty minutes yields two hundred sixty-six watts of estimated FTP at default precision. POST a JSON body with one required finite positive number: avg_power_20min, the maximal average power sustained over twenty minutes, in watts. Aliases accepted for convenience include power_20min, avg_power, power, p20, and watts. Each numeric field may arrive as a JSON number or as a numeric string; non-finite values, missing fields, empty strings, zero, and negative power fail with invalid_input naming the field, and those failures are not billed. On success the body includes avg_power_20min echoed and rounded, ftp_watts equal to zero point nine five times that average, factor zero point nine five, unit W, method 20min_95_percent, the precision used, and a formula string restating the protocol. Pair the response with your own zone table if you need training bands from FTP; this endpoint estimates the threshold only and does not emit heart-rate zones, maps, or coaching prose beyond the formula field.
Where an FTP-from-20-minute calculator earns its keep
Race planners convert a hard twenty-minute climb average into a threshold estimate for pacing models. Because invalid or non-positive inputs are rejected before any arithmetic, bad sensor dumps never become fake thresholds that corrupt zone charts downstream. The capability does not replace a coach judgment call when the rider sandbagged the test or the power meter drifted; it only encodes the standard ninety-five percent rule so every system agrees on the number once the average is trusted. Indoor training platforms and coaching CRMs need a deterministic oracle that turns a twenty-minute test result into FTP without calling a third-party sports API. Keep requests small, pin the rounded ftp_watts in tests, and you get reproducible FTP estimates for LMS plugins, edge workers, and mobile offline mode without standing up a full training-load stack of your own.
Integration use cases
Offline and edge training apps
Run the same 0.95 multiply on-device or in an edge worker so browser and API paths never disagree on the threshold.
Technical questions
How do I estimate FTP from a 20-minute test?
Send a positive finite avg_power_20min in watts (your maximal twenty-minute average). The API returns FTP = 0.95 × that average as ftp_watts.
What units does the calculator use?
Watts only. Enter the twenty-minute average in watts; the response reports avg_power_20min and ftp_watts with unit W.
What does the FTP from 20-minute test calculator cost?
The browser tool is free. API requests use prepaid KIT balance at $0.002 per successful call; invalid input is not billed.