{
  "openapi": "3.1.1",
  "info": {
    "title": "TimeFair Meeting Planner API",
    "version": "1.0.0",
    "summary": "Find fair meeting times across time zones, with DST-exact local times and rotation.",
    "description": "Every endpoint performs the same calculation as the human page and returns inputs, result, units, assumptions, sources, warnings and computedAt. MCP endpoint: /mcp."
  },
  "servers": [
    {
      "url": "https://timefair.usefulweb.site"
    }
  ],
  "paths": {
    "/api/v1/calculate": {
      "post": {
        "operationId": "calculate",
        "summary": "Find fair meeting times across time zones, with DST-exact local times and rotation.",
        "description": "Enter participants with their IANA time zone and working hours. TimeFair scores every candidate slot by how far it falls outside each person’s working day (extra penalty at night), ranks slots by total and worst-case inconvenience, and builds a rotation plan for recurring meetings so the pain is shared. Exports an .ics calendar file.\n\nMethodology:\n- Time zones are IANA names validated with Intl.DateTimeFormat; every UTC↔local conversion uses the runtime’s tz database, so DST changes and +05:30 / +05:45 offsets are exact for each individual minute.\n- Candidate slots start at the organiser’s local midnight of the first day and step by the chosen granularity (in UTC) until the last slot ends before midnight after the last day.\n- Inconvenience per participant = weight × (minutes outside the working window + 2 × minutes outside the window between 22:00 and 07:00 local) ÷ 60, i.e. 1 point per hour outside working hours and 3 points per hour at night. A day that is not a working day counts entirely as outside.\n- Overnight windows (end earlier than start) belong to the day they start on; e.g. 22:00–06:00 on a Friday covers Friday 22:00 to Saturday 06:00.\n- Fairness cost of a slot = total + 1 × worst individual inconvenience + 0.5 × standard deviation (population). Lower is better; ties go to the earliest slot.\n- Rotation (deterministic greedy): the 48 lowest-cost slots form the pool; each occurrence repeats the slot’s organiser-local wall time every N days, re-scored for that exact date (DST-aware). For each occurrence the slot minimising the worst cumulative burden, then the total cumulative burden, then its own cost, then start time, is picked.\n- The .ics file uses UTC (Z) times, RFC 5545 escaping and 75-octet line folding; UID is a SHA-256 hash of the slot, participants and title (stable across re-imports); DTSTAMP is the computation time.",
        "tags": [
          "developer"
        ],
        "x-mcp-tool": "timefair",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TimefairInput"
              },
              "examples": {
                "example1": {
                  "summary": "Berlin, New York, Bengaluru, Kathmandu",
                  "value": {
                    "title": "Platform sync",
                    "startDate": "2026-10-19",
                    "days": 5,
                    "durationMinutes": 60,
                    "granularity": "30",
                    "recurrences": 4,
                    "participants": [
                      {
                        "name": "Anna",
                        "timeZone": "Europe/Berlin",
                        "workStart": "09:00",
                        "workEnd": "17:30"
                      },
                      {
                        "name": "Ben",
                        "timeZone": "America/New_York",
                        "workStart": "09:00",
                        "workEnd": "17:00"
                      },
                      {
                        "name": "Chitra",
                        "timeZone": "Asia/Kolkata",
                        "workStart": "10:00",
                        "workEnd": "18:30"
                      },
                      {
                        "name": "Dev",
                        "timeZone": "Asia/Kathmandu",
                        "workStart": "10:00",
                        "workEnd": "17:00"
                      }
                    ]
                  }
                },
                "example2": {
                  "summary": "San Francisco, London, Sydney (no comfortable slot → rotation)",
                  "value": {
                    "startDate": "2026-11-02",
                    "days": 5,
                    "durationMinutes": 45,
                    "granularity": "15",
                    "recurrences": 6,
                    "participants": [
                      {
                        "name": "Sam",
                        "timeZone": "America/Los_Angeles",
                        "workStart": "08:00",
                        "workEnd": "17:00"
                      },
                      {
                        "name": "Lea",
                        "timeZone": "Europe/London",
                        "workStart": "09:00",
                        "workEnd": "17:30"
                      },
                      {
                        "name": "Kai",
                        "timeZone": "Australia/Sydney",
                        "workStart": "08:30",
                        "workEnd": "17:00"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimefairResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tool": {
      "get": {
        "operationId": "getToolDefinition",
        "summary": "Inputs, output schema and examples for this tool",
        "responses": {
          "200": {
            "description": "Tool definition.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. 422 responses include per-field `errors`.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "TimefairInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "title": {
            "title": "Meeting title",
            "type": "string",
            "maxLength": 120,
            "default": "Team meeting"
          },
          "participants": {
            "title": "Participants",
            "description": "IANA time zone such as Europe/Berlin, America/New_York, Asia/Kolkata. Work end earlier than start means an overnight shift. The first participant is the organiser (date range uses their calendar).",
            "type": "array",
            "minItems": 2,
            "maxItems": 20,
            "items": {
              "type": "object",
              "additionalProperties": false,
              "properties": {
                "name": {
                  "title": "Name",
                  "type": "string",
                  "maxLength": 80
                },
                "timeZone": {
                  "title": "Time zone",
                  "type": "string",
                  "maxLength": 64
                },
                "workStart": {
                  "title": "Work start",
                  "type": "string",
                  "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
                  "default": "09:00"
                },
                "workEnd": {
                  "title": "Work end",
                  "type": "string",
                  "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$",
                  "default": "17:00"
                },
                "weight": {
                  "title": "Weight",
                  "type": "number",
                  "minimum": 0.1,
                  "maximum": 10,
                  "default": 1
                }
              },
              "required": [
                "name",
                "timeZone"
              ]
            }
          },
          "startDate": {
            "title": "First day",
            "description": "In the organiser’s time zone. Defaults to today (UTC date).",
            "type": "string",
            "format": "date"
          },
          "days": {
            "title": "Number of days",
            "type": "integer",
            "minimum": 1,
            "maximum": 14,
            "default": 5
          },
          "durationMinutes": {
            "title": "Meeting length",
            "type": "integer",
            "minimum": 15,
            "maximum": 480,
            "default": 60,
            "x-unit": "min"
          },
          "granularity": {
            "title": "Slot granularity",
            "type": "string",
            "enum": [
              "15",
              "30",
              "60"
            ],
            "default": "30"
          },
          "workdays": {
            "title": "Working days (local to each participant)",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "sun",
                "mon",
                "tue",
                "wed",
                "thu",
                "fri",
                "sat"
              ]
            },
            "default": [
              "mon",
              "tue",
              "wed",
              "thu",
              "fri"
            ]
          },
          "recurrences": {
            "title": "Occurrences to rotate",
            "type": "integer",
            "minimum": 1,
            "maximum": 12,
            "default": 4
          },
          "intervalDays": {
            "title": "Repeat every",
            "type": "integer",
            "minimum": 1,
            "maximum": 28,
            "default": 7,
            "x-unit": "days"
          }
        },
        "required": [
          "participants"
        ]
      },
      "TimefairResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "organizerTimeZone",
              "rangeStartUtc",
              "rangeEndUtc",
              "durationMinutes",
              "granularityMinutes",
              "candidatesEvaluated",
              "participants",
              "bestSlots",
              "rotation",
              "rotationTotals",
              "chosen"
            ],
            "properties": {
              "organizerTimeZone": {
                "type": "string"
              },
              "rangeStartUtc": {
                "type": "string",
                "format": "date-time"
              },
              "rangeEndUtc": {
                "type": "string",
                "format": "date-time"
              },
              "durationMinutes": {
                "type": "integer"
              },
              "granularityMinutes": {
                "type": "integer"
              },
              "candidatesEvaluated": {
                "type": "integer",
                "minimum": 1
              },
              "participants": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "name",
                    "timeZone",
                    "workStart",
                    "workEnd",
                    "overnight",
                    "weight"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "timeZone": {
                      "type": "string"
                    },
                    "workStart": {
                      "type": "string"
                    },
                    "workEnd": {
                      "type": "string"
                    },
                    "overnight": {
                      "type": "boolean"
                    },
                    "weight": {
                      "type": "number"
                    }
                  }
                }
              },
              "bestSlots": {
                "type": "array",
                "minItems": 1,
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "rank",
                    "startUtc",
                    "endUtc",
                    "cost",
                    "totalInconvenience",
                    "maxInconvenience",
                    "stdDevInconvenience",
                    "local"
                  ],
                  "properties": {
                    "rank": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "startUtc": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "endUtc": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "cost": {
                      "type": "number",
                      "minimum": 0
                    },
                    "totalInconvenience": {
                      "type": "number",
                      "minimum": 0
                    },
                    "maxInconvenience": {
                      "type": "number",
                      "minimum": 0
                    },
                    "stdDevInconvenience": {
                      "type": "number",
                      "minimum": 0
                    },
                    "local": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "name",
                          "timeZone",
                          "localStart",
                          "localEnd",
                          "utcOffset",
                          "weekday",
                          "inconvenience",
                          "outsideMinutes",
                          "nightMinutes"
                        ],
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "timeZone": {
                            "type": "string"
                          },
                          "localStart": {
                            "type": "string",
                            "description": "Local wall time YYYY-MM-DDTHH:MM"
                          },
                          "localEnd": {
                            "type": "string"
                          },
                          "utcOffset": {
                            "type": "string",
                            "pattern": "^[+-]\\d{2}:\\d{2}$"
                          },
                          "weekday": {
                            "type": "string"
                          },
                          "inconvenience": {
                            "type": "number",
                            "minimum": 0
                          },
                          "outsideMinutes": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "nightMinutes": {
                            "type": "integer",
                            "minimum": 0
                          }
                        }
                      }
                    }
                  }
                }
              },
              "rotation": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "occurrence",
                    "startUtc",
                    "endUtc",
                    "local",
                    "cumulative"
                  ],
                  "properties": {
                    "occurrence": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "startUtc": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "endUtc": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "local": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "name",
                          "timeZone",
                          "localStart",
                          "localEnd",
                          "utcOffset",
                          "weekday",
                          "inconvenience",
                          "outsideMinutes",
                          "nightMinutes"
                        ],
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "timeZone": {
                            "type": "string"
                          },
                          "localStart": {
                            "type": "string",
                            "description": "Local wall time YYYY-MM-DDTHH:MM"
                          },
                          "localEnd": {
                            "type": "string"
                          },
                          "utcOffset": {
                            "type": "string",
                            "pattern": "^[+-]\\d{2}:\\d{2}$"
                          },
                          "weekday": {
                            "type": "string"
                          },
                          "inconvenience": {
                            "type": "number",
                            "minimum": 0
                          },
                          "outsideMinutes": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "nightMinutes": {
                            "type": "integer",
                            "minimum": 0
                          }
                        }
                      }
                    },
                    "cumulative": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": false,
                        "required": [
                          "name",
                          "total"
                        ],
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "total": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              },
              "rotationTotals": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "name",
                    "total"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "total": {
                      "type": "number"
                    }
                  }
                }
              },
              "chosen": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "startUtc",
                  "endUtc"
                ],
                "properties": {
                  "startUtc": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "endUtc": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    }
  }
}