Heart rate zones
At 30 years old with a resting pulse of 60, your endurance zone runs from 138 to 151 beats per minute. You get all five Karvonen zones, from recovery to max effort, built from your own heart rate reserve. Free in your browser.
- Free
- Runs in your browser
- No sign-up
- Your data stays on your device
Run — free
Worked example
- Age
- 30
- Resting heart rate
- 60
- Maximum heart rate
- 190
- Heart rate reserve
- 130
Calculates five training zones from your age and resting heart rate using the Karvonen heart rate reserve method, returning the lower and upper beats-per-minute bounds for recovery, endurance, tempo, threshold, and max effort.
How the heart rate reserve shapes your zones
Your heart rate reserve is your maximum heart rate minus your resting heart rate. If you do not enter a measured maximum, it is estimated as 220 minus your age, so at 30 years old the maximum is 190 and, with a resting pulse of 60, the reserve is 130 beats. Each zone bound is the resting pulse plus a share of that reserve, rounded to the nearest whole beat. That is why two people of the same age but different resting rates get different zones.
The five zones and how to use them
Recovery covers 50 to 60 percent of the reserve, endurance 60 to 70, tempo 70 to 80, threshold 80 to 90 and max effort 90 to 100. For the example above that gives 125 to 138, 138 to 151, 151 to 164, 164 to 177 and 177 to 190 beats per minute. Use the bounds as targets for interval and endurance sessions, or set them on a watch so it flags when you leave the band. A maximum measured in a lab or field test makes every zone more personal than the age estimate.
What you can do with it
Training plans
Generate personalised bpm targets for interval and endurance workouts from two inputs.
Wearable apps
Set per-user zone boundaries so a watch can flag when a session leaves the target band.
FAQ
How many zones are returned?
Five: recovery, endurance, tempo, threshold, and max, each with a lo/hi bpm pair.
What is the Karvonen method?
It builds training zones from your heart rate reserve, the gap between your maximum and resting heart rate, instead of a flat share of the maximum. Each zone bound is your resting rate plus a percentage of that reserve.
Can I use a maximum heart rate I measured myself?
Yes. By default the maximum is estimated as 220 minus your age, but you can enter a value measured in a lab or a field test, and every zone is recalculated from it.
Why do two neighbouring zones share the same beat?
By design, the top of one zone is the bottom of the next, so there are no gaps between them. With the example values, endurance ends at 151 and tempo starts at 151 beats per minute.
Why does my resting heart rate change the zones?
Because each band sits on top of your resting pulse. Two people with the same maximum but different resting rates get different targets, which a simple percentage-of-maximum model misses entirely.
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/dev/heart-rate-zones \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"age":30,"resting_hr":60}'const res = await fetch("https://api.kit.forhosting.com/dev/heart-rate-zones", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"age": 30,
"resting_hr": 60
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/dev/heart-rate-zones",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"age": 30,
"resting_hr": 60
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/dev/heart-rate-zones", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"age":30,"resting_hr":60}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"age":30,"resting_hr":60}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/dev/heart-rate-zones", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"age": 30,
"resting_hr": 60
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "dev.heart_rate_zones",
"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
The Karvonen heart rate reserve formula
The calculator uses the Karvonen method, which builds zones from heart rate reserve (HRR) rather than a flat percentage of maximum heart rate. It takes age and resting_hr as required integers; max_hr is optional and defaults to 220 minus age. HRR is max_hr minus resting_hr, and each zone bound is computed as resting_hr + intensity × HRR, then rounded to the nearest whole beat. Five zones are returned: z1_recovery (50-60% of HRR), z2_endurance (60-70%), z3_tempo (70-80%), z4_threshold (80-90%), and z5_max (90-100%), each as a lo/hi pair in beats per minute. The response also echoes age, resting_hr, max_hr, and hrr. For age 30 and resting_hr 60, max_hr is 190, HRR is 130, and z2_endurance spans 138 to 151 bpm.
Why zones are personalised, and how they round
Each bound is rounded to zero decimals with an explicit half-up rule — sign * Math.floor(abs(x) + 0.5) — so a computed 137.5 becomes 138 rather than banking down, and adjacent zones share their boundary beat by design (z1's hi equals z2's lo). Because the Karvonen formula adds each intensity band on top of your resting pulse, the zones are personalised: two athletes with the same max_hr but different resting rates get different targets, which a plain percentage-of-max model misses entirely. The optional max_hr lets you override the age estimate with a measured lab or field value, and the method parameter documents the algorithm; it defaults to 'karvonen'.
Validation, ranges and typed errors
A non-object top-level input is refused as well. Inputs are range-checked before any zone is built. age must be an integer from 10 to 100 and resting_hr an integer from 30 to 120; anything outside those bounds, or non-integer, throws a typed invalid_input error such as 'age inválido' or 'resting_hr inválido'. When max_hr is supplied it must be an integer strictly greater than resting_hr, otherwise 'max_hr inválido' is raised — this prevents a negative or zero HRR that would invert the zones.
Integration use cases
Coaching tools
Batch athletes through the API to build zone charts with measured max heart rates.
Technical questions
What method does it use?
The Karvonen formula: resting_hr + intensity × (max_hr - resting_hr).
Do I need to know my max heart rate?
No. max_hr defaults to 220 minus age, but you can override it with a measured value.
Why does the zone bound depend on resting_hr?
Karvonen adds each band onto your resting pulse, so zones are personalised, not a flat percentage of max.
Is it free?
Yes in the browser widget; the API runs the same core from $0.002 per call.