Skip to main content

ISO 8601 Duration Parser & Calculator API

REAL DATA

Parse ISO 8601 durations, add them with documented end-of-month clamping, break down the gap between two dates, and humanize a span in any locale.

Authoritative reference data or standards computation

Base URL
/api/durations
Category
Date & Time
Capabilities
4 routes
Last updated
July 29, 2026
Data source

GET /api/durations/parse

Validates the P-form grammar and returns every component plus which units are nominal, calendar or exact. Whole-duration second totals are reported only as a labelled approximation.

Live requestRuns against the public API
No key required
GET/api/durations/parse?value=P1Y2M3DT4H5M6S

Request parameters

ISO 8601 duration, at most 64 characters: P[nY][nM][nW][nD][T[nH][nM][nS]], optionally signed. Uppercase designators; a decimal fraction only in the smallest component present.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/durations/parse?value=P1Y2M3DT4H5M6S"
const res = await fetch("https://randomapi.dev/api/durations/parse?value=P1Y2M3DT4H5M6S");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/durations/parse?value=P1Y2M3DT4H5M6S").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/durations/parse?value=P1Y2M3DT4H5M6S"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 1
value string required

ISO 8601 duration, at most 64 characters: P[nY][nM][nW][nD][T[nH][nM][nS]], optionally signed. Uppercase designators; a decimal fraction only in the smallest component present.

allowed: ≤ 64
example: value=P1Y2M3DT4H5M6S
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=value,normalized
exclude list

Return all fields except these (comma-separated).

example: exclude=approximate
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 15
value string

The validated input duration, unchanged.

example: P1Y2M3DT4H5M6S

normalized string

Canonical re-rendering with zero components dropped and a full stop as decimal sign; an all-zero duration becomes PT0S.

example: P1Y2M3DT4H5M6S

negative boolean

True when the duration carries a leading '-'. A duration whose components are all zero is never reported as negative.

example: false

years float

Unsigned magnitude of the years component; 0 when absent. Nominal unit.

example: 1

months float

Unsigned magnitude of the months component; 0 when absent. Nominal unit.

example: 2

weeks float

Unsigned magnitude of the weeks component; 0 when absent. Never combined with other components.

example: 0

days float

Unsigned magnitude of the days component; 0 when absent. Calendar unit.

example: 3

hours float

Unsigned magnitude of the hours component; 0 when absent. Exact unit.

example: 4

minutes float

Unsigned magnitude of the minutes component; 0 when absent. Exact unit.

example: 5

seconds float

Unsigned magnitude of the seconds component; 0 when absent. Exact unit.

example: 6

classification object

Which of the PRESENT units are nominal (years, months — no fixed length), calendar (weeks, days — exactly 24h in UTC, wall-clock days in a zone) and exact (hours, minutes, seconds).

example: {"nominal":["years","months"],"calendar":["days"],"exact":["hours","minutes","seconds"]}

nominalMonths float

12 × years + months. Exact as a count of months; it cannot be converted to a fixed number of days.

example: 14

calendarDays float

7 × weeks + days, as calendar days.

example: 3

exactSeconds float

Seconds contributed by the hours, minutes and seconds components only — exact under every interpretation of the calendar units, and summed on the decimal text rather than as a floating-point product, so PT1.1H is 3960 and not 3960.0000000000005.

example: 14706

approximate object

Clearly-labelled approximation of the whole duration in seconds and days, plus the assumption used. Never treat it as an exact conversion of years or months.

example: {"seconds":37090350,"days":429.2864583333333,"assumption":"Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s."}

Documented examples

