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/readyreturns{ items, page, pageSize, total, totalPages }like every other list endpoint, instead of a bare array. The rows insideitemsare unchanged. pageandpageSizequery parameters.pagedefaults to 1,pageSizeto 20 with a maximum of 100. Loop untilpage >= totalPagesto 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 withPOST /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 theexpensesofPOST /api/v1/travel-entriesstore a fuel, toll, parking, accommodation or other expense without a vehicle, and every read returnsvehicle: nullfor it. AvehicleIdposted 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: nulltoo. - A vehicle outside your company is refused on every expense type. A
vehicleIdthat is not a vehicle of your company returns 400VALIDATION_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 byPOST /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/vehicleswith atypeoutsideCompanyandPersonalreturns 400. A numeric value such as2(the removed public-transport type) used to be stored. It is now rejected with a 400VALIDATION_FAILEDproblem response undererrors.Type, asPUT /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.
ManageVehiclesinCompanyPermissions. 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 everyCompanyAdminnow carries it.ManageCompanyDatakeeps 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}/archiveandPOST /api/v1/vehicles/{id}/unarchivereturn 403 to a caller without it, whatever else the caller holds. Every employee who heldManageCompanyDatabefore this release was givenManageVehicles, so no existing user loses access. - Reading other employees’ private cars needs
ManageVehicles.GET /api/v1/vehiclesandGET /api/v1/vehicles/{id}return other employees’ private cars only to a caller holdingManageVehiclesorEditAllEntries(previouslyManageCompanyData). - Send the whole list back. A client that drops flag names it does not know and
writes the employee’s permissions back removes
ManageVehiclesfrom that employee. Keep unknown names when you round-trip permissions. FleetManagerpreset on input. Permission writes acceptFleetManager, which the server stores asReadAllEntriesplusManageVehicles. Output stays a list of atomic flags.
2026-10-03: engine power on vehicles
Additive contract field; no shape change.
enginePowerKwon 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 and0clears 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-entriesmultipart 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 400VALIDATION_FAILEDproblem response undererrors.attachments, the same shape as any other rejected attachment type.POST /api/v1/travel-entries/{id}/attachmentswith 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 400VALIDATION_FAILEDproblem response undererrors.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 409CONFLICTproblem 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 withisArchived: true. This is the way to archive a vehicle that delete refuses. Archiving an archived vehicle changes nothing;POST /api/v1/vehicles/{id}/unarchivereverses it. SameManageCompanyDatapermission as delete.
2026-09-28: night off-window override deprecated
Deprecated fields; no shape change.
nightOffWindowOverrideandnightOverrideReasonon travel entry allowances. Both fields stay in the response, one element per night, but every element is nowfalseandnull. The override they reported is retired: a night under six hours is answered throughnightAllocationand 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.
fuelUnitPriceOverrideandmileagePetrolGradeon 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.fuelUnitPriceOverrideis 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(Octane95orOctane98) 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 readsnulleverywhere 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.
costCenterIdson 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 asmaxItemson 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.
2026-09-13 — per-diem itinerary amounts on the legal clock
Corrected amounts on existing per-diem itinerary endpoints; no shape change.
- Itinerary per-diem calculation and
from-itinerary. Stoptimevalues 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.
categoryon vehicle update. The field is now optional. Omit it and the stored category is kept; send it when you mean to change it, including0to clear it deliberately. Create requests still require it.
2026-08-23 — engine displacement on vehicles
Additive contract field; no shape change.
engineCapacityCcon 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 and0clears it.
2026-08-17 — locked now reports document locks
Corrected value semantics on an existing response field; no shape change.
lockedon travel-entry detail and list responses previously reported only the legacy pre-2023 import lock and stayedfalsefor documents locked in the app, including the automatic lock applied at payout. It now carries the same derived lock state theentry.updatedwebhook already reports inisLocked:truewhenever 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
departureLocationPlaceIdanddestinationPlaceId(Google Placesplace_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 acceptmanualAdvanceExchangeRatefor 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 withEXCHANGE_RATE_UNAVAILABLE. - Per-diem overrides. The per-diem request accepts
overrideAmountandoverrideReason, 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-ratesnow carries adirectiondiscriminator: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-ratesnow documents the full provider set: HNB for HR, ECB for SI and SK, CNB for CZ. Thesourcefield can be"CNB", andbaseCurrencyis 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, anddinnersProvided(per-meal integer counts) plus an optional per-daymealsProvidedAllocation. They are the canonical write model: the server recomputes the reduction from these counts. The existing v1 aggregatemealsProvidedis 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 derivedmealsProvided(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 gainsadvancePaymentExchangeRateSource, recording where the applied rate came from: a provider code ("HNB","ECB") for an official rate,"MANUAL"for a user-supplied rate, ornullfor 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-keyederrorsbody, 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/computeandPOST /api/v1/per-diems/calculate-simple. Both are subsumed byutilities/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-municipalitiesreturns the SURS municipality list for place dropdowns on Slovenian travel forms. Callable from any tenant. - Employee tax residency — employee records gain a
residencyfield (Resident/NonResident) for Slovenian per-diem and payroll-tax reporting. Defaults toResident.
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.