HTTP API
The package ships opt-in JSON endpoints per guard. Register them with routes.enabled = true for the guard, or explicitly:
// routes/api.php
use RoundlyConsulting\Auth\Facades\Authentication;
Authentication::routes('users'); // defaults
Authentication::routes('clients')
->prefix('api/clients/auth')
->name('clients.auth.')
->middleware(['api'])
->authenticatedMiddleware(['authentication.active'])
->except(['registration', 'invitations.manage']);RouteRegistrar offers prefix(), name(), middleware(), authenticatedMiddleware(), only() and except(); it registers on destruct. Defaults: prefix {guard}/auth, names authentication.{guard}.*, middleware api. Registering a guard twice throws. Groups for only() / except(): login, challenge, tokens, registration, invitations, passwords, email, account, sessions, two-factor, passkeys, invitations.manage.
Endpoints
Every route carries authentication.guard:{guard} and Cache-Control: no-store, and exists only when its feature is enabled for the guard (a disabled feature is a 404). Tokens always travel in request bodies (JSON). * = authenticated (auth:{laravel_guard}); ? = optional.
| Method | URI (under the prefix) | Body | Purpose |
|---|---|---|---|
| POST | login | identifier, password | Password login — identifier is any of identifier.columns (the email by default); there is no email field. |
| POST | login/magic-link | email | Request a magic link. |
| POST | login/magic-link/consume | token | Sign in with the link’s token. |
| POST | login/otp | email | Request an email code. |
| POST | login/otp/verify | email, code | Sign in with the code. |
| POST | login/passkey/options | — | Passwordless passkey options. |
| POST | login/passkey | credential | Passwordless passkey login. |
| POST | challenge/two-factor | challenge_token, code, method? (totp | recovery_code) | TOTP or recovery code (either kind is accepted under totp). |
| POST | challenge/two-factor/enrol | challenge_token | Start the forced TOTP enrolment (secret, QR code, recovery codes). |
| POST | challenge/two-factor/enrol/confirm | challenge_token, code | Confirm it. |
| POST | challenge/passkey/options | challenge_token | Passkey second-factor options. |
| POST | challenge/passkey | challenge_token, credential | Passkey second factor. |
| POST | challenge/passkey/enrol/options | challenge_token | Forced passkey enrolment options. |
| POST | challenge/passkey/enrol | challenge_token, credential, name? | Forced passkey enrolment. |
| POST | refresh | refresh_token | Rotate the refresh token (the previous access token stops working). |
| POST | register | email, password (when required), attributes? | Registration (attributes = the host fields of registration.rules). |
| POST | invitations/preview | token | What an invitation is for. |
| POST | invitations/accept | token, password (when required), email? (only when lock_email is off), attributes? | Accept an invitation. |
| POST | password/forgot | email | Request a reset link. |
| POST | password/reset | token, password | Reset the password. |
| POST | email/verify | token, email (with the code channel) | Verify an address — token is the link token or the code. |
| POST | email/verification/resend | email | Resend verification (guest). |
| POST | email/change/confirm | token | Confirm an email change (opened from the mail). |
| GET* | me | — | The account. |
| PATCH* | locale | locale?, timezone? | Locale / timezone. |
| POST* | reauthenticate | method + password | code | credential | Re-authentication — method is password, totp, recovery_code, email_otp or passkey. |
| POST* | reauthenticate/passkey/options, reauthenticate/otp | — | Passkey options / mail a re-authentication code. |
| GET* | activity | query per_page? | Own login activity. |
| POST* | logout, logout/others, logout/everywhere | — | Logouts. |
| GET* / DELETE* | sessions, sessions/{session} | — | Device sessions. |
| PUT* | password | current_password (when the account has one), password | Change password. |
| POST* | email/change | email | Request an email change. |
| POST* | email/verification | — | Send verification. |
| GET* POST* DELETE* | two-factor, two-factor/recovery-codes | — | Status, start enrolment, disable, regenerate recovery codes. |
| POST* | two-factor/confirm | code | Confirm the enrolment. |
| GET* POST* | passkeys, passkeys/options | passkeys: credential, name? | List, registration options, register. |
| PATCH* / DELETE* | passkeys/{passkey} | PATCH: name | Rename / remove. |
| GET* | invitations | query status?, per_page? | Invitation admin (Gate ability). |
| POST* | invitations | email, payload?, locale?, ttl? (seconds), send? (default true) | Create — 201 with data and the link as url (shown once; with send: false nothing is mailed and you deliver it). |
| POST* / DELETE* | invitations/{invitation}/resend, invitations/{invitation} | — | Resend ({"status": "sent", "url": …}) / revoke. |
Every endpoint that signs in also takes device_name? (shown in the session list); register and invitations/accept also take timezone? (stored on the new account). The device is read from the X-Device-Id header (activity.new_device.header), the locale from X-Locale / Accept-Language. credential is the browser’s PublicKeyCredential JSON with base64url members, plus the ceremonyId of the options it answers.
Responses
// 200 — authenticated
{ "status": "authenticated", "token_type": "Bearer", "access_token": "eyJ…", "expires_in": 900,
"expires_at": "2026-09-26T10:15:00Z", "refresh_token": "…", "refresh_expires_at": "2026-10-26T10:00:00Z",
"session_id": "0199…" }
// 200 — challenge
{ "status": "challenge", "challenge_token": "…", "expires_at": "…", "attempts_left": 5, "method": "password",
"completed": [], "remaining": [ { "step": "second_factor", "methods": ["totp", "recovery_code", "passkey"] } ] }
// 202 — enumeration-safe acknowledgement (magic link, email code, forgot, resend, email change)
{ "status": "sent" }
// error
{ "message": "…", "code": "invalid_credentials", "errors": { "identifier": ["…"] } }Credential-change responses (PUT password, POST two-factor/confirm, DELETE two-factor, POST two-factor/recovery-codes, POST passkeys, DELETE passkeys/{passkey}) include tokens or null. When non-null the client must swap to it immediately — its previous access token died with the change.
Errors
| Code | Status | When |
|---|---|---|
invalid_credentials | 422 | Wrong credentials (uniform). |
invalid_token / invalid_code | 422 | An emailed link or code is invalid (uniform). |
challenge_invalid / factor_failed / factor_not_allowed | 422 | Challenge problems (attempts_left on factor_failed). |
invalid_invitation | 422 | An invitation is invalid (uniform). |
passkey_registration_failed | 422 | A passkey did not register. |
refresh_invalid | 401 | The refresh token is invalid (uniform). |
account_disabled / email_not_verified / enrolment_required / reauthentication_required | 403 | Account state or policy (methods on reauth). |
registration_closed / invitation_required / login_denied | 403 | Registration or risk. |
account_locked | 423 | A re-authenticating account is locked. |
too_many_attempts | 429 | Throttled or hard-locked (Retry-After). |
two_factor_required / last_credential / two_factor_already_enabled / two_factor_not_enabled | 409 | Refused changes. |
method_disabled / not_found | 404 | Feature off or unknown id. |
| a validation error on password | 422 | Breached-password service down with fail_closed. |
misconfigured | 500 | Configuration error (detail only with app.debug). |
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.