Build-generated requests and complete responses
4
All six components
GET /api/durations/parse?value=P1Y2M3DT4H5M6S
{
  "data": {
    "value": "P1Y2M3DT4H5M6S",
    "normalized": "P1Y2M3DT4H5M6S",
    "negative": false,
    "years": 1,
    "months": 2,
    "weeks": 0,
    "days": 3,
    "hours": 4,
    "minutes": 5,
    "seconds": 6,
    "classification": {
      "nominal": [
        "years",
        "months"
      ],
      "calendar": [
        "days"
      ],
      "exact": [
        "hours",
        "minutes",
        "seconds"
      ]
    },
    "nominalMonths": 14,
    "calendarDays": 3,
    "exactSeconds": 14706,
    "approximate": {
      "seconds": 37090350,
      "days": 429.2864583333333,
      "assumption": "Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s. Years and months have no fixed length, so this is an assumption — use /add or /between for calendar-exact answers."
    }
  },
  "meta": {
    "endpoint": "durations",
    "route": "/parse",
    "params": {
      "value": "P1Y2M3DT4H5M6S"
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
A YouTube-style video length
GET /api/durations/parse?value=PT1H30M
{
  "data": {
    "value": "PT1H30M",
    "normalized": "PT1H30M",
    "negative": false,
    "years": 0,
    "months": 0,
    "weeks": 0,
    "days": 0,
    "hours": 1,
    "minutes": 30,
    "seconds": 0,
    "classification": {
      "nominal": [],
      "calendar": [],
      "exact": [
        "hours",
        "minutes"
      ]
    },
    "nominalMonths": 0,
    "calendarDays": 0,
    "exactSeconds": 5400,
    "approximate": {
      "seconds": 5400,
      "days": 0.0625,
      "assumption": "Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s. Years and months have no fixed length, so this is an assumption — use /add or /between for calendar-exact answers."
    }
  },
  "meta": {
    "endpoint": "durations",
    "route": "/parse",
    "params": {
      "value": "PT1H30M"
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
The exclusive week form
GET /api/durations/parse?value=P2W
{
  "data": {
    "value": "P2W",
    "normalized": "P2W",
    "negative": false,
    "years": 0,
    "months": 0,
    "weeks": 2,
    "days": 0,
    "hours": 0,
    "minutes": 0,
    "seconds": 0,
    "classification": {
      "nominal": [],
      "calendar": [
        "weeks"
      ],
      "exact": []
    },
    "nominalMonths": 0,
    "calendarDays": 14,
    "exactSeconds": 0,
    "approximate": {
      "seconds": 1209600,
      "days": 14,
      "assumption": "Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s. Years and months have no fixed length, so this is an assumption — use /add or /between for calendar-exact answers."
    }
  },
  "meta": {
    "endpoint": "durations",
    "route": "/parse",
    "params": {
      "value": "P2W"
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Negative with a fractional second
GET /api/durations/parse?value=-PT1.5S
{
  "data": {
    "value": "-PT1.5S",
    "normalized": "-PT1.5S",
    "negative": true,
    "years": 0,
    "months": 0,
    "weeks": 0,
    "days": 0,
    "hours": 0,
    "minutes": 0,
    "seconds": 1.5,
    "classification": {
      "nominal": [],
      "calendar": [],
      "exact": [
        "seconds"
      ]
    },
    "nominalMonths": 0,
    "calendarDays": 0,
    "exactSeconds": 1.5,
    "approximate": {
      "seconds": 1.5,
      "days": 0.00001736111111111111,
      "assumption": "Mean Gregorian conversion: 1 year = 31556952 s (365.2425 days), 1 month = 2629746 s, 1 week = 604800 s, 1 day = 86400 s. Years and months have no fixed length, so this is an assumption — use /add or /between for calendar-exact answers."
    }
  },
  "meta": {
    "endpoint": "durations",
    "route": "/parse",
    "params": {
      "value": "-PT1.5S"
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}

GET /api/durations/between

Returns the years/months/days/hours/minutes/seconds breakdown in the same order /add applies, plus exact elapsed totals. Omitting 'to' uses the request time and echoes the resolved instant.

Live requestRuns against the public API
No key required
GET/api/durations/between?from=2026-01-31T00%3A00%3A00Z&to=2026-03-01T00%3A00%3A00Z&tz=Europe%2FCopenhagen

Request parameters

Start instant: YYYY-MM-DD (UTC midnight) or a timestamp with Z / an explicit offset.

End instant, same formats. Defaults to the request time, in which case the resolved instant is echoed in meta.params. May be earlier than 'from'.

IANA timezone used for the calendar (nominal) part of the arithmetic, case-insensitive. UTC means days are exactly 86400 seconds; a real zone makes days wall-clock days that can be 23 or 25 hours long.

Report at most this many non-zero units, counting from the largest one present; a zero component between two reported units costs nothing. Truncation is toward zero and never rounds — whatever is dropped is reported separately.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/durations/between?from=2026-01-31T00%3A00%3A00Z&to=2026-03-01T00%3A00%3A00Z&tz=Europe%2FCopenhagen"
const res = await fetch("https://randomapi.dev/api/durations/between?from=2026-01-31T00%3A00%3A00Z&to=2026-03-01T00%3A00%3A00Z&tz=Europe%2FCopenhagen");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/durations/between?from=2026-01-31T00%3A00%3A00Z&to=2026-03-01T00%3A00%3A00Z&tz=Europe%2FCopenhagen").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/durations/between?from=2026-01-31T00%3A00%3A00Z&to=2026-03-01T00%3A00%3A00Z&tz=Europe%2FCopenhagen"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 4
from date required

Start instant: YYYY-MM-DD (UTC midnight) or a timestamp with Z / an explicit offset.

example: from=2026-01-31T00:00:00Z
to date

End instant, same formats. Defaults to the request time, in which case the resolved instant is echoed in meta.params. May be earlier than 'from'.

example: to=2026-03-01T00:00:00Z
tz string

IANA timezone used for the calendar (nominal) part of the arithmetic, case-insensitive. UTC means days are exactly 86400 seconds; a real zone makes days wall-clock days that can be 23 or 25 hours long.

default: UTC
allowed: 1 – 64
example: tz=Europe/Copenhagen
maxUnits int

Report at most this many non-zero units, counting from the largest one present; a zero component between two reported units costs nothing. Truncation is toward zero and never rounds — whatever is dropped is reported separately.

default: 6
allowed: 1 – 6
example: maxUnits=3
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=from,to
exclude list

Return all fields except these (comma-separated).

example: exclude=totalDays
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 15
from string (ISO 8601 UTC)

Resolved start instant.

example: 2026-01-31T00:00:00.000Z

to string (ISO 8601 UTC)

Resolved end instant; the request time when 'to' was omitted.

example: 2026-03-01T00:00:00.000Z

timeZone string

Canonical IANA zone the calendar breakdown was computed in.

example: Europe/Copenhagen

negative boolean

True when 'to' is earlier than 'from'; the breakdown is then how far back you must go from 'from'.

example: false

years integer

Whole calendar years from 'from', applied first.

example: 0

months integer

Whole calendar months after the years step, with end-of-month clamping.

example: 1

days integer

Whole wall-clock days after the years and months steps.

example: 1

hours integer

Whole hours left over. Can reach 24 inside a 25-hour fall-back day.

example: 0

minutes integer

Whole minutes left over.

example: 0

seconds float

Seconds left over, carrying the millisecond fraction of the inputs — the nearest JSON number to that exact decimal, so it never disagrees with 'iso' or 'totalSeconds'.

example: 0

iso string

The breakdown as an ISO 8601 duration, signed. Feeding it to /add with the same 'from' and 'tz' reproduces 'to' when truncated is false.

example: P1M1D

truncated boolean

True when maxUnits dropped a non-zero unit.

example: false

remainderSeconds float

Exact seconds still between /add(from, iso) and 'to' — always 0 unless maxUnits truncated the breakdown.

example: 0

totalSeconds float

Exact elapsed SI seconds between the two instants, signed. Unaffected by the zone.

example: 2505600

totalDays float

totalSeconds ÷ 86400 as an IEEE-754 double — elapsed days, not calendar days.

example: 29

Documented examples

Build-generated requests and complete responses
4
Copenhagen calendar breakdown
GET /api/durations/between?from=2026-01-31T00%3A00%3A00Z&to=2026-03-01T00%3A00%3A00Z&tz=Europe%2FCopenhagen
{
  "data": {
    "from": "2026-01-31T00:00:00.000Z",
    "to": "2026-03-01T00:00:00.000Z",
    "timeZone": "Europe/Copenhagen",
    "negative": false,
    "years": 0,
    "months": 1,
    "days": 1,
    "hours": 0,
    "minutes": 0,
    "seconds": 0,
    "iso": "P1M1D",
    "truncated": false,
    "remainderSeconds": 0,
    "totalSeconds": 2505600,
    "totalDays": 29
  },
  "meta": {
    "endpoint": "durations",
    "route": "/between",
    "params": {
      "from": "2026-01-31",
      "to": "2026-03-01",
      "tz": "Europe/Copenhagen",
      "maxUnits": 6
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Only the two largest units
GET /api/durations/between?from=2026-01-01T00%3A00%3A00Z&to=2027-03-15T06%3A30%3A00Z&maxUnits=2
{
  "data": {
    "from": "2026-01-01T00:00:00.000Z",
    "to": "2027-03-15T06:30:00.000Z",
    "timeZone": "UTC",
    "negative": false,
    "years": 1,
    "months": 2,
    "days": 0,
    "hours": 0,
    "minutes": 0,
    "seconds": 0,
    "iso": "P1Y2M",
    "truncated": true,
    "remainderSeconds": 1233000,
    "totalSeconds": 37866600,
    "totalDays": 438.2708333333333
  },
  "meta": {
    "endpoint": "durations",
    "route": "/between",
    "params": {
      "from": "2026-01-01",
      "to": "2027-03-15T06:30:00.000Z",
      "tz": "UTC",
      "maxUnits": 2
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Backwards, from a date to an earlier one
GET /api/durations/between?from=2026-03-31&to=2026-02-28
{
  "data": {
    "from": "2026-03-31T00:00:00.000Z",
    "to": "2026-02-28T00:00:00.000Z",
    "timeZone": "UTC",
    "negative": true,
    "years": 0,
    "months": 1,
    "days": 0,
    "hours": 0,
    "minutes": 0,
    "seconds": 0,
    "iso": "-P1M",
    "truncated": false,
    "remainderSeconds": 0,
    "totalSeconds": -2678400,
    "totalDays": -31
  },
  "meta": {
    "endpoint": "durations",
    "route": "/between",
    "params": {
      "from": "2026-03-31",
      "to": "2026-02-28",
      "tz": "UTC",
      "maxUnits": 6
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Time since a release, live
GET /api/durations/between?from=2026-01-01&maxUnits=2
{
  "data": {
    "from": "2026-01-01T00:00:00.000Z",
    "to": "2026-07-29T08:13:22.000Z",
    "timeZone": "UTC",
    "negative": false,
    "years": 0,
    "months": 6,
    "days": 28,
    "hours": 0,
    "minutes": 0,
    "seconds": 0,
    "iso": "P6M28D",
    "truncated": true,
    "remainderSeconds": 29602,
    "totalSeconds": 18087202,
    "totalDays": 209.34261574074074
  },
  "meta": {
    "endpoint": "durations",
    "route": "/between",
    "params": {
      "from": "2026-01-01",
      "tz": "UTC",
      "maxUnits": 2,
      "to": "2026-07-29T08:13:22.000Z"
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}

GET /api/durations/add

Applies years, then months, then weeks and days on the wall clock, resolves that value in the zone once, then adds the exact time components. Clamping and any DST adjustment are reported explicitly.

Live requestRuns against the public API
No key required
GET/api/durations/add?start=2026-01-31&duration=P1M

Request parameters

Starting instant: YYYY-MM-DD (UTC midnight) or a timestamp with Z / an explicit offset.

ISO 8601 duration to add; a leading '-' subtracts. Years, months, weeks and days must be whole numbers — only H, M and S may carry a fraction.

IANA timezone used for the calendar (nominal) part of the arithmetic, case-insensitive. UTC means days are exactly 86400 seconds; a real zone makes days wall-clock days that can be 23 or 25 hours long.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/durations/add?start=2026-01-31&duration=P1M"
const res = await fetch("https://randomapi.dev/api/durations/add?start=2026-01-31&duration=P1M");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/durations/add?start=2026-01-31&duration=P1M").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/durations/add?start=2026-01-31&duration=P1M"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 3
start date required

Starting instant: YYYY-MM-DD (UTC midnight) or a timestamp with Z / an explicit offset.

example: start=2026-01-31
duration string required

ISO 8601 duration to add; a leading '-' subtracts. Years, months, weeks and days must be whole numbers — only H, M and S may carry a fraction.

allowed: ≤ 64
example: duration=P1M
tz string

IANA timezone used for the calendar (nominal) part of the arithmetic, case-insensitive. UTC means days are exactly 86400 seconds; a real zone makes days wall-clock days that can be 23 or 25 hours long.

default: UTC
allowed: 1 – 64
example: tz=Europe/Copenhagen
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=start,startLocal
exclude list

Return all fields except these (comma-separated).

example: exclude=calendarDaysApplied
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 13
start string (ISO 8601 UTC)

Resolved starting instant.

example: 2026-01-31T00:00:00.000Z

startLocal string (local wall clock)

Wall-clock value of 'start' in the requested zone.

example: 2026-01-31T00:00:00.000

startOffset string (±HH:MM)

UTC offset in effect at 'start'.

example: +00:00

timeZone string

Canonical IANA zone the calendar part was applied in.

example: UTC

duration string

Normalized form of the duration that was applied.

example: P1M

steps object[]

The four ordered steps (years, months, days, time) with the signed amount, the wall clock after the step and whether an end-of-month clamp fired. The calendar steps run on the wall clock; the zone is applied once afterwards.

example: [{"unit":"years","amount":0,"local":"2026-01-31T00:00:00.000","clamped":false},{"unit":"months","amount":1,"local":"2026-02-28T00:00:00.000","clamped":true},{"unit":"days","amount":0,"local":"2026-02-28T00:00:00.000","clamped":false},{"unit":"time","amount":0,"local":"2026-02-28T00:00:00.000","clamped":false}]

clamped boolean

True when the years or months step clamped the day of month to the end of a shorter month.

example: true

dstAdjustment object nullable

Null when the duration has no calendar part (nothing is resolved through the wall clock) or when the calendar result was an ordinary wall-clock value. Otherwise names the gap (shifted forward by the missing interval) or fold (earlier instant chosen) and by how much.

example: null

result string (ISO 8601 UTC)

The resulting instant.

example: 2026-02-28T00:00:00.000Z

resultLocal string (local wall clock)

Wall-clock value of the result in the requested zone.

example: 2026-02-28T00:00:00.000

resultOffset string (±HH:MM)

UTC offset in effect at the result.

example: +00:00

exactSecondsApplied float

Signed seconds added by the exact time components (H, M, S) after the calendar steps.

example: 0

calendarDaysApplied integer

Signed calendar days added by the day step, counting each week as 7 days.

example: 0

Documented examples

Build-generated requests and complete responses
5
End-of-month clamping
GET /api/durations/add?start=2026-01-31&duration=P1M
{
  "data": {
    "start": "2026-01-31T00:00:00.000Z",
    "startLocal": "2026-01-31T00:00:00.000",
    "startOffset": "+00:00",
    "timeZone": "UTC",
    "duration": "P1M",
    "steps": [
      {
        "unit": "years",
        "amount": 0,
        "local": "2026-01-31T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "months",
        "amount": 1,
        "local": "2026-02-28T00:00:00.000",
        "clamped": true
      },
      {
        "unit": "days",
        "amount": 0,
        "local": "2026-02-28T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "time",
        "amount": 0,
        "local": "2026-02-28T00:00:00.000",
        "clamped": false
      }
    ],
    "clamped": true,
    "dstAdjustment": null,
    "result": "2026-02-28T00:00:00.000Z",
    "resultLocal": "2026-02-28T00:00:00.000",
    "resultOffset": "+00:00",
    "exactSecondsApplied": 0,
    "calendarDaysApplied": 0
    …
    ]
  }
}
Leap-year clamping
GET /api/durations/add?start=2024-01-31&duration=P1M
{
  "data": {
    "start": "2024-01-31T00:00:00.000Z",
    "startLocal": "2024-01-31T00:00:00.000",
    "startOffset": "+00:00",
    "timeZone": "UTC",
    "duration": "P1M",
    "steps": [
      {
        "unit": "years",
        "amount": 0,
        "local": "2024-01-31T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "months",
        "amount": 1,
        "local": "2024-02-29T00:00:00.000",
        "clamped": true
      },
      {
        "unit": "days",
        "amount": 0,
        "local": "2024-02-29T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "time",
        "amount": 0,
        "local": "2024-02-29T00:00:00.000",
        "clamped": false
      }
    ],
    "clamped": true,
    "dstAdjustment": null,
    "result": "2024-02-29T00:00:00.000Z",
    "resultLocal": "2024-02-29T00:00:00.000",
    "resultOffset": "+00:00",
    "exactSecondsApplied": 0,
    "calendarDaysApplied": 0
    …
    ]
  }
}
One calendar day across spring-forward
GET /api/durations/add?start=2026-03-28T23%3A00%3A00Z&duration=P1D&tz=Europe%2FCopenhagen
{
  "data": {
    "start": "2026-03-28T23:00:00.000Z",
    "startLocal": "2026-03-29T00:00:00.000",
    "startOffset": "+01:00",
    "timeZone": "Europe/Copenhagen",
    "duration": "P1D",
    "steps": [
      {
        "unit": "years",
        "amount": 0,
        "local": "2026-03-29T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "months",
        "amount": 0,
        "local": "2026-03-29T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "days",
        "amount": 1,
        "local": "2026-03-30T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "time",
        "amount": 0,
        "local": "2026-03-30T00:00:00.000",
        "clamped": false
      }
    ],
    "clamped": false,
    "dstAdjustment": null,
    "result": "2026-03-29T22:00:00.000Z",
    "resultLocal": "2026-03-30T00:00:00.000",
    "resultOffset": "+02:00",
    "exactSecondsApplied": 0,
    "calendarDaysApplied": 1"generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Exactly 24 hours across the same change
GET /api/durations/add?start=2026-03-28T23%3A00%3A00Z&duration=PT24H&tz=Europe%2FCopenhagen
{
  "data": {
    "start": "2026-03-28T23:00:00.000Z",
    "startLocal": "2026-03-29T00:00:00.000",
    "startOffset": "+01:00",
    "timeZone": "Europe/Copenhagen",
    "duration": "PT24H",
    "steps": [
      {
        "unit": "years",
        "amount": 0,
        "local": "2026-03-29T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "months",
        "amount": 0,
        "local": "2026-03-29T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "days",
        "amount": 0,
        "local": "2026-03-29T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "time",
        "amount": 86400,
        "local": "2026-03-30T01:00:00.000",
        "clamped": false
      }
    ],
    "clamped": false,
    "dstAdjustment": null,
    "result": "2026-03-29T23:00:00.000Z",
    "resultLocal": "2026-03-30T01:00:00.000",
    "resultOffset": "+02:00",
    "exactSecondsApplied": 86400,
    "calendarDaysApplied": 0"generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Subtract three months
GET /api/durations/add?start=2026-05-31&duration=-P3M
{
  "data": {
    "start": "2026-05-31T00:00:00.000Z",
    "startLocal": "2026-05-31T00:00:00.000",
    "startOffset": "+00:00",
    "timeZone": "UTC",
    "duration": "-P3M",
    "steps": [
      {
        "unit": "years",
        "amount": 0,
        "local": "2026-05-31T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "months",
        "amount": -3,
        "local": "2026-02-28T00:00:00.000",
        "clamped": true
      },
      {
        "unit": "days",
        "amount": 0,
        "local": "2026-02-28T00:00:00.000",
        "clamped": false
      },
      {
        "unit": "time",
        "amount": 0,
        "local": "2026-02-28T00:00:00.000",
        "clamped": false
      }
    ],
    "clamped": true,
    "dstAdjustment": null,
    "result": "2026-02-28T00:00:00.000Z",
    "resultLocal": "2026-02-28T00:00:00.000",
    "resultOffset": "+00:00",
    "exactSecondsApplied": 0,
    "calendarDaysApplied": 0
    …
    ]
  }
}

GET /api/durations/humanize

Formats the duration with Intl.DurationFormat and echoes the resolved locale. Units are never re-balanced; maxUnits truncates without rounding and returns the dropped part.

Live requestRuns against the public API
No key required
GET/api/durations/humanize?value=PT1H30M

Request parameters

ISO 8601 duration to render. A fraction in the smallest exact component is carried into the next smaller unit; a fractional year, month, week or day is rejected.

CLDR duration style: long ('1 hour, 30 minutes'), short ('1 hr, 30 min') or digital ('1:30:00', which always shows the full H:MM:SS clock).

Report at most this many non-zero units, counting from the largest one present; a zero component between two reported units costs nothing. Truncation is toward zero and never rounds — whatever is dropped is reported separately.

BCP 47 language tag for the wording and digits, e.g. de, ja or ar-EG-u-nu-arab. Malformed tags are a 400; an unmatched but well-formed tag falls back, which resolvedLocale makes visible.

Advanced response options4 options

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

Return all fields except these (comma-separated).

Pretty-print the JSON response.

Drop the envelope: return the raw array/object without data/meta wrapper.

Response

Example parameters are ready. Send the request to inspect the live response.

Route reference

Code samples Ready-to-copy requests in 4 languages
Choose a code sample language
curl "https://randomapi.dev/api/durations/humanize?value=PT1H30M"
const res = await fetch("https://randomapi.dev/api/durations/humanize?value=PT1H30M");
const { data, meta } = await res.json();
import requests

data = requests.get("https://randomapi.dev/api/durations/humanize?value=PT1H30M").json()["data"]
$json = json_decode(file_get_contents(
  "https://randomapi.dev/api/durations/humanize?value=PT1H30M"
), true);
$data = $json["data"];
Parameters Route-specific request inputs 4
value string required

ISO 8601 duration to render. A fraction in the smallest exact component is carried into the next smaller unit; a fractional year, month, week or day is rejected.

allowed: ≤ 64
example: value=PT1H30M
style enum

CLDR duration style: long ('1 hour, 30 minutes'), short ('1 hr, 30 min') or digital ('1:30:00', which always shows the full H:MM:SS clock).

default: long
allowed: long | short | digital
example: style=short
maxUnits int

Report at most this many non-zero units, counting from the largest one present; a zero component between two reported units costs nothing. Truncation is toward zero and never rounds — whatever is dropped is reported separately.

default: 6
allowed: 1 – 6
example: maxUnits=3
locale string

BCP 47 language tag for the wording and digits, e.g. de, ja or ar-EG-u-nu-arab. Malformed tags are a 400; an unmatched but well-formed tag falls back, which resolvedLocale makes visible.

default: en
allowed: 2 – 35
example: locale=de
Universal parameters Shared response and formatting options 4
fields list

Return only these fields (comma-separated). Mutually exclusive with 'exclude'.

example: fields=value,normalized
exclude list

Return all fields except these (comma-separated).

example: exclude=negative
pretty boolean

Pretty-print the JSON response.

default: false
example: pretty=true
unwrap boolean

Drop the envelope: return the raw array/object without data/meta wrapper.

default: false
example: unwrap=true
Response schema Fields returned in each record 11
value string

The validated input duration, unchanged.

example: PT1H30M

normalized string

What was actually formatted: the input with any fraction carried exactly into the next smaller exact unit, and truncated to maxUnits.

example: PT1H30M

text string

The rendered duration, produced from the runtime's CLDR data: Intl.DurationFormat where the runtime provides it, otherwise the same unit and list patterns composed from Intl.NumberFormat and Intl.ListFormat.

example: 1 hour, 30 minutes

style string

Requested style: long, short or digital.

example: long

locale string

The BCP 47 tag exactly as requested.

example: en

resolvedLocale string

resolvedOptions().locale of the formatter that ran — the locale the runtime actually matched, which reveals any fallback.

example: en

numberingSystem string

resolvedOptions().numberingSystem of the formatter that ran, used for the digits.

example: latn

units string[]

Units with a non-zero value that were handed to the formatter, largest first.

example: ["hours","minutes"]

truncated boolean

True when maxUnits dropped a non-zero unit.

example: false

remainder string nullable

The dropped part as an ISO 8601 duration, or null when nothing was dropped. Nothing is rounded away silently.

example: null

negative boolean

True when the duration carries a leading '-'.

example: false

Documented examples

Build-generated requests and complete responses
4
Long English wording
GET /api/durations/humanize?value=PT1H30M
{
  "data": {
    "value": "PT1H30M",
    "normalized": "PT1H30M",
    "text": "1 hour, 30 minutes",
    "style": "long",
    "locale": "en",
    "resolvedLocale": "en",
    "numberingSystem": "latn",
    "units": [
      "hours",
      "minutes"
    ],
    "truncated": false,
    "remainder": null,
    "negative": false
  },
  "meta": {
    "endpoint": "durations",
    "route": "/humanize",
    "params": {
      "value": "PT1H30M",
      "style": "long",
      "maxUnits": 6,
      "locale": "en"
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
Short style, three units
GET /api/durations/humanize?value=P1DT2H30M15S&style=short&maxUnits=3
{
  "data": {
    "value": "P1DT2H30M15S",
    "normalized": "P1DT2H30M",
    "text": "1 day, 2 hr, 30 min",
    "style": "short",
    "locale": "en",
    "resolvedLocale": "en",
    "numberingSystem": "latn",
    "units": [
      "days",
      "hours",
      "minutes"
    ],
    "truncated": true,
    "remainder": "PT15S",
    "negative": false
  },
  "meta": {
    "endpoint": "durations",
    "route": "/humanize",
    "params": {
      "value": "P1DT2H30M15S",
      "style": "short",
      "maxUnits": 3,
      "locale": "en"
    },
    "generatedAt": "2026-07-29T08:13:22.000Z",
    "warnings": [
      "maxUnits=3 dropped seconds; the omitted part is returned in 'remainder' and nothing was rounded."
    ]
  }
}
Digital clock style
GET /api/durations/humanize?value=PT1H30M5S&style=digital
{
  "data": {
    "value": "PT1H30M5S",
    "normalized": "PT1H30M5S",
    "text": "1:30:05",
    "style": "digital",
    "locale": "en",
    "resolvedLocale": "en",
    "numberingSystem": "latn",
    "units": [
      "hours",
      "minutes",
      "seconds"
    ],
    "truncated": false,
    "remainder": null,
    "negative": false
  },
  "meta": {
    "endpoint": "durations",
    "route": "/humanize",
    "params": {
      "value": "PT1H30M5S",
      "style": "digital",
      "maxUnits": 6,
      "locale": "en"
    },
    "generatedAt": "2026-07-29T08:13:22.000Z"
  }
}
German, two largest units
GET /api/durations/humanize?value=P2DT4H30M&locale=de&maxUnits=2
{
  "data": {
    "value": "P2DT4H30M",
    "normalized": "P2DT4H",
    "text": "2 Tage, 4 Stunden",
    "style": "long",
    "locale": "de",
    "resolvedLocale": "de",
    "numberingSystem": "latn",
    "units": [
      "days",
      "hours"
    ],
    "truncated": true,
    "remainder": "PT30M",
    "negative": false
  },
  "meta": {
    "endpoint": "durations",
    "route": "/humanize",
    "params": {
      "value": "P2DT4H30M",
      "style": "long",
      "maxUnits": 2,
      "locale": "de"
    },
    "generatedAt": "2026-07-29T08:13:22.000Z",
    "warnings": [
      "maxUnits=2 dropped minutes; the omitted part is returned in 'remainder' and nothing was rounded."
    ]
  }
}

About this API

Coverage & behavior

Everything about the ISO 8601 duration form P[nY][nM][nW][nD][T[nH][nM][nS]] — the string the YouTube Data API returns as PT1H30M, OpenAPI and JSON Schema validate as format: duration, and iCalendar carries as DURATION. Pure computation: no dataset, no clock except where documented, no network.

Grammar. Components must appear in the order above, each at most once, digits before every designator, designators uppercase. The week form is exclusiveP2W or the date/time form, never both — in the ABNF of RFC 5545 §3.3.6 and of RFC 3339 Appendix A alike, so P1W2D is a 400 (write P9D). The Y and M designators come from RFC 3339 Appendix A (dur-year, dur-month) and xs:duration, not from RFC 5545: iCalendar's dur-value has no year or month production at all and says so in its own prose, so it is cited here only for week-vs-day exclusivity and the leading sign. A decimal fraction (full stop or comma) is accepted only in the smallest component present, ISO 8601's lowest-order-component rule; both public ABNFs allow whole numbers only, and XML Schema restates the same idea for seconds alone, so a fractional input is an extension of those grammars. A leading - (or +) is also accepted: ISO 8601-1 durations are unsigned, but RFC 5545's dur-value admits + or - and xs:duration admits -, and that is the form used here. P alone, a T with no time component, and unknown designators are all explicit 400s that name the problem. Once leading zeroes are dropped, a component value may carry at most 9 digits either side of the decimal sign and must be exactly representable as a JSON number — anything else is rejected, never rounded.

Nominal vs exact. Years and months are nominal: they have no fixed length, and no conversion of P1Y to seconds is a fact. /parse therefore reports exactSeconds for the H/M/S part and puts any whole-duration total inside approximate, together with the mean-Gregorian assumption it used. Weeks and days are calendar units: exactly 24 hours in UTC, but wall-clock days in a real zone, so they can be 23 or 25 hours long. Hours, minutes and seconds are always exact SI seconds, and every total labelled exact (exactSeconds, nominalMonths, calendarDays) is summed on the decimal text with integer arithmetic rather than as a floating-point product — PT1.1H is 3960 seconds, not 3960.0000000000005.

/add — order, clamping and DST. The calendar part is applied to the wall clock in a fixed, documented order: years, then months, then weeks and days; the zone then resolves that wall-clock value to an instant exactly once; finally the exact time components are added as milliseconds. Order changes the answer, so it is pinned: 2024-02-29 +P1Y1M is 2025-03-28 while +P13M is 2025-03-29. Adding a month clamps to the last day of the shorter month — 2026-01-31 +P1M is 2026-02-28, and 2024-01-31 +P1M is 2024-02-29 — which is a documented choice, not a universal rule: because of it, P1M twice (2026-03-28) is not the same as P2M (2026-03-31). P1D means the same wall time on the next calendar day; PT24H means exactly 86400 seconds, so across a daylight-saving change the two differ by the size of the change. If the calendar result lands in a DST gap it is shifted forward by the missing interval; if it lands in a fold the earlier of the two instants is taken. Both cases are reported in dstAdjustment and as a warning — never silently. A duration with no calendar part never touches the wall clock at all, so PT1H moves the instant by exactly 3600 seconds even when the start sits in a repeated hour, where reading the local time back would have collapsed it onto the earlier occurrence. /add requires whole years, months, weeks and days: a fractional calendar unit is a 400 rather than a guess, and exact components are applied to millisecond precision, so PT0.0005S is a 400 instead of a rounded answer.

/between. Returns both the calendar breakdown (years, months, days, hours, minutes, seconds, in the same order /add applies) and the exact totals (totalSeconds, totalDays = elapsed SI time, unaffected by the zone). The breakdown is built by the same arithmetic /add uses, so /add(from, iso, tz) reproduces to exactly whenever truncated is false; maxUnits truncates toward zero and reports the exact leftover in remainderSeconds. Calendar differences are not symmetric under clamping: 2026-02-28 → 2026-03-31 is P1M3D, while 2026-03-31 → 2026-02-28 is -P1M. Inside a 25-hour fall-back day the hours component can legitimately reach 24, because the calendar day it sits in really was 25 hours long. Omitting to uses the request time and echoes the resolved instant.

/humanize. Wording and unit names come from the runtime's CLDR data through Intl.DurationFormat (resolvedLocale and numberingSystem echo what the runtime matched, so a locale fallback is visible). Intl.DurationFormat only landed in Node 23, so on an older runtime — including a static build importing this module — the same CLDR data is composed instead from Intl.NumberFormat's style: "unit" patterns and Intl.ListFormat's unit list, which is how CLDR builds a duration in the first place. The test suite compares the two paths on every locale its runtime resolves, in all three styles: they agree byte-for-byte, the one known exception being the digital clock separator in kl and ars, whose CLDR duration pattern no other Intl API exposes. The choices around the formatter are ours, not a specification: units are never re-balanced (PT90M is "90 minutes", not "1 hour, 30 minutes"), a fraction is carried exactly into the next smaller exact unit (PT1.5HPT1H30M), maxUnits truncates without rounding and returns the dropped part in remainder, and a fractional year, month, week or day is a 400 because it cannot be carried exactly.

Scope. This endpoint owns the duration grammar only. ISO 8601 date, time and interval strings (2026-07-10, 2026-07-10T12:00:00Z/PT1H) are deliberately out of scope; use /api/date-info and /api/timezones for those. There is no repeating-interval (R/…) support. ISO 8601-1:2019 is paywalled, so the grammar implemented here follows its public restatements — RFC 5545 §3.3.6, RFC 3339 Appendix A and XML Schema's xs:duration — and this API makes no claim of certified ISO conformance.

Use it for

  • Turn a YouTube Data API contentDetails.duration such as PT1H30M into hours and minutes
  • Validate an ISO 8601 duration from an OpenAPI, JSON Schema or iCalendar payload before storing it
  • Roll a subscription anchor date forward by P1M with explicit end-of-month clamping
  • Show a locale-aware 'time since deploy' label limited to the two largest units
  • Prove in a review that P1D and PT24H are not the same across a daylight-saving change

Frequently asked questions

What does PT1H30M mean?

It is the ISO 8601 duration for one hour and thirty minutes: P opens the duration, T separates the time part, and H/M are hours and minutes. /humanize?value=PT1H30M renders it as 1 hour, 30 minutes.

How do I convert an ISO 8601 duration to seconds?

/parse returns exactSeconds for the hours, minutes and seconds part, which is exact. Years and months have no fixed length, so any whole-duration total appears inside approximate with the mean-Gregorian assumption stated — it is never presented as a fact.

Does adding P1M always land on the same day of the month?

No. /add clamps to the last day of a shorter month, so 2026-01-31 +P1M is 2026-02-28 and 2024-01-31 +P1M is 2024-02-29. Because clamping is not reversible, applying P1M twice gives 2026-03-28 while P2M gives 2026-03-31.

Is P1D the same as PT24H?

Only when the zone has no offset change in between (tz=UTC always). P1D keeps the same wall-clock time on the next calendar day, while PT24H adds exactly 86400 seconds, so across a spring-forward in Europe/Copenhagen the two results differ by an hour.

Why is P1W2D rejected?

The ABNF in RFC 5545 §3.3.6 and in RFC 3339 Appendix A alike makes the week form an alternative to the date form, so weeks cannot be combined with other components. Write P9D instead; P2W on its own is fine.

Can a duration contain a decimal, like PT0.5H?

Yes, in the smallest component present — ISO 8601's lowest-order-component rule, with a full stop or a comma. PT1.5H parses, and /humanize carries the fraction exactly into PT1H30M. A fraction in a larger component such as P1.5DT2H is a 400, and /add rejects fractional years, months, weeks and days outright.

Standards & references