To build a REST API with token authentication in Laravel 13, install Sanctum with php artisan install:api, add the HasApiTokens trait to your User model, issue tokens with createToken(), and protect routes with the auth:sanctum middleware. Clients then send the token as a Bearer header. This guide builds a small login, profile and logout API and shows how to scope tokens with abilities.
What do you need before you start?
- Laravel 13 (it requires PHP 8.3 or newer; PHP 8.3 to 8.5 are supported)
- A database connection and a
userstable (a fresh Laravel app has both) - An HTTP client such as curl, Postman or the Laravel test client
How do you install Sanctum for API tokens?
Run one Artisan command. It publishes the API route file, creates the personal_access_tokens migration and prepares the app for token auth.
php artisan install:api
php artisan migrateThen add the trait to the User model so it can create and own tokens.
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
}How do you issue a token on login?
Validate the credentials, check the password hash, then return the plain-text token. Sanctum stores only a SHA-256 hash, so the plain-text value is available once, at creation time.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function login(Request $request)
{
$data = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
'device_name' => ['required', 'string', 'max:100'],
]);
$user = User::where('email', $data['email'])->first();
if (! $user || ! Hash::check($data['password'], $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
return response()->json([
'token' => $user->createToken($data['device_name'])->plainTextToken,
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->noContent();
}
}How do you protect API routes?
Attach the sanctum guard through the auth:sanctum middleware in routes/api.php.
<?php
use App\Http\Controllers\Api\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn (Request $request) => $request->user());
Route::post('/logout', [AuthController::class, 'logout']);
});How do token abilities work?
Abilities act like scopes. Pass them as the second argument of createToken(), then check them with tokenCan() or with route middleware.
$token = $user->createToken('mobile', ['orders:read'])->plainTextToken;
if ($request->user()->tokenCan('orders:read')) {
// allowed
}To use the middleware, register the aliases in bootstrap/app.php, then use abilities:a,b (all required) or ability:a,b (any one).
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'abilities' => CheckAbilities::class,
'ability' => CheckForAnyAbility::class,
]);
})Route::get('/orders', [OrderController::class, 'index'])
->middleware(['auth:sanctum', 'abilities:orders:read']);How do you expire and revoke tokens?
Tokens never expire by default. Set the expiration option (in minutes) in config/sanctum.php, or pass an expiry as the third argument of createToken(). Schedule sanctum:prune-expired to clean up old rows.
$user->createToken('cli', ['*'], now()->plus(weeks: 1));
// revoke everything for a user
$user->tokens()->delete();How do you verify it works?
Call the API with curl. The login response contains the token. Use it on a protected route.
curl -X POST http://localhost:8000/api/login \
-H "Accept: application/json" \
-d "email=user@example.com&password=secret&device_name=curl"
curl http://localhost:8000/api/user \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_TOKEN"For automated tests, Sanctum::actingAs(User::factory()->create(), ['orders:read']) authenticates a request with the abilities you list.
What are the common mistakes?
- Forgetting
Accept: application/json, which makes failed auth redirect instead of returning 401 - Using API tokens for your own first-party SPA. The Sanctum docs recommend its cookie-based SPA authentication for that case
- Logging or storing the plain-text token. Show it once and discard it
- Granting
['*']abilities to every token instead of the minimum needed
FAQ
Is Sanctum or Passport better for a simple API?
Sanctum is the lighter choice for first-party apps and personal access tokens. Choose Passport only when you need a full OAuth2 server.
Does a Sanctum token expire?
Not by default. Configure expiration or set a per-token expiry.
How do I log a user out of one device?
Delete the current token with $request->user()->currentAccessToken()->delete().



