Changelog

2026-10-07: GET /api/v1/payouts/ready is paginated

Breaking shape change on an existing endpoint.

  • The response is the standard paged envelope. GET /api/v1/payouts/ready returns { items, page, pageSize, total, totalPages } like every other list endpoint, instead of a bare array. The rows inside items are unchanged.
  • page and pageSize query parameters. page defaults to 1, pageSize to 20 with a maximum of 100. Loop until page >= totalPages to read every entry ready for payout.
  • Read every page before you pay. Paid entries drop out of the list, so later pages move up while you pay. Read all pages first, de-duplicate by id, then create the payout with POST /api/v1/payouts. Paying page by page skips entries.
  • Order is unchanged. Most recently approved entries come first.

2026-10-05: vehicleId on non-mileage expenses is ignored

Behavior change on existing endpoints; shape unchanged.

  • Only mileage expenses keep a vehicle. POST /api/v1/travel-entries/{id}/expenses, PUT /api/v1/travel-entries/{id}/expenses/{expenseId} and the expenses of POST /api/v1/travel-entries store a fuel, toll, parking, accommodation or other expense without a vehicle, and every read returns vehicle: null for it. A vehicleId posted on such an expense used to be stored and echoed back.
  • Existing values are cleared. On upgrade, the vehicle stored on existing non-mileage expenses is removed, so they read back vehicle: null too.
  • A vehicle outside your company is refused on every expense type. A vehicleId that is not a vehicle of your company returns 400 VALIDATION_FAILED, also on a non-mileage expense, which the per-expense endpoints used to store unchecked. An archived company car on a non-mileage expense is no longer refused by POST /api/v1/travel-entries; it is ignored like any other vehicle there.

2026-10-03: vehicle create rejects an unknown type

Behavior change on an existing endpoint; shape unchanged.

  • POST /api/v1/vehicles with a type outside Company and Personal returns 400. A numeric value such as 2 (the removed public-transport type) used to be stored. It is now rejected with a 400 VALIDATION_FAILED problem response under errors.Type, as PUT /api/v1/vehicles/{id} already did.

2026-10-03: new ManageVehicles permission, vehicle writes require it

Additive enum value and permission changes on existing vehicle endpoints; shapes unchanged.

  • ManageVehicles in CompanyPermissions. A new atomic flag for running the company’s cars: the cars themselves, their costs, documents, due dates and reminder mail. Employee responses and webhook payloads list it like any other flag, and every CompanyAdmin now carries it. ManageCompanyData keeps departments, cost centers and exchange rates and no longer covers vehicles.
  • Vehicle writes require ManageVehicles. POST /api/v1/vehicles, PUT /api/v1/vehicles/{id}, DELETE /api/v1/vehicles/{id}, POST /api/v1/vehicles/{id}/archive and POST /api/v1/vehicles/{id}/unarchive return 403 to a caller without it, whatever else the caller holds. Every employee who held ManageCompanyData before this release was given ManageVehicles, so no existing user loses access.
  • Reading other employees’ private cars needs ManageVehicles. GET /api/v1/vehicles and GET /api/v1/vehicles/{id} return other employees’ private cars only to a caller holding ManageVehicles or EditAllEntries (previously ManageCompanyData).
  • Send the whole list back. A client that drops flag names it does not know and writes the employee’s permissions back removes ManageVehicles from that employee. Keep unknown names when you round-trip permissions.
  • FleetManager preset on input. Permission writes accept FleetManager, which the server stores as ReadAllEntries plus ManageVehicles. Output stays a list of atomic flags.

2026-10-03: engine power on vehicles

Additive contract field; no shape change.

  • enginePowerKw on vehicles. Vehicle create and update requests accept an optional maximum engine power as a whole number of kilowatts (field P.2 of the registration document, 1 to 2000), and both the vehicle detail and list responses return it. On update, omitting it keeps the stored value and 0 clears it.

2026-10-03: corrupt image bytes on multipart attachment uploads return 400

