Skip to main content

Errors & response conventions

The Loca.fi API predates today's REST conventions in a few places, and it is consistent about its own rules — but those rules are not always the ones you would guess. This page is the single source of truth for how responses behave platform-wide. Guides link here instead of re-explaining error handling.

Status codes at a glance

CodeMeaning on this API
200Success — and also some failures: a handful of workflow operations return 200 with the failure described in the body. See 200-despite-failure.
204Success with no body — returned by actions that have nothing to return.
400Validation failure and not-found. This API never returns 404 for a missing resource.
401Not authenticated — missing/invalid token, or the wrong auth scheme (Bearer instead of Token). Body is empty.
402Payment Required — the endpoint is gated on a module licence your organisation doesn't hold.
403Authenticated but not permitted — you lack the module permission (e.g. no Update on ReferenceData).
412Precondition Failed — your password has expired. Change it, then log in again.
500Unhandled server error — notably, OData filters using functions the database layer can't translate (e.g. matchesPattern).

Success responses

  • Create / Update return the Detail DTO of the affected row (not a bare id, not the request echoed back). EntityVersionNo increments on every update.
  • Delete returns the literal JSON boolean true.
  • Actions with nothing to return respond 204 No Content.
  • CSV exports are always application/octet-stream with attachment name Export.csv — never text/csv.

Validation failures — the error array

Every service-level validation failure is HTTP 400 with a JSON array of error objects:

[
{
"ErrorCode": "InvalidOperation",
"Message": "Name is already in use",
"Field": "Name"
}
]

Two things to know:

  1. ErrorCode frequently collapses to "InvalidOperation" regardless of the underlying cause — the Message field carries the useful signal. Match on Message text, not ErrorCode, when you need to distinguish failure causes.
  2. In a few endpoints the descriptive text lands in Field instead of Message (e.g. the open-stocktake uniqueness check on POST api/inventories/createinventory). If Message looks generic, check Field too.

Not-found is 400, never 404

The platform's not-found convention differs by operation shape:

OperationResponse for an unknown id
Get by id400 with an empty array body: []
Update / Delete400 with [{"ErrorCode": "NotFound", "Message": "the resource does not exist"}]

Practical consequence: on Get-by-id endpoints a client cannot distinguish "doesn't exist" from a generic validation failure — treat 400 [] as not-found.

Authentication failures

The auth scheme is Authorization: Token <jwt> — a Bearer-style JWT under the custom keyword Token, not Bearer. All of these return 401 with an empty body:

  • No Authorization header
  • An expired or malformed token
  • The right token under the wrong keyword (Bearer <jwt>)

Authenticated-but-unpermitted is 403 (module permission missing), and licence-gated endpoints return 402 when the organisation's licence lacks the module. Expired passwords surface as 412 on login — the client must call the change-password endpoint and log in again.

OData endpoints

List endpoints accepting OData ($filter, $orderby, $top, $skip, $count) have two failure modes of their own:

  • Invalid OData syntax → a bare 400 with no body (unlike service-level 400s, there is no error array).
  • Untranslatable functions500. The filter is composed into the SQL query, so any OData function the database provider can't translate (e.g. matchesPattern, which maps to a .NET regex) fails server-side. Stick to eq/ne/gt/lt/ge/le, and/or/not, contains, startswith, endswith, and date/arithmetic operators.

The 200-despite-failure family

A small family of workflow-transition operations returns HTTP 200 even when the transition fails, describing the outcome in the body instead. Always check the body, not just the status code, on:

  • Order processing (dispatchorder, receiveorder, completeorder, …) — a failed stage gate returns 200 with the order's OrderState unchanged and the blocking problems listed in the order's error collections. Diff the state you got back against the state you expected.
  • SKU updates (updatesku) — a rejected lifecycle transition returns 200 with a LifecycleTransitionRejected cascading-update marker, and the other field changes still commit.
  • Bulk upserts (BulkUpsert*) — always 200; success/failure is per row in the response (Created / Updated / Unchanged / Failed statuses). A batch containing failed rows does not fail the request.

Asynchronous operations

Long-running operations (order processing, stocktake resolution, and similar) accept a NotifyOnceComplete flag. When used, the endpoint returns 200 immediately with a BackgroundTaskId in the result DTO — the work continues in the background and the immediate response body is a placeholder, not the final state. Poll the background-task endpoints (or subscribe to the completion events) to learn the outcome. A dedicated Async operations concepts page covers the full pattern.