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 selectbalanced, an optional providerregion, existingapprovedLeave,excludedDatesandlockedBreaks. 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_vacationrequires Premium. So does supplying any ofhorizon,workingDays,companyHolidays,holidayWorkingDates,preferredMonths,minBreakLength,maxBreakLength,preferredBreakLength,maxBreaks,minSeparationDays,seasonalWeightorrequiredSummerBreak.- Working calendar
- The default is Monday–Friday.
workingDaysuses 0 for Sunday through 6 for Saturday. Company holidays and approved leave remove leave cost. A public holiday remains classified as a holiday even whenholidayWorkingDatessays the employee normally works it. Overlapping classifications do not double-charge leave. - Dates and horizon
- All day-level inputs are local
YYYY-MM-DDcalendar 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;
minDaysandmonthscan 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
- Maximum time off: maximize distinct calendar days in qualifying breaks, then used allowance, then preferred-month days, then prefer fewer breaks with stable date ties.
- Longest continuous vacation: prioritize the longest feasible principal break, then maximize calendar days in the remaining compatible breaks.
- Balanced: maximize the engine's declared additive length, preferred-month and seasonal score under the break-count and separation constraints. This is not a universal definition of the best vacation.
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 reportsconstraintsSatisfied: falsewith 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
nullwhen all allowance is used. Otherwiseinfeasible_constraintsidentifies an infeasible request,balanced_objectiveidentifies the explicitly selected balanced score, andno_additional_leave_within_optimal_plan_and_constraintsmeans 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.
totalDaysOffis 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 inleaveDates, 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;
requiresLeavedefines the new leave cost.dateis 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.
- GET /session: returns
authenticatedandpremium. No token gives false/false; an invalid supplied bearer token returns 401. - POST /plans: body
{"input":{…},"title":"My 2027 plan"}. Requires a session. Free users can save basic maximum-time-off or balanced plans. The title is optional and at most 120 characters. Saving generates and stores a new input/result snapshot; infeasible results cannot be saved. - GET /plans: returns up to 30 of the caller's v2 snapshots, newest first. Reading does not recalculate them.
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®ion=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"}}.
- 400 INVALID_INPUT: malformed inputs, unsupported jurisdiction, contradictory interval boundaries or infeasible save.
- 401 AUTH_REQUIRED: authentication needed or supplied session invalid.
- 403 PREMIUM_REQUIRED: current entitlement does not permit the selected strategy, settings or export.
- 404 NOT_FOUND: no exportable snapshot belonging to this account matches the ID.
- 429 RATE_LIMITED: wait the
Retry-Afterdelay. - 503 HOLIDAY_DATA_UNAVAILABLE or SERVICE_UNAVAILABLE: provider or service dependency failure; retry later.
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.