Your Laravel API works.
Your credentials are correct.
/sanctum/csrf-cookie might even return 204.
Then you send the login request:
POST /login
and Laravel responds with:
419 Page Expired
If you've been changing random CORS settings hoping the error disappears, stop there.
A Laravel Sanctum 419 error is usually a CSRF/session authentication problem, and the fastest way to fix it is to identify exactly where the Sanctum authentication flow breaks.
Here's the debugging order I use.
TL;DR
For Laravel Sanctum SPA authentication, verify these five things:
1. Stateful middleware
2. SANCTUM_STATEFUL_DOMAINS
3. CORS credentials
4. Session cookies
5. Axios CSRF handling
And always request:
GET /sanctum/csrf-cookie
before:
POST /login
If one piece of that chain is wrong, Laravel can respond with 419 CSRF token mismatch / Page Expired.
1. Understand What 419 Actually Means
When using Laravel Sanctum's SPA authentication, Laravel relies on normal session cookies and CSRF protection.
The browser flow looks roughly like this:
Frontend
|
| GET /sanctum/csrf-cookie
v
Laravel
|
| Sets XSRF-TOKEN
| Sets session cookie
v
Browser
|
| POST /login
| Cookie + X-XSRF-TOKEN
v
Laravel validates request
When Laravel cannot validate that CSRF/session relationship, you can end up with:
419 Page Expired
So the question isn't simply:
"Is my CORS config correct?"
The better question is:
"At which point does the authentication chain break?"
2. Start With the Browser Network Panel
Before touching .env, Axios, or Laravel configuration, open:
Chrome DevTools → Network
Then perform the login again.
Find the first request that doesn't behave as expected.
Case A: OPTIONS fails
If the browser blocks an OPTIONS request, investigate CORS first.
Case B: /sanctum/csrf-cookie fails
Your browser isn't successfully initializing Sanctum's CSRF protection.
Case C: CSRF request succeeds but /login returns 419
This is where I'd inspect:
XSRF-TOKEN- Laravel session cookie
X-XSRF-TOKEN- cookie domain
- Axios credentials
- Sanctum stateful domains
Case D: Login succeeds but /api/user returns 401
Now the problem is more likely related to the authenticated session or Sanctum recognizing the request as stateful.
This simple classification eliminates a lot of unnecessary debugging.
3. Make Sure Sanctum Treats the SPA as Stateful
Modern Laravel applications can enable Sanctum's stateful API middleware in bootstrap/app.php.
For example:
use Illuminate\Foundation\Configuration\Middleware;
->withMiddleware(function (Middleware $middleware): void {
$middleware->statefulApi();
})
This matters because Sanctum needs to understand that requests coming from your first-party SPA should use session authentication.
If you're following an older Laravel tutorial, you may see instructions involving:
app/Http/Kernel.php
Be careful.
Laravel's application structure has evolved, so old Sanctum tutorials can send you to configuration locations that aren't appropriate for newer Laravel applications.
4. Check SANCTUM_STATEFUL_DOMAINS
Assume your setup looks like this:
Frontend:
https://app.example.com
API:
https://api.example.com
Your SPA host needs to be recognized as stateful.
For example:
SANCTUM_STATEFUL_DOMAINS=app.example.com
One common mistake is adding the protocol:
SANCTUM_STATEFUL_DOMAINS=https://app.example.com
That's not what you normally want here.
Think host, not frontend URL.
localhost can be even trickier
Suppose Vite runs on:
http://localhost:5173
Then you may need:
SANCTUM_STATEFUL_DOMAINS=localhost:5173
Don't casually mix:
localhost
127.0.0.1
localhost:5173
127.0.0.1:5173
Use one consistent development setup.
A surprising number of Sanctum bugs come from tiny hostname differences.
5. CORS Must Allow Credentials
If your SPA authentication uses cookies, your CORS configuration must allow credentialed requests.
A typical setup could include:
return [
'paths' => [
'api/*',
'sanctum/csrf-cookie',
'login',
'logout',
],
'allowed_methods' => ['*'],
'allowed_origins' => [
'https://app.example.com',
],
'allowed_headers' => ['*'],
'supports_credentials' => true,
];
The important setting is:
'supports_credentials' => true,
And don't casually do this:
'allowed_origins' => ['*']
when you're relying on cookie credentials.
Specify the frontend origin explicitly:
https://app.example.com
Origin and domain are not the same thing
For CORS:
https://app.example.com
is an origin.
The scheme matters.
These are different origins:
http://app.example.com
https://app.example.com
Meanwhile, Sanctum's stateful domain configuration is concerned with the appropriate host/domain.
Mixing these concepts is one reason Sanctum configuration feels confusing at first.
6. Inspect Your Session Cookie Configuration
Now suppose your architecture is:
app.example.com
api.example.com
Both are under:
example.com
A typical production session configuration might include:
SESSION_DOMAIN=.example.com
SESSION_SECURE_COOKIE=true
SESSION_SAME_SITE=lax
The parent-domain cookie configuration allows the session to work across the relevant subdomains.
After changing environment settings, also remember that cached configuration can make you think your changes aren't working.
Depending on your deployment workflow, refresh Laravel's configuration cache appropriately.
For example:
php artisan config:clear
and rebuild/cache configuration according to your production deployment process.
7. Configure Axios Correctly
Your Laravel setup can be perfect and still fail if the frontend never sends the cookies back.
For Axios:
import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
withCredentials: true,
withXSRFToken: true,
headers: {
Accept: 'application/json',
},
});
Before login:
await api.get('/sanctum/csrf-cookie');
await api.post('/login', {
email,
password,
});
The two important Axios options are:
withCredentials: true
and:
withXSRFToken: true
Now look in the browser.
After:
GET /sanctum/csrf-cookie
you should normally be able to see the relevant CSRF/session cookies.
Then inspect:
POST /login
and confirm the authentication data is actually being sent back to Laravel.
A Better Way to Debug 419
Instead of changing Laravel, CORS, Axios, cookies, and middleware simultaneously, use this sequence.
Test 1 — CSRF endpoint
Request:
GET /sanctum/csrf-cookie
Expected:
Successful response
+
XSRF/session cookies created
If no cookies appear, don't debug your login controller yet.
Test 2 — Login request
Request:
POST /login
Check:
Are cookies attached?
Is X-XSRF-TOKEN present?
Is the request being blocked by CORS?
If the answer is no, you've narrowed the problem substantially.
Test 3 — Authenticated endpoint
After successful login:
GET /api/user
If login succeeds but this gives:
401 Unauthenticated
focus on:
Sanctum stateful configuration
Session cookie
Cookie domain
withCredentials
Middleware
instead of CSRF.
419 vs 401 vs 422
These errors frequently get mixed together.
419
Usually investigate:
CSRF
XSRF token
Cookies
Session
401
Usually investigate:
Authentication
Session recognition
Sanctum stateful requests
Missing credentials
422
Usually investigate:
Validation errors
Missing fields
Invalid input
Knowing the difference can save a surprising amount of time.
Why Does Sanctum Work in Postman but Fail in Chrome?
This is an extremely common situation:
Postman ✅
Browser ❌
And it's an important clue.
Browsers enforce:
- CORS
- cookie policies
- origins
- credentials
- SameSite behavior
- CSRF flows
API clients such as Postman don't behave exactly like a browser page.
So if the API works from Postman but fails from React/Vue/Vite, start investigating the browser authentication flow, not your database credentials.
Don't "Fix" 419 by Disabling CSRF
You might find solutions online suggesting that you exclude your login endpoint from CSRF protection.
That can make the error disappear.
It doesn't necessarily mean you've fixed Sanctum.
For first-party SPA session authentication, the better solution is to correctly configure:
CSRF
Session cookies
Stateful domains
CORS
Frontend credentials
rather than removing the protection that exposed the configuration problem.
One Production Architecture Detail People Miss
This setup:
app.example.com
api.example.com
is very different from:
my-app.vercel.app
api.example.com
In the first example, the frontend and backend share the same top-level site.
In the second, they're on unrelated domains.
CORS cannot magically turn unrelated domains into the same first-party cookie environment.
So if you're struggling with Sanctum cookie authentication across completely different production domains, revisit the architecture itself.
A custom frontend domain such as:
app.example.com
can make a first-party Sanctum setup considerably cleaner.
My 60-Second Sanctum 419 Checklist
Before searching Stack Overflow again, check this:
[ ] statefulApi() enabled
[ ] SPA exists in SANCTUM_STATEFUL_DOMAINS
[ ] Exact frontend origin allowed by CORS
[ ] supports_credentials = true
[ ] Correct SESSION_DOMAIN
[ ] Secure cookie configuration matches HTTPS setup
[ ] Axios withCredentials = true
[ ] Axios withXSRFToken = true
[ ] /sanctum/csrf-cookie called before login
[ ] XSRF-TOKEN appears in browser
[ ] Session cookie appears in browser
[ ] Login request sends cookies
[ ] X-XSRF-TOKEN is sent correctly
If all twelve are correct, you've eliminated most of the common causes of a Laravel Sanctum 419 error.
The Debugging Order Matters More Than the Fix
The biggest lesson isn't a specific .env setting.
It's this:
Debug the first broken request instead of randomly changing authentication configuration.
Use this order:
OPTIONS
↓
/sanctum/csrf-cookie
↓
Cookies
↓
POST /login
↓
/api/user
Find where the expected behavior stops.
That's usually where your real problem begins.
Need the Full Laravel Sanctum 419 Reference?
I wrote a more detailed guide covering Laravel Sanctum CORS, CSRF cookies, Axios configuration, production domains, localhost issues, session configuration, 401 vs 419 vs 422 errors, and deployment troubleshooting.
👉 Read the complete Laravel Sanctum CORS 419 Error Fix guide
Use the full guide when the quick checklist above doesn't expose the problem immediately.
If you're debugging Sanctum today, remember:
419 is the symptom. The browser's request flow tells you the cause.