Errors return JSON in this shape:
error.code is stable and machine-readable. Domain failures use specific codes where the client can take different action, for example invalid_credentials, invoice_already_generated, payment_exceeds_invoice_balance, payment_already_reversed, organization_suspended, and room_has_active_lease. Login credential failures for POST /api/v1/auth/login and POST /api/v1/tenant/auth/login return HTTP 401 with error.code = "invalid_credentials". Missing or invalid bearer-token authentication on protected routes continues to use unauthorized.

Localization

error.code (top-level) and the validation fields[].path / fields[].code are identical across locales — branch on those, never on message text. The human-readable error.message is localized to the request’s Accept-Language header. Supported locales are vi (default) and en; when no usable Accept-Language is sent, the API responds in Vietnamese. The negotiated locale is returned in the Content-Language response header. A code with no catalog entry falls back to its English message. The JSON examples on this page show the en text for readability. Common status codes:
  • 400 bad request
  • 401 unauthorized
  • 403 forbidden
  • 404 not found
  • 405 method not allowed
  • 409 conflict
  • 415 unsupported media type
  • 429 too many requests
  • 500 internal error