The V2 auth endpoints are how partner integrations exchange an email + password for a Sanctum personal-access token. They are the only V2 routes that do not require an Authorization header.
Log in
POST /api/v2/login
Validates the credentials against Laravel's auth guard. On success it issues a Sanctum personal-access token via auth()->user()->createToken('authToken')->plainTextToken and returns the user record alongside the plaintext token.
Body parameters
Validated inline in App\Http\Controllers\Api\V2\AuthController::login() (not via a Form Request):
| Field | Type | Rules | Description |
|---|---|---|---|
email |
string | required, email | The user's email address. |
password |
string | required | The user's password. |
Responses
200—{ "user": { ... }, "access_token": "..." }. SendAuthorization: Bearer {access_token}on subsequent V2 requests.401 Unauthorized—{ "message": "Invalid Credentials" }whenauth()->attempt()fails.422 Unprocessable Entity— standard Laravel validation error envelope whenemailorpasswordis missing or malformed.
Register
POST /api/v2/register
RegisterRequest uses the App\Http\Requests\Api\V2\Concerns\DeniesAccess trait, so the controller is never reached: the request authorisation always fails and the endpoint returns 403 Forbidden with body { "message": "Forbidden" }. User registration must be performed inside FinDesk's web app.
V2 input normalisation (NormalizeApiInput)
Every authenticated V2 request (everything except POST /api/v2/login and POST /api/v2/register) passes through App\Http\Middleware\NormalizeApiInput after auth:sanctum and the api_version:v2 middleware.
The middleware recursively walks the request body ($request->all()) and converts string keys to snake_case via Str::snake(). URL parameters and headers are left untouched.
In practice this means:
FirstName,firstName, andfirst_nameall reach the validator asfirst_name.- Nested objects are normalised at every depth — a
Contact.PhoneNumbers[0].PhoneTypekey in the body becomescontact.phone_numbers.0.phone_type. - Order matters when casings collide. If both
personNameandperson_nameappear in the same payload, whichever comes later in the JSON wins, because both normalise toperson_name. Servers generally append the snake-case variant last, so the snake form typically wins in practice.
This middleware exists so partner integrations that emit camelCase or PascalCase JSON can talk to V2 without rewriting their serialisers. The V1 routes do not include it — see Versioning for the full list of V1 → V2 changes.
V2 differences — V2 wraps every authenticated request with
NormalizeApiInput; V1 does not. The login endpoint itself is unauthenticated, so it bypasses the middleware and consumespasswordas-is. The register endpoint is wired but disabled byDeniesAccess.