Agrofin

Getting started

On this page 6

Base URL: /api/v1. All requests and responses are JSON — send Accept: application/json.

Response envelope

Every response uses one envelope:

json
{
  "status": "ok",
  "message": "Success",
  "data": {},
  "errors": null,
  "meta": null
}
  • status is the string "ok" (HTTP < 400) or "error" (HTTP >= 400) — not the HTTP code.
  • message is localized (see Localization below).
  • errors and meta are null when empty and an object when filled — they never arrive as [], so the type never flips.
  • data is the exception: it stays []/{} when empty, so an empty list is still iterable.
  • Validation failures (422) put field errors under errors:
json
{
  "status": "error",
  "message": "The given data was invalid",
  "data": [],
  "errors": { "email": ["The email field is required."] },
  "meta": null
}

Authentication

Stateless Bearer tokens (Laravel Sanctum). Tokens expire after 5 days.

Public registration was removed: POST /api/v1/register no longer exists. Accounts are created by an admin instead (planned for a later phase).

Endpoint Auth Notes
POST /api/v1/login email, password; returns user + token; throttled 6/min
POST /api/v1/logout Bearer revokes the current token
GET /api/v1/user Bearer current profile
PUT /api/v1/user Bearer updates name, email and password — see the policy below

Send the token as Authorization: Bearer <token>. Inactive accounts are rejected at login exactly like bad credentials (401, no account enumeration). A token issued with an explicit future expires_at (machine/service tokens) outlives the global 5-day TTL; revoking it still takes effect immediately.

Every login attempt through this endpoint is written to the audit log, the same as an admin cookie login.

Password policy

Every password the API accepts is validated against Password::defaults(), configured once in AppServiceProvider::boot() and applied wherever a password is set (currently PUT /api/v1/user):

Rule Requirement
Length at least 12 characters
Case at least one uppercase and one lowercase letter
Digits at least one number
Symbols at least one symbol
Breach check rejected if the password appears in the Have I Been Pwned corpus (k-anonymity: only a 5-character hash prefix leaves the server, never the password)

A password failing any of these returns 422 with the reasons under errors.password. The breach check is a network call — if HIBP cannot be reached, Laravel skips that single rule and the other four still apply.

Rate limits

Two buckets, both per minute, both reported via X-RateLimit-Limit / X-RateLimit-Remaining:

Traffic Key Default
Anonymous (no user, no bearer token) client IP 60/min
App (authenticated or bearer-carrying) user id, else IP 600/min

POST /api/v1/login carries an extra route limit of 6/min. Exceeding any bucket returns 429.

Localization

Send Accept-Language: uz, ru or uz_cyrl — the response message values follow it and the response carries Content-Language. Unsupported locales fall back to uz. The Cyrillic tag is accepted in any casing (uz_cyrl, uz-cyrl, uz-Cyrl, UZ-CYRL) and always answers as uz_cyrl.

Errors

Every error response under /api/* uses the envelope above as JSON — this holds regardless of whether the client sends an Accept: application/json header.

HTTP When
401 Missing/invalid/expired token
403 Authenticated but not allowed
404 Unknown route or model
422 Validation failed (errors filled)
429 Rate limit exceeded
500 Server error (message is generic in production)