Annual leave optimizer API v2

Calculate vacation plans from an explicit working calendar and leave budget. The core is deterministic; it makes no LLM calls. Version 2 is separate from the existing v1 preview and saved-plan APIs, whose behavior remains unchanged.

Base URL: https://hopladay-backend.onrender.com/api/v2. Send JSON for POST requests. The OpenAPI contract defines every field, response and implemented endpoint.

Calculate without an account

POST /optimize with:

{
  "country": "NO",
  "year": 2027,
  "availableDays": 20,
  "strategy": "maximum_time_off",
  "lang": "en"
}

Required: an existing supported uppercase country code, an integer year from 2024 to 2100, and an allowance from 0 to 366. The default strategy is maximum_time_off; language defaults to en. Omit settings you do not use: supplying a Premium field, even its default value, requires Premium. Unknown top-level fields are rejected. Do not send userId or isPremium as credentials.

Inputs and entitlement

Basic planning
Anonymous and free users can use maximum_time_off (the default) or explicitly select balanced, an optional provider region, existing approvedLeave, excludedDates and lockedBreaks. Approved leave is already off and does not consume the new allowance. Excluded dates block entire recommended intervals, including weekends or holidays on those dates.
Verified Premium planning
longest_vacation requires Premium. So does supplying any of horizon, workingDays, companyHolidays, holidayWorkingDates, preferredMonths, minBreakLength, maxBreakLength, preferredBreakLength, maxBreaks, minSeparationDays, seasonalWeight or requiredSummerBreak.
Working calendar
The default is Monday–Friday. workingDays uses 0 for Sunday through 6 for Saturday. Company holidays and approved leave remove leave cost. A public holiday remains classified as a holiday even when holidayWorkingDates says the employee normally works it. Overlapping classifications do not double-charge leave.
Dates and horizon
All day-level inputs are local YYYY-MM-DD calendar dates. Never convert them between time zones. The default horizon is the whole configured year; an explicit horizon must be an ordered inclusive range within that year. Date arrays contain distinct dates within the horizon and at most 366 items.
Break constraints
For maximum_time_off and longest_vacation, minimum length defaults to 1 calendar day, maximum length and break count to 366, and separation to 1 unselected calendar day. Explicitly selecting balanced retains its score-oriented defaults: minimum length 3, maximum length 21, maximum count 4 and separation 21. For the default maximum-time-off strategy, the selected year and allowance provide the natural bounds; there is no hidden shorter break-length, break-count or spacing cap. Custom limits are Premium settings. Locked intervals must obey these constraints, remain within the horizon, and include adjacent non-working dates. Locking a suggestion unchanged preserves its boundary.
Preferences and summer
Preferred length defaults to 9 days. Preferred months are a soft score preference, not a guarantee. Seasonal weight is 0–10, default 1. A required summer break defaults to at least 14 days entirely in June–August; minDays and months can specify another requirement. An infeasible hard requirement is disclosed rather than silently relaxed.
Language
Use en, no, nl, de, fr, es, sv, da. Suggestion descriptions/reasons and general errors follow the requested language. Machine identifiers, technical objective metadata and validation details can remain English. Holiday names are retained from the provider.

Three explicit objectives

The bounded interval dynamic program uses stable tie-breaking. It considers full contiguous breaks, with at least one new leave day, existing approved leave or an explicit lock; it does not count unrelated naturally free weekends as vacation recommendations. Breaks cannot overlap or touch. The result's optimality, constraints and limitations identify the objective and practical bounds; exactness applies to that defined objective, not to every possible conception of vacation quality.

Understand the result

