Pokedex API

Laravel 13 Sanctum Tokens REST JSON

A token-authenticated REST API where every user maintains their own Pokémon records alongside a shared catalog of ~1 300 Pokémon seeded from PokeAPI.

Introduction

All endpoints are prefixed with /api. Responses are JSON. The base URL for a local Laragon install is:

http://localhost:8000/api

Ownership model

Pokémon fall into two categories:

Typeuser_idDescription
Global / seedednullRead-only catalog seeded from PokeAPI. Visible to every authenticated user.
User-ownedyour user idCreated by you. Only you can edit or delete them.

Authentication

Protected routes require a Bearer token obtained from /api/register or /api/login.

Authorization: Bearer <your-token>
Requests to protected endpoints without a valid token return 401 Unauthorized.

Error Handling

StatusMeaning
200OK
201Created
401Unauthenticated — missing or invalid token
403Forbidden — you don't own this Pokémon
404Not Found
422Validation failed — errors key contains field messages

Validation error shape:

{ "message": "The name field is required.", "errors": { "name": ["The name field is required."] } }

POST  /api/register

Create a new account. Returns the user object and a Sanctum token.

POST /api/register No auth required
FieldTypeRequiredNotes
namestringYesmax 255 chars
emailstringYesunique, valid email
passwordstringYesmin 8 chars
password_confirmationstringYesmust match password
POST /api/register Content-Type: application/json { "name": "Ash Ketchum", "email": "ash@pallet.town", "password": "pikachu123", "password_confirmation": "pikachu123" }
{ "user": { "id": 1, "name": "Ash Ketchum", "email": "ash@pallet.town", "created_at": "2026-05-11T00:00:00.000000Z" }, "token": "1|abc123..." }

POST  /api/login

Authenticate with email and password. Returns a fresh Sanctum token.

POST /api/login No auth required
FieldTypeRequired
emailstringYes
passwordstringYes
{ "user": { "id": 1, "name": "Ash Ketchum", "email": "ash@pallet.town", "created_at": "..." }, "token": "2|xyz789..." }
{ "message": "...", "errors": { "email": ["The provided credentials are incorrect."] } }

POST  /api/logout

Revoke the current access token. Subsequent requests with the same token return 401.

POST /api/logout 🔒 Requires token

No request body needed. Send only the Authorization header.

{ "message": "Logged out successfully." }

GET  /api/me

Returns the authenticated user's profile.

GET /api/me 🔒 Requires token
{ "id": 1, "name": "Ash Ketchum", "email": "ash@pallet.town", "created_at": "2026-05-11T00:00:00.000000Z" }

GET  /api/pokemons

Paginated list of global Pokémon and the authenticated user's own entries.

GET /api/pokemons 🔒 Requires token
ParamTypeDescription
minebooleanSet to 1 to return only your own Pokémon (excludes globals).
searchstringPartial name filter. E.g. ?search=pika
typestringFilter by type. E.g. ?type=electric
pageintegerPage number (50 per page).
{ "data": [ { "id": 25, "pokeapi_id": 25, "name": "pikachu", "height": 4, "weight": 60, "base_experience": 112, "types": ["electric"], "stats": { "hp": 35, "attack": 55, "defense": 40, "special_attack": 50, "special_defense": 50, "speed": 90 }, "sprite_front": "https://raw.githubusercontent.com/.../25.png", "sprite_official": "https://raw.githubusercontent.com/.../25.png", "is_global": true, "user_id": null, "created_at": "...", "updated_at": "..." } ], "links": { "first": "...", "last": "...", "prev": null, "next": "..." }, "meta": { "current_page": 1, "per_page": 50, "total": 1302 } }

GET  /api/pokemons/{id}

Retrieve a single Pokémon by its database ID.

GET /api/pokemons/{id} 🔒 Requires token
ParamDescription
idDatabase primary key of the Pokémon.
{ "data": { "id": 25, "name": "pikachu", "types": ["electric"], ... } }
{ "message": "No query results for model [App\\Models\\Pokemon] 999" }

POST  /api/pokemons

Create a new user-owned Pokémon. The user_id is set automatically to the authenticated user.

POST /api/pokemons 🔒 Requires token
FieldTypeRequiredNotes
namestringYes
heightintegerYesdecimetres
weightintegerYeshectograms
base_experienceintegerNonullable
typesarray of stringsYesat least 1 type, e.g. ["fire","flying"]
statsobjectYessee keys below
stats.hpinteger ≥ 0Yes
stats.attackinteger ≥ 0Yes
stats.defenseinteger ≥ 0Yes
stats.special_attackinteger ≥ 0Yes
stats.special_defenseinteger ≥ 0Yes
stats.speedinteger ≥ 0Yes
sprite_frontfile (image)NoPNG/JPG/GIF/WEBP · max 2 MB. Omit to leave blank.
sprite_officialfile (image)NoPNG/JPG/GIF/WEBP · max 2 MB. Omit to leave blank.
Sprite fields are file uploads — the request must be sent as multipart/form-data, not JSON. The API stores the file and returns a full URL in the response.
POST /api/pokemons Authorization: Bearer <token> Content-Type: multipart/form-data name=fakemon height=5 weight=50 base_experience=80 types[]=fire stats[hp]=50 stats[attack]=60 stats[defense]=40 stats[special_attack]=55 stats[special_defense]=45 stats[speed]=70 sprite_front=@/path/to/front.png ← optional file sprite_official=@/path/to/art.png ← optional file
{ "data": { "id": 1303, "pokeapi_id": null, "name": "fakemon", "is_global": false, "user_id": 1, "sprite_front": "http://localhost/storage/sprites/abc123.png", "sprite_official": null, ... } }