Behavior change on two existing endpoints; shape unchanged.

  • POST /api/v1/travel-entries multipart attachment with corrupt image bytes returns 400. An attachment whose declared type is an image but whose bytes do not decode as one used to fail with a 500. It is now rejected with a 400 VALIDATION_FAILED problem response under errors.attachments, the same shape as any other rejected attachment type.
  • POST /api/v1/travel-entries/{id}/attachments with the same corrupt image also returns 400. The attachment endpoint used to fail the same way with a 500. It is now rejected with a 400 VALIDATION_FAILED problem response under errors.File, the same shape as any other rejected attachment type.

2026-10-02: vehicle delete refuses a vehicle in use, new archive endpoint

Behavior change on an existing endpoint and one new endpoint; existing shapes unchanged.

  • DELETE /api/v1/vehicles/{id} returns 409 for a vehicle in use. A vehicle that appears on a travel entry expense or has a recorded cost is no longer archived in place of the delete. Deleted travel entries and removed expenses or costs do not count, so moving the vehicle off its expenses and deleting its costs frees it. The request is rejected with a 409 CONFLICT problem response and the vehicle is left as it was. A vehicle with no such records, archived or not, is still deleted with a 204.
  • POST /api/v1/vehicles/{id}/archive. Archives a vehicle, in use or not, and returns it with isArchived: true. This is the way to archive a vehicle that delete refuses. Archiving an archived vehicle changes nothing; POST /api/v1/vehicles/{id}/unarchive reverses it. Same ManageCompanyData permission as delete.

2026-09-28: night off-window override deprecated

Deprecated fields; no shape change.

  • nightOffWindowOverride and nightOverrideReason on travel entry allowances. Both fields stay in the response, one element per night, but every element is now false and null. The override they reported is retired: a night under six hours is answered through nightAllocation and no override changes any figure. Stop reading them; they may be removed in a later version.

2026-09-28: fuel pricing inputs on mileage expenses

Additive contract fields; no shape change.

  • fuelUnitPriceOverride and mileagePetrolGrade on expenses. Expense create and update requests accept both, including expenses nested in a travel entry create, and every expense response returns them, inline on a travel entry too. fuelUnitPriceOverride is a receipt-proven fuel price per litre (per kWh for an electric vehicle) in the company’s base currency; on a CZ or SK mileage row it replaces the statutory average in the fuel component. mileagePetrolGrade (Octane95 or Octane98) selects the CZ Natural 95 or Natural 98 statutory average for an own petrol vehicle without a receipt price. Each is stored only where it prices the row and reads null everywhere else. An update is a full replace: omitting them reprices the row at the statutory average, Natural 95 for petrol, so send back the values the expense returned to keep a receipt price or a 98 grade.

2026-09-16 — cost centers on employees

Additive contract field; no shape change.

  • costCenterIds on employees. The employee detail and list responses return the cost centers that employee may charge, and employee create and update requests accept them. An empty array means unrestricted — every active cost center of the company is available. On create, omitting the field is the same as sending an empty array. On update, omitting it keeps the stored list, an empty array clears it, and a non-empty array replaces it wholesale. Every id must belong to your own company, otherwise the request is rejected with a 400. Adding a cost center outside a non-empty list to a travel entry is still rejected with a 400 — that rejection is why the field exists. The restriction applies to additions only: allocations already on an entry are not removed retroactively when a cost center leaves the list. A safety bound of 100 distinct cost centers per employee applies to the write paths — duplicates count once, and the schema publishes the bound as maxItems on both request bodies. It bounds request size rather than expressing a business limit: an allowlist can only name cost centers the company actually has, so it will not constrain real use.

Corrected amounts on existing per-diem itinerary endpoints; no shape change.

  • Itinerary per-diem calculation and from-itinerary. Stop time values are still read as UTC, and returned windows are still UTC. Czech and Slovak companies now split calendar days at midnight in Prague or Bratislava rather than at UTC midnight. Polish companies count nights (21:00–07:00), doby and bill dates on Warsaw time. The same request can therefore return different hours and amounts than before for these markets.