status and constraintsSatisfied
A valid calculation returns status: "ok" or "infeasible". Infeasibility is still HTTP 200 and reports constraintsSatisfied: false with violation codes. It is not permission to ignore a hard constraint. A feasible request with no qualifying breaks returns an empty suggestion list.
usedDays and remainingDays
Newly required vacation days and the unallocated allowance. Existing approved leave is not charged again. Zero allowance is valid; existing approved leave can still support a zero-cost break.
unusedAllowanceReason
null when all allowance is used. Otherwise infeasible_constraints identifies an infeasible request, balanced_objective identifies the explicitly selected balanced score, and no_additional_leave_within_optimal_plan_and_constraints means more leave cannot be allocated within the optimal plan and its constraints.
totalDaysOff, longestVacation and breakCount
Distinct calendar days within suggested breaks, the longest uninterrupted suggested break, and the number of suggested breaks. totalDaysOff is not all annual weekends and public holidays.
suggestions
Each includes inclusive start/end dates, consecutive calendar-day count, exact new leaveDates, leave cost, public holidays, approved leave, lock state, strategy identifier and explanation. Request only the dates listed in leaveDates, not every date in the full interval.
calendar
One record per horizon date, with working-calendar classification, public/company holiday flags, approved leave, exclusions and recommended leave. Flags can overlap; requiresLeave defines the new leave cost. date is the day identifier; internal ordinal values do not replace it.
source and regions
The provider URL, local retrieval time, jurisdiction scope and chosen region, plus available provider subdivision codes. Nationwide is the default. Unknown regions return 400; a regional holiday is never applied countrywide by default. The source can be coarser than municipal or employer entitlement. Retrieval time is not the date legislation changed.
previewUrl and linkScope
A ready-filled website link containing basic inputs only: country, year, allowance, strategy and optional region. linkScope: "basic-inputs-only" means approved leave, locked periods and other personal date constraints are deliberately absent. The link recalculates current data; it is neither a full private-plan share nor an immutable snapshot.

Authentication and saved snapshots

Successful verified passkey or magic-link authentication returns an opaque sessionToken. Supply it as Authorization: Bearer <sessionToken>. Sessions expire after 14 days, and the server reads current Premium entitlement on each protected request. A user ID or frontend checkbox is not proof of identity. Existing logins without a session token must sign in again for protected v2 actions.

Version 2 uses a separate plan collection. Previously saved legacy plans remain stored without recalculation; reading them requires an authorized session. Fresh anonymous accounts receive secure sessions. An old browserId alone cannot recover an existing account or its plans: use verified account login. The legacy /api/users/claim endpoint is retired and returns HTTP 410. Editing inputs or regenerating a preview does not mutate a saved snapshot.

Premium iCalendar export

POST /plans/{id}/export with {"mode":"leave"} or {"mode":"breaks"}. This requires a verified Premium owner and an existing saved v2 plan. Other accounts cannot export it. The public raw calculation does not grant access to this paid feature.

leave emits only newly required leave dates; breaks emits full vacation intervals. Events use all-day VALUE=DATE, an exclusive DTEND, no timezone shift, and tentative status. Downloading or importing an event does not formally request or approve leave with an employer. Existing paid capabilities and checkout entitlements remain unchanged.

Holiday catalog

GET /holidays/2027?country=DE&region=DE-BY returns applicable holidays, available provider regions and source metadata. Omit region for nationwide holidays. Unknown query fields are rejected. The validated provider response is shared across region selections, cached for 24 hours, and bounded to 128 country-year entries per process. Identical in-flight country-year requests share one fetch. The provider request has an eight-second timeout. Failures and expired data do not silently become stale success responses.

Opt-in aggregate analytics

After explicit client opt-in, POST /events with exactly one field, for example {"event":"generated"}. Allowed values are opened, generated, strategy_selected, recommendation_accepted, regenerated, saved, export_requested, upgrade_initiated and purchase_completed. The server stores UTC daily aggregate counts, not individual vacation dates or identities. Do not send settings, dates, user IDs, URLs or extra fields. A purchase event is an analytics count, not proof of payment.

Errors and request budgets

Error shape: {"error":{"code":"INVALID_INPUT","message":"…","details":"optional technical detail"}}.

All v2 routes share 30 requests per client and 300 requests globally in a fixed 60-second window per process. Rate-limit responses currently set Retry-After: 60. Proxies can make clients share an IP limit, and multi-instance deployments need edge controls for a fleet-wide budget. Responses use Cache-Control: no-store and X-Robots-Tag: noindex. Publishing an OpenAPI file does not automatically install a tool in an assistant; the assistant needs HTTP capability or an explicit integration.

Unusually complex custom constraints can return HTTP 503 with OPTIMIZATION_CAPACITY_EXCEEDED. Shorten the horizon or relax a break constraint. This is a calculation limit; no partial plan is presented as optimal. Unrestricted free defaults avoid the expensive count dimension.