PUT  /api/pokemons/{id}

Update a user-owned Pokémon. Only the owner can update. Global (seeded) Pokémon return 403.

PUT /api/pokemons/{id} 🔒 Owner only

Body fields are the same as Create but all are optional (partial update supported). Send only the fields you want to change. Because sprite fields are file uploads the request must use multipart/form-data with _method=PUT for method spoofing.

Omitting a sprite field leaves the existing image unchanged. Uploading a new file replaces the old one (old file is deleted from storage).
POST /api/pokemons/1303 Authorization: Bearer <token> Content-Type: multipart/form-data _method=PUT name=fakemon-v2 stats[hp]=60 stats[attack]=65 stats[defense]=45 stats[special_attack]=60 stats[special_defense]=50 stats[speed]=75 sprite_front=@/path/to/new-front.png ← optional
{ "data": { "id": 1303, "name": "fakemon-v2", ... } }
{ "message": "This action is unauthorized." }

DELETE  /api/pokemons/{id}

Delete a user-owned Pokémon. Only the owner can delete. Seeded globals return 403.

DEL /api/pokemons/{id} 🔒 Owner only

No request body. Returns a confirmation message on success.

{ "message": "Pokemon deleted." }
{ "message": "This action is unauthorized." }

POST  /api/battle

Simulate a turn-based battle between two Pokémon. The winner is determined by stats (attack, defense, special attack, special defense, speed) and type effectiveness. Both Pokémon must be different entries in the database.

POST /api/battle 🔒 Requires token
FieldTypeRequiredNotes
pokemon1_idintegerYesDatabase ID of the first Pokémon
pokemon2_idintegerYesDatabase ID of the second Pokémon — must differ from pokemon1_id
MechanicDescription
Turn orderThe faster Pokémon (higher speed stat) attacks first. Ties are broken randomly.
Move typeEach attacker uses physical or special moves based on which offensive stat is higher (attack vs special_attack). The matching defensive stat is used on the defender.
Type effectivenessFull 18-type chart applied. When an attacker has multiple types, the best (highest multiplier) type is used against all defender types combined.
Critical hit1/16 chance per hit. Deals 1.5× damage. Overrides the effectiveness label in the log.
Damage formula(offStat × 16 / defStat) × typeMultiplier × critMod × rand(0.85–1.0)
Turn limitMaximum 50 turns; if both Pokémon survive, the last attacker is declared winner.
LabelType multiplier
super effective> 1.0×
normal1.0×
not very effective< 1.0×
immune0.0×
critical hit!any — overrides when a crit lands (except immune)
POST /api/battle Authorization: Bearer <token> Content-Type: application/json { "pokemon1_id": 6, "pokemon2_id": 9 }
{ "winner": { "id": 6, "name": "charizard", "types": ["fire","flying"], ... }, "loser": { "id": 9, "name": "blastoise", "types": ["water"], ... }, "turns": 5, "p1": { "pokemon": { "id": 6, "name": "charizard", ... }, "remaining_hp": 24, "max_hp": 78 }, "p2": { "pokemon": { "id": 9, "name": "blastoise", ... }, "remaining_hp": 0, "max_hp": 79 }, "log": [ { "turn": 1, "attacker": "charizard", "defender": "blastoise", "damage": 18, "effectiveness": "not very effective", "type_multiplier": 0.5, "critical": false, "attacker_remaining_hp": 78, "defender_remaining_hp": 61 }, { "turn": 1, "attacker": "blastoise", "defender": "charizard", "damage": 31, "effectiveness": "super effective", "type_multiplier": 2, "critical": false, "attacker_remaining_hp": 61, "defender_remaining_hp": 47 } ] }
FieldTypeDescription
turnintegerTurn number (both Pokémon may act in the same turn)
attackerstringName of the attacking Pokémon
defenderstringName of the defending Pokémon
damageintegerHP deducted from the defender (minimum 1)
effectivenessstringOne of the effectiveness labels above
type_multiplierfloatRaw multiplier applied (0.0, 0.5, 1.0, 2.0, 4.0, …)
criticalbooleanWhether the hit was a critical strike
attacker_remaining_hpintegerAttacker's HP after this hit
defender_remaining_hpintegerDefender's HP after this hit (0 = fainted)
{ "message": "The pokemon2_id field must be different from pokemon1_id.", "errors": { "pokemon2_id": ["The pokemon2_id field must be different from pokemon1_id."] } }