Pokedex API
Laravel 13 Sanctum Tokens REST JSONA 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:
Ownership model
Pokémon fall into two categories:
| Type | user_id | Description |
|---|---|---|
| Global / seeded | null | Read-only catalog seeded from PokeAPI. Visible to every authenticated user. |
| User-owned | your user id | Created by you. Only you can edit or delete them. |
Authentication
Protected routes require a Bearer token obtained from /api/register or /api/login.
Error Handling
| Status | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 401 | Unauthenticated — missing or invalid token |
| 403 | Forbidden — you don't own this Pokémon |
| 404 | Not Found |
| 422 | Validation failed — errors key contains field messages |
Validation error shape:
POST /api/register
Create a new account. Returns the user object and a Sanctum token.
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | max 255 chars |
email | string | Yes | unique, valid email |
password | string | Yes | min 8 chars |
password_confirmation | string | Yes | must match password |
POST /api/login
Authenticate with email and password. Returns a fresh Sanctum token.
| Field | Type | Required |
|---|---|---|
email | string | Yes |
password | string | Yes |
POST /api/logout
Revoke the current access token. Subsequent requests with the same token return 401.
No request body needed. Send only the Authorization header.
GET /api/me
Returns the authenticated user's profile.
GET /api/pokemons
Paginated list of global Pokémon and the authenticated user's own entries.
| Param | Type | Description |
|---|---|---|
mine | boolean | Set to 1 to return only your own Pokémon (excludes globals). |
search | string | Partial name filter. E.g. ?search=pika |
type | string | Filter by type. E.g. ?type=electric |
page | integer | Page number (50 per page). |
GET /api/pokemons/{id}
Retrieve a single Pokémon by its database ID.
| Param | Description |
|---|---|
id | Database primary key of the Pokémon. |
POST /api/pokemons
Create a new user-owned Pokémon. The user_id is set automatically to the authenticated user.
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | |
height | integer | Yes | decimetres |
weight | integer | Yes | hectograms |
base_experience | integer | No | nullable |
types | array of strings | Yes | at least 1 type, e.g. ["fire","flying"] |
stats | object | Yes | see keys below |
stats.hp | integer ≥ 0 | Yes | |
stats.attack | integer ≥ 0 | Yes | |
stats.defense | integer ≥ 0 | Yes | |
stats.special_attack | integer ≥ 0 | Yes | |
stats.special_defense | integer ≥ 0 | Yes | |
stats.speed | integer ≥ 0 | Yes | |
sprite_front | file (image) | No | PNG/JPG/GIF/WEBP · max 2 MB. Omit to leave blank. |
sprite_official | file (image) | No | PNG/JPG/GIF/WEBP · max 2 MB. Omit to leave blank. |
multipart/form-data, not JSON. The API stores the file and returns a full URL in the response.PUT /api/pokemons/{id}
Update a user-owned Pokémon. Only the owner can update. Global (seeded) Pokémon return 403.
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.
DELETE /api/pokemons/{id}
Delete a user-owned Pokémon. Only the owner can delete. Seeded globals return 403.
No request body. Returns a confirmation message on success.
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.
| Field | Type | Required | Notes |
|---|---|---|---|
pokemon1_id | integer | Yes | Database ID of the first Pokémon |
pokemon2_id | integer | Yes | Database ID of the second Pokémon — must differ from pokemon1_id |
| Mechanic | Description |
|---|---|
| Turn order | The faster Pokémon (higher speed stat) attacks first. Ties are broken randomly. |
| Move type | Each 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 effectiveness | Full 18-type chart applied. When an attacker has multiple types, the best (highest multiplier) type is used against all defender types combined. |
| Critical hit | 1/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 limit | Maximum 50 turns; if both Pokémon survive, the last attacker is declared winner. |
| Label | Type multiplier |
|---|---|
super effective | > 1.0× |
normal | 1.0× |
not very effective | < 1.0× |
immune | 0.0× |
critical hit! | any — overrides when a crit lands (except immune) |
| Field | Type | Description |
|---|---|---|
turn | integer | Turn number (both Pokémon may act in the same turn) |
attacker | string | Name of the attacking Pokémon |
defender | string | Name of the defending Pokémon |
damage | integer | HP deducted from the defender (minimum 1) |
effectiveness | string | One of the effectiveness labels above |
type_multiplier | float | Raw multiplier applied (0.0, 0.5, 1.0, 2.0, 4.0, …) |
critical | boolean | Whether the hit was a critical strike |
attacker_remaining_hp | integer | Attacker's HP after this hit |
defender_remaining_hp | integer | Defender's HP after this hit (0 = fainted) |