2026-09-12 — category is optional on vehicle update

Relaxed field on an existing request; no shape change.

  • category on vehicle update. The field is now optional. Omit it and the stored category is kept; send it when you mean to change it, including 0 to clear it deliberately. Create requests still require it.

2026-08-23 — engine displacement on vehicles

Additive contract field; no shape change.

  • engineCapacityCc on vehicles. Vehicle create and update requests accept an optional engine displacement as a whole number of cubic centimetres, and both the vehicle detail and list responses return it. On update, omitting it keeps the stored value and 0 clears it.

2026-08-17 — locked now reports document locks

Corrected value semantics on an existing response field; no shape change.

  • locked on travel-entry detail and list responses previously reported only the legacy pre-2023 import lock and stayed false for documents locked in the app, including the automatic lock applied at payout. It now carries the same derived lock state the entry.updated webhook already reports in isLocked: true whenever the document is closed to writes. Read it as “this entry cannot be modified”, not as a marker of a specific lock origin.

2026-07-13 — place ids, manual FX rates, per-diem overrides, exchange-rate direction

Additive, backward-compatible contract fields plus a new exchange-rate discriminator.

  • Place ids on travel entries. Create and update requests accept departureLocationPlaceId and destinationPlaceId (Google Places place_id), and the travel-entry response returns them. Echo the stored values back on a full-replace update to preserve place identity when you edit destination text.
  • Destination country on travel entries. Requests accept an optional destinationCountryCode (ISO 3166-1 alpha-2). When supplied it is stored directly and the server-side geocoding fallback is skipped.
  • Manual exchange rates. Expense and per-diem requests accept an optional manualExchangeRate, and travel-entry requests accept manualAdvanceExchangeRate for the advance. Each is honored only when the FX provider publishes no rate for the referential date; supplying one while an official rate exists is rejected with EXCHANGE_RATE_UNAVAILABLE.
  • Per-diem overrides. The per-diem request accepts overrideAmount and overrideReason, and the per-diem read models return them. To preserve an existing override on a full-replace update, echo the values back; omitting them clears the override and reverts to the statutory payout.
  • Exchange-rate direction. GET /api/v1/reference/exchange-rates now carries a direction discriminator: ForeignPerBase (HNB / ECB) means divide the rate to convert to the base currency, BasePerForeign (CNB) means multiply.

2026-07-08 — advance-payment date, CZ/SK exchange-rate provider

Two additive, backward-compatible changes.

  • Advance-payment date on travel entries. Create and update requests now accept an optional advancePaymentDate (yyyy-MM-dd), and the travel-entry response returns it. When omitted on create the server defaults it to the entry date. It sets the FX date for the advance and, for Czech tenants, the per-diem FX referential date.
  • Exchange-rate provider for CZ/SK. GET /api/v1/reference/exchange-rates now documents the full provider set: HNB for HR, ECB for SI and SK, CNB for CZ. The source field can be "CNB", and baseCurrency is the provider’s native base (EUR for HNB/ECB, CZK for CNB). Behavior is unchanged; the docs now match runtime.

2026-06-11 — per-meal counts, FX provenance, exchange-rate-unavailable code

Three additive surface changes. All are backward compatible: existing payloads keep working unchanged, and the new response fields are additions you can ignore until you need them.

  • Per-meal counts on per-diem create/update. Per-diem requests now accept breakfastsProvided, lunchesProvided, and dinnersProvided (per-meal integer counts) plus an optional per-day mealsProvidedAllocation. They are the canonical write model: the server recomputes the reduction from these counts. The existing v1 aggregate mealsProvided is still accepted; when the per-meal fields are absent the server maps the aggregate as before, so legacy clients are unaffected. Responses keep returning the derived mealsProvided (lunch + dinner, the HR Pravilnik shape) alongside the per-meal counts, so read paths are unchanged.
  • FX provenance on responses. Expense and per-diem responses gain exchangeRateSource, and the travel-entry response gains advancePaymentExchangeRateSource, recording where the applied rate came from: a provider code ("HNB", "ECB") for an official rate, "MANUAL" for a user-supplied rate, or null for a same-currency row (no conversion) or a legacy row saved before provenance tracking. Read-only; no request change.
  • New error code EXCHANGE_RATE_UNAVAILABLE (HTTP 400). Fires when the tenant’s FX provider publishes no rate for a currency on the requested date and no historical rate exists to fall back on. It is a validation-family failure with the standard field-keyed errors body, so you can discriminate the FX-unavailable case specifically and prompt for a manual rate. Added to the errors catalog.

