{
 "openapi": "3.1.0",
 "info": {
  "title": "Anvil",
  "version": "1.24.0",
  "description": "Small, honest JSON utilities. No keys, no accounts, no logs. 60 requests a minute per client."
 },
 "servers": [
  {
   "url": "https://api.oddlathe.com"
  }
 ],
 "paths": {
  "/v1/uuid": {
   "get": {
    "summary": "Random UUIDs (v4) or time-ordered UUIDs (v7)",
    "description": "v7 UUIDs put a millisecond timestamp in the first 48 bits, so they sort by creation time and index well in databases. v4 is 122 random bits.",
    "tags": [
     "Identifiers"
    ],
    "parameters": [
     {
      "name": "version",
      "in": "query",
      "required": false,
      "description": "4 (random) or 7 (time-ordered, sorts by creation time)",
      "schema": {
       "type": "integer",
       "enum": [
        4,
        7
       ],
       "default": 4
      }
     },
     {
      "name": "count",
      "in": "query",
      "required": false,
      "description": "How many, 1 to 100",
      "schema": {
       "type": "integer",
       "default": 1
      }
     },
     {
      "name": "upper",
      "in": "query",
      "required": false,
      "description": "Uppercase hex",
      "schema": {
       "type": "boolean",
       "default": false
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/ulid": {
   "get": {
    "summary": "ULIDs: sortable, 26-character IDs",
    "description": "48-bit millisecond time + 80 random bits in Crockford base32. Lexicographically sortable and URL-safe.",
    "tags": [
     "Identifiers"
    ],
    "parameters": [
     {
      "name": "count",
      "in": "query",
      "required": false,
      "description": "How many, 1 to 100",
      "schema": {
       "type": "integer",
       "default": 1
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/nanoid": {
   "get": {
    "summary": "Short URL-safe random IDs (21 chars, 126 bits)",
    "description": "",
    "tags": [
     "Identifiers"
    ],
    "parameters": [
     {
      "name": "count",
      "in": "query",
      "required": false,
      "description": "How many, 1 to 100",
      "schema": {
       "type": "integer",
       "default": 1
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/password": {
   "get": {
    "summary": "Strong random passwords with entropy estimate",
    "description": "Generated with the operating system's cryptographic random source. Every chosen character set is guaranteed to appear. Nothing is stored; responses are marked no-store. For anything important, generating locally is still best.",
    "tags": [
     "Random"
    ],
    "parameters": [
     {
      "name": "length",
      "in": "query",
      "required": false,
      "description": "8 to 128 characters",
      "schema": {
       "type": "integer",
       "default": 20
      }
     },
     {
      "name": "count",
      "in": "query",
      "required": false,
      "description": "1 to 20",
      "schema": {
       "type": "integer",
       "default": 1
      }
     },
     {
      "name": "symbols",
      "in": "query",
      "required": false,
      "description": "Include symbols",
      "schema": {
       "type": "boolean",
       "default": true
      }
     },
     {
      "name": "numbers",
      "in": "query",
      "required": false,
      "description": "Include digits",
      "schema": {
       "type": "boolean",
       "default": true
      }
     },
     {
      "name": "uppercase",
      "in": "query",
      "required": false,
      "description": "Include capitals",
      "schema": {
       "type": "boolean",
       "default": true
      }
     },
     {
      "name": "no_ambiguous",
      "in": "query",
      "required": false,
      "description": "Leave out I, l, 1, O, 0, o",
      "schema": {
       "type": "boolean",
       "default": false
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/random": {
   "get": {
    "summary": "Random integers, coin flips or dice rolls",
    "description": "Uses a cryptographically secure generator, so results are unpredictable and unbiased.",
    "tags": [
     "Random"
    ],
    "parameters": [
     {
      "name": "min",
      "in": "query",
      "required": false,
      "description": "Lowest value (integers mode)",
      "schema": {
       "type": "integer",
       "default": 1
      }
     },
     {
      "name": "max",
      "in": "query",
      "required": false,
      "description": "Highest value",
      "schema": {
       "type": "integer",
       "default": 100
      }
     },
     {
      "name": "count",
      "in": "query",
      "required": false,
      "description": "1 to 100",
      "schema": {
       "type": "integer",
       "default": 1
      }
     },
     {
      "name": "unique",
      "in": "query",
      "required": false,
      "description": "No repeats",
      "schema": {
       "type": "boolean",
       "default": false
      }
     },
     {
      "name": "dice",
      "in": "query",
      "required": false,
      "description": "Dice notation like 2d6+1 (overrides min/max)",
      "schema": {
       "type": "string"
      },
      "example": "2d6"
     },
     {
      "name": "coin",
      "in": "query",
      "required": false,
      "description": "Flip coins instead",
      "schema": {
       "type": "boolean",
       "default": false
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/time": {
   "get": {
    "summary": "Current time (or any moment) in a time zone, with next DST change",
    "description": "Includes UTC offset, abbreviation, whether daylight saving is in force, ISO week and the next offset change (if the zone has one in the next two years).",
    "tags": [
     "Time"
    ],
    "parameters": [
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "description": "IANA zone like Europe/London",
      "schema": {
       "type": "string",
       "default": "UTC"
      },
      "example": "America/New_York"
     },
     {
      "name": "at",
      "in": "query",
      "required": false,
      "description": "'now', a Unix timestamp, or ISO 8601",
      "schema": {
       "type": "string",
       "default": "now"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/time/convert": {
   "get": {
    "summary": "Convert a local time between time zones",
    "description": "A time with no offset is read as wall-clock time in 'from'. Times that don't exist (the hour skipped when clocks go forward) are flagged.",
    "tags": [
     "Time"
    ],
    "parameters": [
     {
      "name": "time",
      "in": "query",
      "required": true,
      "description": "ISO 8601 local time, Unix timestamp or 'now'",
      "schema": {
       "type": "string"
      },
      "example": "2026-11-03T09:30"
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "Zone the time is in",
      "schema": {
       "type": "string",
       "default": "UTC"
      },
      "example": "Europe/London"
     },
     {
      "name": "to",
      "in": "query",
      "required": true,
      "description": "Comma-separated target zones (up to 20)",
      "schema": {
       "type": "string"
      },
      "example": "Asia/Tokyo,America/Los_Angeles"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/timezones": {
   "get": {
    "summary": "All IANA time zone names with their current offset",
    "description": "",
    "tags": [
     "Time"
    ],
    "parameters": [
     {
      "name": "filter",
      "in": "query",
      "required": false,
      "description": "Only names containing this text",
      "schema": {
       "type": "string"
      },
      "example": "Europe"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/holidays": {
   "get": {
    "summary": "Public / bank holidays for a year",
    "description": "Calculated from the published rules (Easter by the Gregorian computus, 'first Monday' rules, substitute days) plus known one-off UK and Irish proclamations. Future one-off holidays appear only once announced, so check the official source for legal deadlines.",
    "tags": [
     "Calendar"
    ],
    "parameters": [
     {
      "name": "region",
      "in": "query",
      "required": true,
      "description": "gb-eng\u2026, ie, us, de/de-by\u2026, fr, nl, ca/ca-on\u2026, au/au-nsw\u2026, nz",
      "schema": {
       "type": "string",
       "enum": [
        "gb-eng",
        "gb-wls",
        "gb-sct",
        "gb-nir",
        "ie",
        "us",
        "de",
        "de-bw",
        "de-by",
        "de-be",
        "de-bb",
        "de-hb",
        "de-hh",
        "de-he",
        "de-mv",
        "de-ni",
        "de-nw",
        "de-rp",
        "de-sl",
        "de-sn",
        "de-st",
        "de-sh",
        "de-th",
        "ca",
        "ca-ab",
        "ca-bc",
        "ca-mb",
        "ca-nb",
        "ca-nl",
        "ca-ns",
        "ca-nt",
        "ca-nu",
        "ca-on",
        "ca-pe",
        "ca-qc",
        "ca-sk",
        "ca-yt",
        "au",
        "au-act",
        "au-nsw",
        "au-nt",
        "au-qld",
        "au-sa",
        "au-tas",
        "au-vic",
        "au-wa",
        "nz",
        "fr",
        "nl"
       ]
      },
      "example": "gb-eng"
     },
     {
      "name": "year",
      "in": "query",
      "required": false,
      "description": "1990 to 2100",
      "schema": {
       "type": "integer"
      },
      "example": 2026
     },
     {
      "name": "set",
      "in": "query",
      "required": false,
      "description": "For Canada: observed (federal + extras, default) or statutory (employment-standards list)",
      "schema": {
       "type": "string",
       "enum": [
        "observed",
        "statutory"
       ]
      },
      "example": "statutory"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/holidays.ics": {
   "get": {
    "summary": "Subscribe to bank holidays in any calendar app (iCalendar feed)",
    "description": "Returns text/calendar (RFC 5545) with one all-day event per holiday, stable UIDs and a daily refresh hint. Paste the URL into Google Calendar (From URL), Apple Calendar (New Calendar Subscription) or Outlook (Subscribe from web). Same rules as /v1/holidays.",
    "tags": [
     "Calendar"
    ],
    "parameters": [
     {
      "name": "region",
      "in": "query",
      "required": true,
      "description": "gb-eng\u2026, ie, us, de/de-by\u2026, fr, nl, ca/ca-on\u2026, au/au-nsw\u2026, nz",
      "schema": {
       "type": "string",
       "enum": [
        "gb-eng",
        "gb-wls",
        "gb-sct",
        "gb-nir",
        "ie",
        "us",
        "de",
        "de-bw",
        "de-by",
        "de-be",
        "de-bb",
        "de-hb",
        "de-hh",
        "de-he",
        "de-mv",
        "de-ni",
        "de-nw",
        "de-rp",
        "de-sl",
        "de-sn",
        "de-st",
        "de-sh",
        "de-th",
        "ca",
        "ca-ab",
        "ca-bc",
        "ca-mb",
        "ca-nb",
        "ca-nl",
        "ca-ns",
        "ca-nt",
        "ca-nu",
        "ca-on",
        "ca-pe",
        "ca-qc",
        "ca-sk",
        "ca-yt",
        "au",
        "au-act",
        "au-nsw",
        "au-nt",
        "au-qld",
        "au-sa",
        "au-tas",
        "au-vic",
        "au-wa",
        "nz",
        "fr",
        "nl"
       ]
      },
      "example": "gb-eng"
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "First year, 1990 to 2100",
      "schema": {
       "type": "integer"
      },
      "example": 2026
     },
     {
      "name": "years",
      "in": "query",
      "required": false,
      "description": "How many years, 1 to 6",
      "schema": {
       "type": "integer",
       "default": 4
      }
     },
     {
      "name": "set",
      "in": "query",
      "required": false,
      "description": "For Canada: observed or statutory",
      "schema": {
       "type": "string",
       "enum": [
        "observed",
        "statutory"
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/holidays/next": {
   "get": {
    "summary": "The next few holidays from today (or a date)",
    "description": "",
    "tags": [
     "Calendar"
    ],
    "parameters": [
     {
      "name": "region",
      "in": "query",
      "required": true,
      "description": "Region code",
      "schema": {
       "type": "string",
       "enum": [
        "gb-eng",
        "gb-wls",
        "gb-sct",
        "gb-nir",
        "ie",
        "us",
        "de",
        "de-bw",
        "de-by",
        "de-be",
        "de-bb",
        "de-hb",
        "de-hh",
        "de-he",
        "de-mv",
        "de-ni",
        "de-nw",
        "de-rp",
        "de-sl",
        "de-sn",
        "de-st",
        "de-sh",
        "de-th",
        "ca",
        "ca-ab",
        "ca-bc",
        "ca-mb",
        "ca-nb",
        "ca-nl",
        "ca-ns",
        "ca-nt",
        "ca-nu",
        "ca-on",
        "ca-pe",
        "ca-qc",
        "ca-sk",
        "ca-yt",
        "au",
        "au-act",
        "au-nsw",
        "au-nt",
        "au-qld",
        "au-sa",
        "au-tas",
        "au-vic",
        "au-wa",
        "nz",
        "fr",
        "nl"
       ]
      },
      "example": "gb-sct"
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "Start date, YYYY-MM-DD",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "count",
      "in": "query",
      "required": false,
      "description": "1 to 20",
      "schema": {
       "type": "integer",
       "default": 3
      }
     },
     {
      "name": "set",
      "in": "query",
      "required": false,
      "description": "For Canada: observed or statutory",
      "schema": {
       "type": "string",
       "enum": [
        "observed",
        "statutory"
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/workdays": {
   "get": {
    "summary": "Count working days between two dates, or add working days to a date",
    "description": "Working days are Monday to Friday, minus the region's public holidays when a region is given. Both ends of a range are included.",
    "tags": [
     "Calendar"
    ],
    "parameters": [
     {
      "name": "from",
      "in": "query",
      "required": true,
      "description": "Start date, YYYY-MM-DD",
      "schema": {
       "type": "string"
      },
      "example": "2026-12-01"
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "description": "End date (inclusive) for counting",
      "schema": {
       "type": "string"
      },
      "example": "2026-12-31"
     },
     {
      "name": "add",
      "in": "query",
      "required": false,
      "description": "Instead of 'to': add this many working days (negative goes back)",
      "schema": {
       "type": "integer"
      },
      "example": 10
     },
     {
      "name": "region",
      "in": "query",
      "required": false,
      "description": "Also skip this region's holidays",
      "schema": {
       "type": "string",
       "enum": [
        "gb-eng",
        "gb-wls",
        "gb-sct",
        "gb-nir",
        "ie",
        "us",
        "de",
        "de-bw",
        "de-by",
        "de-be",
        "de-bb",
        "de-hb",
        "de-hh",
        "de-he",
        "de-mv",
        "de-ni",
        "de-nw",
        "de-rp",
        "de-sl",
        "de-sn",
        "de-st",
        "de-sh",
        "de-th",
        "ca",
        "ca-ab",
        "ca-bc",
        "ca-mb",
        "ca-nb",
        "ca-nl",
        "ca-ns",
        "ca-nt",
        "ca-nu",
        "ca-on",
        "ca-pe",
        "ca-qc",
        "ca-sk",
        "ca-yt",
        "au",
        "au-act",
        "au-nsw",
        "au-nt",
        "au-qld",
        "au-sa",
        "au-tas",
        "au-vic",
        "au-wa",
        "nz",
        "fr",
        "nl"
       ]
      },
      "example": "gb-eng"
     },
     {
      "name": "set",
      "in": "query",
      "required": false,
      "description": "For Canada: observed or statutory (only used with region)",
      "schema": {
       "type": "string",
       "enum": [
        "observed",
        "statutory"
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/cron/next": {
   "get": {
    "summary": "When will a cron schedule run next?",
    "description": "Supports lists, ranges, steps, month and weekday names, 7 as Sunday, and the classic rule that when both day-of-month and day-of-week are restricted, either one matching is enough. Local times skipped by a DST change are left out; repeated times run once.",
    "tags": [
     "Time"
    ],
    "parameters": [
     {
      "name": "expr",
      "in": "query",
      "required": true,
      "description": "5-field cron expression or @daily, @hourly\u2026",
      "schema": {
       "type": "string"
      },
      "example": "*/15 9-17 * * mon-fri"
     },
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "description": "Time zone the schedule runs in",
      "schema": {
       "type": "string",
       "default": "UTC"
      },
      "example": "Europe/London"
     },
     {
      "name": "count",
      "in": "query",
      "required": false,
      "description": "1 to 50",
      "schema": {
       "type": "integer",
       "default": 5
      }
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "Start after this moment (ISO 8601 / Unix)",
      "schema": {
       "type": "string",
       "default": "now"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/cron/explain": {
   "get": {
    "summary": "Explain a cron schedule in plain English, with next runs",
    "description": "Sibling of /v1/cron/next and of Tooltern's cron explainer: breaks each field down, writes one plain-English sentence, lists the next runs (DST-aware) and warns when day-of-month and day-of-week are ORed. Lists, ranges, steps, month/weekday names, 7 as Sunday, and @hourly/@daily/@weekly/@monthly/@yearly. Pure parsing; nothing is stored.",
    "tags": [
     "Time"
    ],
    "parameters": [
     {
      "name": "expr",
      "in": "query",
      "required": true,
      "description": "5-field cron expression or @daily, @hourly\u2026",
      "schema": {
       "type": "string"
      },
      "example": "*/15 9-17 * * mon-fri"
     },
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "description": "Time zone the schedule runs in",
      "schema": {
       "type": "string",
       "default": "UTC"
      },
      "example": "Europe/London"
     },
     {
      "name": "count",
      "in": "query",
      "required": false,
      "description": "Next runs to list, 1 to 50",
      "schema": {
       "type": "integer",
       "default": 5
      }
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "Start after this moment (ISO 8601 / Unix)",
      "schema": {
       "type": "string",
       "default": "now"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/date/diff": {
   "get": {
    "summary": "Age, duration and ISO week maths between two dates",
    "description": "Difference between two moments: signed Y/M/D/H/M/S breakdown, total seconds, ISO 8601 duration, calendar age (decimal years + Y/M/D), ISO week numbers, weekdays, and whether the span crosses a daylight-saving change when a time zone is given. Accepts dates, datetimes, Unix timestamps or 'now'. Pure parsing; nothing is stored.",
    "tags": [
     "Time"
    ],
    "parameters": [
     {
      "name": "from",
      "in": "query",
      "required": true,
      "description": "Start: YYYY-MM-DD, ISO 8601 datetime, Unix timestamp or 'now'",
      "schema": {
       "type": "string"
      },
      "example": "1990-05-15"
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "description": "End moment (defaults to now)",
      "schema": {
       "type": "string",
       "default": "now"
      },
      "example": "2026-11-06T12:00"
     },
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "description": "IANA zone for naive inputs and DST detection",
      "schema": {
       "type": "string",
       "default": "UTC"
      },
      "example": "Europe/London"
     },
     {
      "name": "units",
      "in": "query",
      "required": false,
      "description": "Optional comma list to filter breakdown: years,months,days,hours,minutes,seconds",
      "schema": {
       "type": "string"
      },
      "example": "years,months,days"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/countdown": {
   "get": {
    "summary": "Days until a date: sleeps, weekdays vs weekend days, time left, progress",
    "description": "How long until (or since) a date, counted the ways people actually count: calendar days, sleeps (midnights passed in the chosen zone), weekdays (Mon\u2013Fri) and weekend days among the days ahead, whole 24-hour days, inclusive days and days/hours/minutes left. Optional start gives a progress fraction and nights done/left; a clock-based day/night phase is included for each end. Named events cover Christmas, New Year, next Monday/Friday/Saturday, end of month and Halloween. Pure maths; nothing is stored.",
    "tags": [
     "Time"
    ],
    "parameters": [
     {
      "name": "to",
      "in": "query",
      "required": false,
      "description": "Target: YYYY-MM-DD (counts to the start of that day) or YYYY-MM-DDTHH:MM",
      "schema": {
       "type": "string"
      },
      "example": "2026-12-25"
     },
     {
      "name": "event",
      "in": "query",
      "required": false,
      "description": "Instead of to: christmas, new-year, next-monday, next-friday, next-saturday, end-of-month, halloween",
      "schema": {
       "type": "string",
       "enum": [
        "christmas",
        "new-year",
        "next-monday",
        "next-friday",
        "next-saturday",
        "end-of-month",
        "halloween"
       ]
      }
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "Count from this moment",
      "schema": {
       "type": "string",
       "default": "now"
      },
      "example": "2026-11-07T19:00"
     },
     {
      "name": "tz",
      "in": "query",
      "required": false,
      "description": "IANA zone whose midnights count as nights",
      "schema": {
       "type": "string",
       "default": "UTC"
      },
      "example": "Europe/London"
     },
     {
      "name": "start",
      "in": "query",
      "required": false,
      "description": "Optional start date for progress (how far through the wait)",
      "schema": {
       "type": "string"
      },
      "example": "2026-12-01"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/units": {
   "get": {
    "summary": "Convert length, mass, temperature, data-size or data-rate units",
    "description": "Convert a number between common units of length, mass, temperature or digital data size. Length uses metres as the base (m, km, cm, mm, mi, yd, ft, in, nmi). Mass uses kilograms (kg, g, mg, lb, oz, t for the metric tonne). Temperature accepts C, F and K with proper offset formulae. Data sizes distinguish SI (KB/MB/GB/TB = 1000) from IEC binary (KiB/MiB/GiB/TiB = 1024). Data rates convert between bits and bytes per second (Mbps, MB/s, MiB/s\u2026; lower-case b = bits). Optional category disambiguates. Pure maths; nothing is stored.",
    "tags": [
     "Convert"
    ],
    "parameters": [
     {
      "name": "value",
      "in": "query",
      "required": true,
      "description": "Number to convert",
      "schema": {
       "type": "string"
      },
      "example": "5"
     },
     {
      "name": "from",
      "in": "query",
      "required": true,
      "description": "Source unit (m, km, lb, C, MiB, Mbps, MB/s\u2026)",
      "schema": {
       "type": "string"
      },
      "example": "km"
     },
     {
      "name": "to",
      "in": "query",
      "required": true,
      "description": "Target unit",
      "schema": {
       "type": "string"
      },
      "example": "mi"
     },
     {
      "name": "category",
      "in": "query",
      "required": false,
      "description": "Optional: length, mass, temperature, data or rate (disambiguates)",
      "schema": {
       "type": "string"
      },
      "example": "length"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/percent": {
   "get": {
    "summary": "Percentage calculator: of, is, change, apply, reverse",
    "description": "Five percentage questions: what is X% of Y, X is what % of Y, percent change from A to B, apply \u00b1% to a value, or recover the original before a % change. Pure maths; nothing is stored.",
    "tags": [
     "Convert"
    ],
    "parameters": [
     {
      "name": "mode",
      "in": "query",
      "required": true,
      "description": "of | is | change | apply | reverse",
      "schema": {
       "type": "string"
      },
      "example": "of"
     },
     {
      "name": "percent",
      "in": "query",
      "required": false,
      "description": "Percent value (modes of / apply / reverse); trailing % ok",
      "schema": {
       "type": "string"
      },
      "example": "20"
     },
     {
      "name": "of",
      "in": "query",
      "required": false,
      "description": "Whole amount (mode=of)",
      "schema": {
       "type": "string"
      },
      "example": "50"
     },
     {
      "name": "part",
      "in": "query",
      "required": false,
      "description": "Part amount (mode=is)",
      "schema": {
       "type": "string"
      },
      "example": "15"
     },
     {
      "name": "whole",
      "in": "query",
      "required": false,
      "description": "Whole amount (mode=is)",
      "schema": {
       "type": "string"
      },
      "example": "60"
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "Start value (mode=change)",
      "schema": {
       "type": "string"
      },
      "example": "80"
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "description": "End value (mode=change)",
      "schema": {
       "type": "string"
      },
      "example": "100"
     },
     {
      "name": "value",
      "in": "query",
      "required": false,
      "description": "Starting value (mode=apply)",
      "schema": {
       "type": "string"
      },
      "example": "200"
     },
     {
      "name": "result",
      "in": "query",
      "required": false,
      "description": "Value after the change (mode=reverse)",
      "schema": {
       "type": "string"
      },
      "example": "115"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/battery": {
   "get": {
    "summary": "Power bank maths: charges after losses, size needed, flight rule, multi-gadget plan",
    "description": "How many real charges a power bank gives once conversion losses (about a third, as heat) are counted. mAh is converted to Wh at the cell voltage, so bank and phone numbers become comparable. Also returns the bank size for N charges, the airline 100 Wh / 160 Wh rule and a rough refill time. mode=plan checks a whole list of gadgets from one bank (in list order or shared evenly) and says which one it runs out on and what size bank the list needs. Pure maths; nothing is stored.",
    "tags": [
     "Convert"
    ],
    "parameters": [
     {
      "name": "mode",
      "in": "query",
      "required": false,
      "description": "charges (default) | plan | devices",
      "schema": {
       "type": "string",
       "enum": [
        "charges",
        "plan",
        "devices"
       ]
      }
     },
     {
      "name": "capacity",
      "in": "query",
      "required": true,
      "description": "Power bank capacity (mAh unless unit=wh)",
      "schema": {
       "type": "string"
      },
      "example": "10000"
     },
     {
      "name": "unit",
      "in": "query",
      "required": false,
      "description": "mah or wh",
      "schema": {
       "type": "string",
       "enum": [
        "mah",
        "wh"
       ]
      }
     },
     {
      "name": "volts",
      "in": "query",
      "required": false,
      "description": "Cell voltage the mAh is rated at",
      "schema": {
       "type": "number",
       "default": 3.7
      }
     },
     {
      "name": "efficiency",
      "in": "query",
      "required": false,
      "description": "Percent of the bank's energy that reaches the gadget (10-100)",
      "schema": {
       "type": "number",
       "default": 65
      }
     },
     {
      "name": "device",
      "in": "query",
      "required": false,
      "description": "Preset gadget (mode=charges): phone, phone_s, phone_l, tablet, watch, buds, ereader, torch, speaker, console, laptop, laptop_l",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "device_wh",
      "in": "query",
      "required": false,
      "description": "Custom device battery in Wh (overrides device)",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "device_mah",
      "in": "query",
      "required": false,
      "description": "Custom device battery in mAh (with device_volts, default 3.85)",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "health",
      "in": "query",
      "required": false,
      "description": "Device battery health percent (20-100)",
      "schema": {
       "type": "number",
       "default": 100
      }
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "description": "Device starts at this percent",
      "schema": {
       "type": "number",
       "default": 0
      }
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "description": "Charge the device to this percent",
      "schema": {
       "type": "number",
       "default": 100
      }
     },
     {
      "name": "want",
      "in": "query",
      "required": false,
      "description": "How many charges you want: returns the bank size needed",
      "schema": {
       "type": "number"
      },
      "example": "3"
     },
     {
      "name": "charger_w",
      "in": "query",
      "required": false,
      "description": "Wall charger watts: returns rough refill hours",
      "schema": {
       "type": "number"
      },
      "example": "20"
     },
     {
      "name": "plan",
      "in": "query",
      "required": false,
      "description": "mode=plan: gadget.count.from.to joined by _ (up to 12)",
      "schema": {
       "type": "string"
      },
      "example": "phone.2.20.100_tablet.1.10.100"
     },
     {
      "name": "order",
      "in": "query",
      "required": false,
      "description": "mode=plan: list (first gets charged first) or share (same fraction each)",
      "schema": {
       "type": "string",
       "enum": [
        "list",
        "share"
       ]
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/stream-data": {
   "get": {
    "summary": "Streaming data use: GB per hour, monthly allowance planner, film size, hours from GB",
    "description": "How much mobile data streaming uses, with typical rounded figures (1 Mbps for an hour is 0.45 GB). mode=plan checks a month of habits against an allowance: GB a day, whether it lasts, the day it runs out, a safe daily budget and the single biggest saving from dropping one quality rung. Pass day and used for a mid-month check. mode=size gives a film or episode's size, mode=hours how long a number of GB lasts, mode=activities the whole ladder. Pure maths; nothing is stored.",
    "tags": [
     "Convert"
    ],
    "parameters": [
     {
      "name": "mode",
      "in": "query",
      "required": false,
      "description": "plan (default) | size | hours | activities",
      "schema": {
       "type": "string",
       "enum": [
        "plan",
        "size",
        "hours",
        "activities"
       ]
      }
     },
     {
      "name": "cap",
      "in": "query",
      "required": false,
      "description": "mode=plan: monthly data allowance in GB",
      "schema": {
       "type": "number"
      },
      "example": "15"
     },
     {
      "name": "plan",
      "in": "query",
      "required": false,
      "description": "mode=plan: activity-hours_a_day-days_a_week joined by _ (up to 12)",
      "schema": {
       "type": "string"
      },
      "example": "v480-1-7_feed-0.5-7_m160-1-5_call-0.5-1"
     },
     {
      "name": "days",
      "in": "query",
      "required": false,
      "description": "mode=plan: days in the billing month (1-31)",
      "schema": {
       "type": "integer",
       "default": 30
      }
     },
     {
      "name": "day",
      "in": "query",
      "required": false,
      "description": "mode=plan: today's day of the billing month (1 = start)",
      "schema": {
       "type": "integer",
       "default": 1
      }
     },
     {
      "name": "used",
      "in": "query",
      "required": false,
      "description": "mode=plan: GB already used this month",
      "schema": {
       "type": "number",
       "default": 0
      }
     },
     {
      "name": "activity",
      "in": "query",
      "required": false,
      "description": "mode=size/hours: v240, v480, v720, v1080, v4k, feed, call, callsd, voice, m96, m160, m320, pod, game, cloud, cloud720, web",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "minutes",
      "in": "query",
      "required": false,
      "description": "mode=size: how long the film or episode is",
      "schema": {
       "type": "number",
       "default": 120
      }
     },
     {
      "name": "gb",
      "in": "query",
      "required": false,
      "description": "mode=hours: how many GB you have",
      "schema": {
       "type": "number"
      },
      "example": "10"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/video-size": {
   "get": {
    "summary": "Video file size: MB for a length, what fits in free space, bitrate and ffmpeg commands for a target size",
    "description": "How big a video is from a recording setting (or bitrate) and length: MB = Mbps x seconds / 8, with GiB as Windows shows it, whether it fits one FAT32 file and a typical 25 MB email limit. mode=fits says how many minutes of each setting fit in free space, mode=target gives the video bitrate (with 3% headroom), a sensible resolution and two-pass ffmpeg commands to land under a size limit, and mode=presets lists the typical rates. Pure maths; nothing is stored.",
    "tags": [
     "Convert"
    ],
    "parameters": [
     {
      "name": "mode",
      "in": "query",
      "required": false,
      "description": "size (default) | fits | target | presets",
      "schema": {
       "type": "string",
       "enum": [
        "size",
        "fits",
        "target",
        "presets"
       ]
      }
     },
     {
      "name": "preset",
      "in": "query",
      "required": false,
      "description": "Recording setting: p720_30, p1080_30, p1080_60, p4k_24, p4k_30, p4k_60, slo1080, screen, call",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "codec",
      "in": "query",
      "required": false,
      "description": "h264 or hevc (HEVC is about half the size)",
      "schema": {
       "type": "string",
       "enum": [
        "h264",
        "hevc"
       ]
      }
     },
     {
      "name": "mbps",
      "in": "query",
      "required": false,
      "description": "Custom bitrate in Mbps (overrides preset)",
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "duration",
      "in": "query",
      "required": false,
      "description": "Length: minutes (12), mm:ss (3:45), h:mm:ss (1:20:00) or 1h20m",
      "schema": {
       "type": "string"
      },
      "example": "10"
     },
     {
      "name": "free_gb",
      "in": "query",
      "required": false,
      "description": "mode=fits: free space in GB",
      "schema": {
       "type": "number"
      },
      "example": "64"
     },
     {
      "name": "target_mb",
      "in": "query",
      "required": false,
      "description": "mode=target: size limit in MB",
      "schema": {
       "type": "number"
      },
      "example": "25"
     },
     {
      "name": "audio_kbps",
      "in": "query",
      "required": false,
      "description": "mode=target: audio bitrate, 0 for none",
      "schema": {
       "type": "number",
       "default": 128
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/input-lag": {
   "get": {
    "summary": "Input lag: ms from button press to picture, split into six parts, with the biggest fixes ranked",
    "description": "How long it takes from pressing a button to seeing it on screen, using rounded typical figures: waiting for the next polling report, the trip over a cable or wireless, waiting for the game to read input, the game making the picture (1.5 to 3 frames), waiting for the screen's refresh and the screen's own processing. Returns typical, luckiest and unluckiest totals, frames late at 60 fps, a five-band verdict and the best single change per setting ranked by milliseconds saved. mode=compare puts two setups side by side (B uses b_ parameters) and mode=options lists every choice. Same model as the Tooltern input lag calculator. Pure maths; nothing is stored.",
    "tags": [
     "Convert"
    ],
    "parameters": [
     {
      "name": "mode",
      "in": "query",
      "required": false,
      "description": "estimate (default) | compare | options",
      "schema": {
       "type": "string",
       "enum": [
        "estimate",
        "compare",
        "options"
       ]
      }
     },
     {
      "name": "example",
      "in": "query",
      "required": false,
      "description": "Start from a preset setup: sofa, sofafix, pc, laptop",
      "schema": {
       "type": "string",
       "enum": [
        "sofa",
        "sofafix",
        "pc",
        "laptop"
       ]
      }
     },
     {
      "name": "link",
      "in": "query",
      "required": false,
      "description": "Controller connection: wired, dongle (own 2.4 GHz receiver) or bt (Bluetooth)",
      "schema": {
       "type": "string",
       "enum": [
        "wired",
        "dongle",
        "bt"
       ]
      }
     },
     {
      "name": "rate",
      "in": "query",
      "required": false,
      "description": "Polling rate in Hz: 125, 250, 500, 1000, 2000, 4000 or 8000",
      "schema": {
       "type": "integer",
       "default": 250
      }
     },
     {
      "name": "fps",
      "in": "query",
      "required": false,
      "description": "Game frame rate (10-1000)",
      "schema": {
       "type": "integer",
       "default": 60
      }
     },
     {
      "name": "pipe",
      "in": "query",
      "required": false,
      "description": "Game setting: low (Reflex / Anti-Lag / Boost), normal or vsync (caps fps at the refresh rate)",
      "schema": {
       "type": "string",
       "enum": [
        "low",
        "normal",
        "vsync"
       ]
      }
     },
     {
      "name": "hz",
      "in": "query",
      "required": false,
      "description": "Screen refresh rate: 60, 75, 90, 120, 144, 165, 240 or 360",
      "schema": {
       "type": "integer",
       "default": 60
      }
     },
     {
      "name": "screen",
      "in": "query",
      "required": false,
      "description": "monitor, office, gametv, tv (picture extras on), projector or custom",
      "schema": {
       "type": "string",
       "enum": [
        "monitor",
        "office",
        "gametv",
        "tv",
        "projector",
        "custom"
       ]
      }
     },
     {
      "name": "screen_ms",
      "in": "query",
      "required": false,
      "description": "Your screen's own processing lag in ms from a review (0-300); implies screen=custom",
      "schema": {
       "type": "number"
      },
      "example": "12"
     },
     {
      "name": "b_*",
      "in": "query",
      "required": false,
      "description": "mode=compare: the same parameters prefixed b_ describe setup B (b_example, b_link, b_screen ...)",
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/broadband-need": {
   "get": {
    "summary": "Broadband speed needed: busiest-moment Mb/s down and up, plan tier, does your plan cope, download times",
    "description": "How much broadband speed a household needs: adds up rounded typical Mb/s for everything happening at the busiest moment (4K and HD streams, video calls, online games, live streaming, cloud cameras, browsing, music), multiplies by headroom for Wi-Fi and busy evenings, and suggests the smallest common plan tier (30 to 2000 Mb/s) and upload tier that cover it. Pass plan_down / plan_up for a Plenty / Tight at busy times / Not enough verdict per direction. Also times a big download at your plan and 50 to 1000 Mb/s, in theory and at a realistic 70%. mode=download is just the time table; mode=options lists every figure. Same model as the Tooltern broadband speed calculator. Pure maths; nothing is stored.",
    "tags": [
     "Convert"
    ],
    "parameters": [
     {
      "name": "mode",
      "in": "query",
      "required": false,
      "description": "need (default) | download | options",
      "schema": {
       "type": "string",
       "enum": [
        "need",
        "download",
        "options"
       ]
      }
     },
     {
      "name": "use",
      "in": "query",
      "required": false,
      "description": "What happens at the same time: activity-count pairs joined by _ (k4, hd, sd, call, game, live, cam, web, music; up to 12, counts 0-20)",
      "schema": {
       "type": "string"
      },
      "example": "k4-1_hd-1_call-1_game-1_web-1"
     },
     {
      "name": "example",
      "in": "query",
      "required": false,
      "description": "Start from a household: solo, couple, family, house",
      "schema": {
       "type": "string",
       "enum": [
        "solo",
        "couple",
        "family",
        "house"
       ]
      }
     },
     {
      "name": "headroom",
      "in": "query",
      "required": false,
      "description": "wired (x1.25), mixed Wi-Fi (x1.5) or weak Wi-Fi / busy evenings (x2)",
      "schema": {
       "type": "string",
       "enum": [
        "wired",
        "mixed",
        "weak"
       ]
      }
     },
     {
      "name": "plan_down",
      "in": "query",
      "required": false,
      "description": "Your plan's download speed in Mb/s, to check it",
      "schema": {
       "type": "number"
      },
      "example": "100"
     },
     {
      "name": "plan_up",
      "in": "query",
      "required": false,
      "description": "Your plan's upload speed in Mb/s, to check it",
      "schema": {
       "type": "number"
      },
      "example": "20"
     },
     {
      "name": "gb",
      "in": "query",
      "required": false,
      "description": "Size of a big download in GB for the download-time table",
      "schema": {
       "type": "number",
       "default": 100
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/lorem": {
   "get": {
    "summary": "Lorem ipsum placeholder text: paragraphs, sentences or words",
    "description": "Placeholder text for mock-ups and fixtures. Ask for paragraphs, sentences or words; optionally start with the classic Lorem ipsum opener, wrap in HTML <p> tags, and pass a seed for a repeatable sample. Pure generation; nothing is stored.",
    "tags": [
     "Text"
    ],
    "parameters": [
     {
      "name": "paragraphs",
      "in": "query",
      "required": false,
      "description": "How many paragraphs (1\u201320). Wins over sentences/words if several are set.",
      "schema": {
       "type": "integer"
      },
      "example": "3"
     },
     {
      "name": "sentences",
      "in": "query",
      "required": false,
      "description": "How many sentences (1\u2013100)",
      "schema": {
       "type": "integer"
      },
      "example": "8"
     },
     {
      "name": "words",
      "in": "query",
      "required": false,
      "description": "How many words (1\u2013500)",
      "schema": {
       "type": "integer"
      },
      "example": "50"
     },
     {
      "name": "start",
      "in": "query",
      "required": false,
      "description": "Begin with classic Lorem ipsum\u2026 (default true)",
      "schema": {
       "type": "boolean",
       "default": true
      }
     },
     {
      "name": "html",
      "in": "query",
      "required": false,
      "description": "Also return an html field with <p> wrappers",
      "schema": {
       "type": "boolean",
       "default": false
      }
     },
     {
      "name": "seed",
      "in": "query",
      "required": false,
      "description": "Repeatable sample (integer or short string)",
      "schema": {
       "type": "string"
      },
      "example": "42"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/qr.svg": {
   "get": {
    "summary": "QR code as SVG (usable as an <img src>)",
    "description": "Encode text into a QR code and return image/svg+xml. Same generator as Tooltern's /api/qr and /qr-code page. Text travels in the URL \u2014 don't put secrets here. Nothing is stored or logged.",
    "tags": [
     "Design"
    ],
    "parameters": [
     {
      "name": "text",
      "in": "query",
      "required": true,
      "description": "Payload to encode (URL or text), up to 1200 UTF-8 bytes",
      "schema": {
       "type": "string"
      },
      "example": "hello"
     },
     {
      "name": "ec",
      "in": "query",
      "required": false,
      "description": "Error correction: L\u22487%, M\u224815%, Q\u224825%, H\u224830%",
      "schema": {
       "type": "string",
       "enum": [
        "L",
        "M",
        "Q",
        "H"
       ]
      }
     },
     {
      "name": "border",
      "in": "query",
      "required": false,
      "description": "Quiet-zone modules, 1\u20138",
      "schema": {
       "type": "integer",
       "default": 2
      }
     },
     {
      "name": "box",
      "in": "query",
      "required": false,
      "description": "Module size 1\u201320 (affects SVG millimetre scale)",
      "schema": {
       "type": "integer",
       "default": 10
      }
     },
     {
      "name": "fg",
      "in": "query",
      "required": false,
      "description": "Dark-module hex colour like 000000 or #1f6f5c",
      "schema": {
       "type": "string"
      },
      "example": "000000"
     },
     {
      "name": "bg",
      "in": "query",
      "required": false,
      "description": "Light-module / background hex colour",
      "schema": {
       "type": "string"
      },
      "example": "ffffff"
     }
    ],
    "responses": {
     "200": {
      "description": "SVG image (image/svg+xml)"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/qr": {
   "get": {
    "summary": "QR code as JSON wrapping the SVG string",
    "description": "Same QR as /v1/qr.svg, but the SVG is a string inside JSON \u2014 handy for scripts that already speak JSON. Prefer /v1/qr.svg when you can use it as an image URL.",
    "tags": [
     "Design"
    ],
    "parameters": [
     {
      "name": "text",
      "in": "query",
      "required": true,
      "description": "Payload to encode (URL or text), up to 1200 UTF-8 bytes",
      "schema": {
       "type": "string"
      },
      "example": "hello"
     },
     {
      "name": "ec",
      "in": "query",
      "required": false,
      "description": "Error correction: L\u22487%, M\u224815%, Q\u224825%, H\u224830%",
      "schema": {
       "type": "string",
       "enum": [
        "L",
        "M",
        "Q",
        "H"
       ]
      }
     },
     {
      "name": "border",
      "in": "query",
      "required": false,
      "description": "Quiet-zone modules, 1\u20138",
      "schema": {
       "type": "integer",
       "default": 2
      }
     },
     {
      "name": "box",
      "in": "query",
      "required": false,
      "description": "Module size 1\u201320 (affects SVG millimetre scale)",
      "schema": {
       "type": "integer",
       "default": 10
      }
     },
     {
      "name": "fg",
      "in": "query",
      "required": false,
      "description": "Dark-module hex colour like 000000 or #1f6f5c",
      "schema": {
       "type": "string"
      },
      "example": "000000"
     },
     {
      "name": "bg",
      "in": "query",
      "required": false,
      "description": "Light-module / background hex colour",
      "schema": {
       "type": "string"
      },
      "example": "ffffff"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/hash": {
   "get": {
    "summary": "Hash a short text (MD5, SHA-1, SHA-2, SHA-3, BLAKE2b)",
    "description": "Text travels in the URL, so don't hash secrets here. Use Tooltern's in-browser hash tool for anything private.",
    "tags": [
     "Text"
    ],
    "parameters": [
     {
      "name": "text",
      "in": "query",
      "required": true,
      "description": "Up to 4 KB of UTF-8 text",
      "schema": {
       "type": "string"
      },
      "example": "hello"
     },
     {
      "name": "algo",
      "in": "query",
      "required": false,
      "description": "Algorithm or 'all'",
      "schema": {
       "type": "string",
       "enum": [
        "md5",
        "sha1",
        "sha256",
        "sha384",
        "sha512",
        "sha3-256",
        "blake2b",
        "all"
       ],
       "default": "sha256"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/slug": {
   "get": {
    "summary": "Turn a title into a clean URL slug",
    "description": "Accents are folded to plain letters (\u00e9 \u2192 e), apostrophes dropped, everything else that isn't a letter or digit becomes the separator.",
    "tags": [
     "Text"
    ],
    "parameters": [
     {
      "name": "text",
      "in": "query",
      "required": true,
      "description": "Up to 4 KB",
      "schema": {
       "type": "string"
      },
      "example": "H\u00e9llo, World\u2019s Best Caf\u00e9!"
     },
     {
      "name": "sep",
      "in": "query",
      "required": false,
      "description": "Separator: - or _",
      "schema": {
       "type": "string",
       "enum": [
        "-",
        "_"
       ],
       "default": "-"
      }
     },
     {
      "name": "max",
      "in": "query",
      "required": false,
      "description": "Max length 1\u2013200",
      "schema": {
       "type": "integer",
       "default": 80
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/color": {
   "get": {
    "summary": "Colour formats (hex, RGB, HSL, OKLCH) and WCAG contrast",
    "description": "Contrast uses the WCAG 2 relative-luminance formula; AA needs 4.5:1 for body text and 3:1 for large text, AAA needs 7:1 and 4.5:1.",
    "tags": [
     "Design"
    ],
    "parameters": [
     {
      "name": "hex",
      "in": "query",
      "required": true,
      "description": "Colour like 1f6f5c or #fff (encode # as %23)",
      "schema": {
       "type": "string"
      },
      "example": "1f6f5c"
     },
     {
      "name": "against",
      "in": "query",
      "required": false,
      "description": "Second colour for a contrast check",
      "schema": {
       "type": "string"
      },
      "example": "ffffff"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/http-status": {
   "get": {
    "summary": "Look up any HTTP status code: meaning, causes, fixes and retry advice",
    "description": "Covers every registered code plus the non-standard ones people actually meet (nginx 444/499 and Cloudflare 520\u2013526). Prefer GET /v1/http-status/404 or ?code=404. Each answer includes phrase, category, plain summary, when_you_see_it, what_to_do, related codes, retry advice and default cacheability.",
    "tags": [
     "Web"
    ],
    "parameters": [
     {
      "name": "code",
      "in": "query",
      "required": false,
      "description": "A code like 404, a class like 5xx, or words from a name like 'too many'. Leave out for the full list. Path form /v1/http-status/{code} also works.",
      "schema": {
       "type": "string"
      },
      "example": "429"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/url/inspect": {
   "get": {
    "summary": "Link X-ray: who really owns a link, and its warning signs",
    "description": "Finds the registrable domain (the real owner) with the full Public Suffix List, decodes international names, and lists warning signs with a severity: @ tricks, disguised IP addresses, look-alike letters from other alphabets, digits for letters, official words dressed up in front, redirect parameters, double file endings and more. Pure text analysis: the link is never fetched, resolved or stored.",
    "tags": [
     "Web"
    ],
    "parameters": [
     {
      "name": "url",
      "in": "query",
      "required": true,
      "description": "The link to inspect, up to 2 KB. It is never opened.",
      "schema": {
       "type": "string"
      },
      "example": "http://paypal.com.secure-login.top/verify?next=https://example.net/"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/cookie/parse": {
   "get": {
    "summary": "Set-Cookie explainer: will the browser accept this cookie, and why not?",
    "description": "Reads Set-Cookie headers the way current browsers do (RFC 6265bis): every attribute explained in plain words, a verdict per cookie (accepted, rejected, deletes, or valid when no url is given), and each reason with a severity. Checks SameSite=None without Secure, Secure from http, Domain that doesn't cover the site or is a public suffix (full Public Suffix List), __Host- and __Secure- prefixes, 4096-byte size, Max-Age vs Expires, the 400-day cap, the default-path trap and session cookies missing HttpOnly. Pure text analysis: nothing is fetched or stored.",
    "tags": [
     "Web"
    ],
    "parameters": [
     {
      "name": "header",
      "in": "query",
      "required": false,
      "description": "One or more Set-Cookie header values, one per line (up to 30 lines, 8 KB). 'Set-Cookie:' in front is fine.",
      "schema": {
       "type": "string"
      },
      "example": "sid=abc123; Path=/; HttpOnly; SameSite=None"
     },
     {
      "name": "url",
      "in": "query",
      "required": false,
      "description": "The page or API address that sent the header, so Domain, Secure and the default Path can be checked",
      "schema": {
       "type": "string"
      },
      "example": "https://shop.example.co.uk/account/login"
     },
     {
      "name": "cookie",
      "in": "query",
      "required": false,
      "description": "Instead of header: a Cookie request header to split up and check for duplicates and size",
      "schema": {
       "type": "string"
      },
      "example": "sid=abc; theme=dark; sid=old"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/csp/parse": {
   "get": {
    "summary": "CSP header explainer: grade a Content-Security-Policy and check what it blocks",
    "description": "Reads a Content-Security-Policy the way current browsers do: every directive explained in plain words, a grade (strong / okay / weak), findings with severity (unquoted keywords, missing semicolons, 'unsafe-inline', wildcards, missing object-src / base-uri / frame-ancestors, meta-tag traps), and an optional check that applies the fallback chains (script-src-elem \u2192 script-src \u2192 default-src, worker-src \u2192 child-src \u2192 \u2026) plus nonce/hash/'strict-dynamic' rules. Pure text analysis: nothing is fetched or stored. Pass hash= to get 'sha256-\u2026' / 'sha384-\u2026' sources for an inline snippet.",
    "tags": [
     "Web"
    ],
    "parameters": [
     {
      "name": "header",
      "in": "query",
      "required": false,
      "description": "A Content-Security-Policy value, a header dump (with or without the name), or a <meta> tag. Up to 8 KB.",
      "schema": {
       "type": "string"
      },
      "example": "default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'none'"
     },
     {
      "name": "page",
      "in": "query",
      "required": false,
      "description": "Document URL used by the optional check simulator (scheme+host+port)",
      "schema": {
       "type": "string"
      },
      "example": "https://app.example.com/"
     },
     {
      "name": "check",
      "in": "query",
      "required": false,
      "description": "Optional action to test: script, inline-script, attr, eval, style, inline-style, style-attr, img, font, connect, frame, media, worker, object, form, base, ancestor",
      "schema": {
       "type": "string"
      },
      "example": "script"
     },
     {
      "name": "url",
      "in": "query",
      "required": false,
      "description": "Resource / form / ancestor URL for the check (not needed for eval or inline kinds)",
      "schema": {
       "type": "string"
      },
      "example": "https://cdn.example.net/lib.js"
     },
     {
      "name": "nonce",
      "in": "query",
      "required": false,
      "description": "Nonce value to try against 'nonce-\u2026' sources (no quotes)",
      "schema": {
       "type": "string"
      },
      "example": "r4nd0m"
     },
     {
      "name": "code",
      "in": "query",
      "required": false,
      "description": "Inline script/style/attribute text for hash matching",
      "schema": {
       "type": "string"
      },
      "example": "startApp()"
     },
     {
      "name": "hash",
      "in": "query",
      "required": false,
      "description": "Optional: compute 'sha256-\u2026' and 'sha384-\u2026' sources for this exact text (up to 4 KB)",
      "schema": {
       "type": "string"
      },
      "example": "startApp()"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/frame/check": {
   "get": {
    "summary": "Clickjacking headers: who may embed this page in a frame?",
    "description": "Reads X-Frame-Options and CSP frame-ancestors the way current browsers do: every value explained in plain words, a grade (protected / okay / needs_work), findings (missing defence, ALLOW-FROM ignored, meta frame-ancestors ignored, broad *, CSP overrides XFO), and an optional check that answers whether a given parent may embed the page. Modern rule: when both headers are present, frame-ancestors wins. Pure text analysis: nothing is fetched or stored.",
    "tags": [
     "Web"
    ],
    "parameters": [
     {
      "name": "header",
      "in": "query",
      "required": false,
      "description": "X-Frame-Options and/or a CSP with frame-ancestors, a header dump, or a <meta> tag. Up to 8 KB.",
      "schema": {
       "type": "string"
      },
      "example": "X-Frame-Options: SAMEORIGIN\nContent-Security-Policy: frame-ancestors 'self' https://pay.example"
     },
     {
      "name": "page",
      "in": "query",
      "required": false,
      "description": "The page being framed (scheme+host+port). Needed for SAMEORIGIN / 'self' and for parent checks.",
      "schema": {
       "type": "string"
      },
      "example": "https://app.example.com/account"
     },
     {
      "name": "parent",
      "in": "query",
      "required": false,
      "description": "Optional parent / ancestor URL to simulate (omit with parent_kind=null for an opaque origin)",
      "schema": {
       "type": "string"
      },
      "example": "https://evil.example/"
     },
     {
      "name": "parent_kind",
      "in": "query",
      "required": false,
      "description": "Optional shortcut instead of parent: same, example, evil, sub, null, sandbox, url",
      "schema": {
       "type": "string"
      },
      "example": "evil"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/text/inspect": {
   "get": {
    "summary": "Text X-ray: code points, graphemes, UTF-8/16 lengths and invisible characters",
    "description": "X-rays a short string the way Unicode sees it: grapheme clusters vs code points vs UTF-8 bytes vs JavaScript string.length (UTF-16 units), every character named and labelled (emoji, zero-width joiner, skin tone, flag letter, control\u2026), and findings for bidirectional overrides, invisible characters, look-alike letters from other alphabets, mixed scripts and characters that need 4-byte UTF-8. Optional form= returns a normalised copy; mojibake=1 previews the classic Windows-1252 misread. Pure text analysis: nothing is fetched or stored.",
    "tags": [
     "Web"
    ],
    "parameters": [
     {
      "name": "text",
      "in": "query",
      "required": true,
      "description": "The string to inspect, up to 4 KB of UTF-8. Never stored.",
      "schema": {
       "type": "string"
      },
      "example": "\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67 caf\u00e9"
     },
     {
      "name": "form",
      "in": "query",
      "required": false,
      "description": "Optional normalisation to return: nfc, nfd, nfkc or nfkd",
      "schema": {
       "type": "string"
      },
      "example": "nfc"
     },
     {
      "name": "mojibake",
      "in": "query",
      "required": false,
      "description": "If true, also show the UTF-8 bytes misread as Windows-1252",
      "schema": {
       "type": "boolean",
       "default": false
      }
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/status": {
   "get": {
    "summary": "Is this API healthy? Version, uptime and today's request count",
    "description": "",
    "tags": [
     "Meta"
    ],
    "parameters": [],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  },
  "/v1/http-status/{code}": {
   "get": {
    "summary": "Look up one HTTP status code by path",
    "description": "Same payload as GET /v1/http-status?code={code}. Prefer this form when the code is a path segment.",
    "tags": [
     "Web"
    ],
    "parameters": [
     {
      "name": "code",
      "in": "path",
      "required": true,
      "description": "A code like 404, a class like 5xx, or words from a name like teapot.",
      "schema": {
       "type": "string"
      },
      "example": "404"
     }
    ],
    "responses": {
     "200": {
      "description": "JSON result"
     },
     "400": {
      "$ref": "#/components/responses/Error"
     },
     "429": {
      "$ref": "#/components/responses/Limited"
     }
    }
   }
  }
 },
 "components": {
  "responses": {
   "Error": {
    "description": "Bad request",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "Limited": {
    "description": "Rate limited; see Retry-After"
   }
  },
  "schemas": {
   "Error": {
    "type": "object",
    "properties": {
     "error": {
      "type": "object",
      "properties": {
       "code": {
        "type": "string"
       },
       "message": {
        "type": "string"
       }
      }
     }
    }
   }
  }
 }
}