2026-05-23 — per-diem itinerary times stored as true UTC (fix)

Per-diem times persisted via POST /api/v1/travel-entries/{id}/per-diems/from-itinerary are now stored as true UTC instants. Previously each leg’s departure/arrival was written using its wall-clock value tagged as UTC without shifting (an 08:00 local leg was stored as 08:00Z instead of 07:00Z/06:00Z), so itinerary-generated rows disagreed with manually-added rows by 1–2 hours and displayed late. The request and response schemas are unchanged, but the departureTime/arrivalTime values this endpoint returns (and that later reads of the same rows return) now reflect the corrected UTC instant. The stateless calculator preview still echoes wall-clock times. Per-diem amounts are unaffected.

2026-05-23 — per-diem calculator + whole-trip persist

The per-diem calculation surface is consolidated and gains whole-trip persist parity with the locco web app.

  • New POST /api/v1/utilities/per-diems/calculate — a stateless calculator. Send an itinerary (one leg = a 2-stop itinerary) and get the computed rows back, with itinerary-level redistribution applied. No entry context, nothing persisted.
  • New POST /api/v1/travel-entries/{id}/per-diems/from-itinerary — rebuild a travel entry’s per-diems from an itinerary in one call. The server re-runs its own engine over the stops (submitted amounts are never trusted), applies per-leg meals keyed by (countryCode, departureTime) (the pairs the calculator returns), then atomically replaces the entry’s per-diems. This reproduces itinerary-level redistribution and domestic-unit upgrades that posting rows one at a time cannot.
  • Removed POST /api/v1/travel-entries/{id}/per-diems/compute and POST /api/v1/per-diems/calculate-simple. Both are subsumed by utilities/per-diems/calculate — a single leg is just a 2-stop itinerary. Move integrations to the new calculator.

2026-05-22 — Slovenian market support

Initial support for Slovenian tenants. Update your client from the refreshed OpenAPI snapshot at /api-reference/:

  • Slovenian municipalities — new GET /api/v1/reference/slovenian-municipalities returns the SURS municipality list for place dropdowns on Slovenian travel forms. Callable from any tenant.
  • Employee tax residency — employee records gain a residency field (Resident / NonResident) for Slovenian per-diem and payroll-tax reporting. Defaults to Resident.

2026-04-28 — v1.0 launching

The first stable release of the locco partner API. Every surface is wired, typed, and covered by the OpenAPI snapshot at /api-reference/:

  • Travel entries — full CRUD plus the workflow transitions (submit, approve, reject, cancel, request-changes), with atomic creates that accept nested expenses, per-diems, cost-center allocations, and attachments in a single call.
  • Attachments — direct upload to a travel entry via POST /api/v1/travel-entries/{id}/attachments. Multipart, up to 5 files per request.
  • Webhooks — subscription registration with one-time signing-secret exposure, delivery history per subscription, and retry endpoint for failed or exhausted deliveries.
  • Payouts — list ready-for-payout entries, create batches, list history, delete a batch.
  • Employees, Departments, Vehicles, Cost centers — CRUD + list, with custom-field values on employees.
  • Reference — Croatian municipalities, HNB exchange rates, per-diem rate table, the company’s enabled-country and enabled-currency allow-lists.
  • Company — profile read.

Enum values for Status, TravelType, PayoutMethod, and the rest are documented in the OpenAPI schema as enum: [...] allow-lists, so generated clients pin against the canonical strings ("PendingApproval", "BankTransfer", etc.) rather than opaque values.