← ShinyForge · App API · OpenAPI Schema · Discord Setup

App API Documentation

ShinyForge App API v1

Start Here — For App Developers And Coding Assistants

Public discovery requires no account: GET /api, /api/v1 or /.well-known/shiny-tracker.json. Read /openapi.json for machine-readable endpoints and /llms.txt for a concise integration guide. These documents are public; accessing a user's data still requires their scoped Bearer token. Give another coding assistant this URL: https://shinytracker.sjl101.co.uk/api-docs.html.

Choose the endpoint by intent: /box-imports records reviewed storage-box observations and checks dex entries, /catches records a genuinely new kept shiny in catch history, and /living-dex or /shiny-dex changes checklist marks only. Scanning an already owned shiny in a box must not create a new catch-history record.

Connection test (read only):

curl --fail-with-body https://shinytracker.sjl101.co.uk/api/v1/me -H "Authorization: Bearer $SHINY_TRACKER_TOKEN"

Download /examples/shiny_tracker_client.py for a dependency-free Python client. It accepts a website URL such as https://shinytracker.sjl101.co.uk, discovers the API and provides connection, catalog, trainer, checklist, catch and box-import helpers. Keep tokens in a private environment variable or credential vault, never in a chat.

Base URL: https://shinytracker.sjl101.co.uk/api/v1 after hosting, or http://localhost:8765/api/v1 for local testing. This is the server contract for the separate Pokémon Automation reader app. The server does not control Automation or read another PC’s files: the future reader parses local logs/screenshots, asks the user to confirm uncertain results, then sends these requests.

Connect an app

In Settings → App Connections & API Tokens, create a named token, choose permissions and expiry, then copy it once. Send Authorization: Bearer YOUR_TOKEN on every API request. Store it in the desktop app’s OS credential vault or a private environment variable. Never embed a user’s token in a distributed app, URL, log, screenshot or source repository. Each user creates their own token. The token decides whose profile receives data; a caller cannot specify another owner. Use HTTPS for remote connections. No password, cookie or CSRF token is required by the v1 API. Browser session cookies do not authenticate these endpoints.

Tokens have 30/90/365-day expiry, are stored hashed, can be revoked from Settings, and are also revoked by a password reset. Maximum 20 active tokens per account. Limit: 120 requests per token per minute; POST bodies up to 128 KiB. There is no token refresh endpoint: create a replacement before expiry. Use GET /me to test the connection. CORS access is not enabled; this API is intended for desktop apps or server-side connectors, never a browser page containing an embedded token.

Endpoints and permissions

MethodEndpointScopePurpose
GET/meprofile:readHandle, public profile fields, granted scopes
GET/optionsdex:readValid individual games, per-game catch/hunt methods, balls and genders
GET/trainersprofile:readYour trainer profiles, including private trainer IDs
GET/living-dexdex:readLiving Dex maps and collection names
POST/living-dexliving:writeTick/untick entries by Pokémon name or number
GET/shiny-dexdex:readShiny Dex maps
POST/shiny-dexshiny:writeTick/untick shiny entries by name or number
GET/huntshunts:readYour hunt trackers and summaries
POST/huntshunts:writeCreate/update an app-owned hunt using an absolute snapshot
GET/catches?limit=50&after=CURSORcatches:readYour catch history, maximum 100 per page
POST/catchescatches:writeAdd a confirmed kept shiny and update both dexes
POST/eventsevents:write plus matching write scopeAutomation reader event ingestion

All successful responses use HTTP 200 and JSON. Errors use { "error": "message" }: 400 invalid data, 401 expired/revoked/missing token, 403 missing permission, 404 endpoint missing, 409 conflicting duplicate or stale snapshot, 413 payload too large, 415 incorrect Content-Type, 429 rate limit. Retry network failures with the exact same event/catch ID and payload. Do not retry validation errors unchanged. Back off on 429. Never retry a 401 indefinitely; ask the user to replace their token.

Tick Living Dex and Shiny Dex by name

POST /living-dex, Content-Type: application/json:

{"entries":["Bulbasaur", "Pikachu", {"name":"Eevee","caught":true}, {"pokemonId":150,"caught":false}]}

POST /shiny-dex:

{"entries":[{"name":"Pikachu","caught":true}, "Eevee", {"pokemonId":25,"caught":false}]}

A string or number checks the entry by default. Names are exact but case/punctuation-insensitive; National Dex numbers 1–1025 and PokeAPI species slugs are accepted. Unknown or ambiguous names are rejected, not guessed. Send 1–500 entries per request. The entire batch is validated before any change is committed. caught/checked must be a JSON boolean. Optional dexGame selects an existing collection name from GET /living-dex; otherwise the first collection is used, normally home. The game a Pokémon was caught in belongs in catch metadata, not dexGame.

A shiny check also checks Living Dex. Unchecking a Living Dex entry unchecks its Shiny Dex entry too. Unchecking Shiny Dex leaves Living Dex checked. These endpoints manage collection checkmarks, not individual catch records or hunt statistics. Use POST /catches when importing an actual caught shiny with metadata. Unticking a dex entry does not delete catch history. Optional formKey, for example 25:25::25, must belong to the species. Existing LivingDex schema 3 data and extra fields are preserved.

The response reports the selected dex, collection and resolved updates. Example:

{"dex":"shiny","dexGame":"home","updates":[{"pokemonId":133,"name":"Eevee","formKey":"133:133::133","caught":true}]}

Send a kept shiny from an app

POST /catches:

{
  "clientId":"my-reader",
  "externalId":"catch-console0-20261108-001",
  "name":"Eevee",
  "shiny":true,
  "kept":true,
  "game":"Violet",
  "method":"full",
  "caughtAt":"2026-11-08T13:00:00Z",
  "encounters":450,
  "durationMs":3600000,
  "nickname":"",
  "gender":"Female",
  "ball":"Premier Ball",
  "notes":"Imported by my desktop app"
}

Use pokemonId instead of name if known. Optional metadata: nickname, gender, ball, notes, owned trainerId and formKey. Unknown encounter counts and duration may be omitted; they remain unknown rather than becoming zero. shiny:true and kept:true are required. A shiny detection, failed capture or discarded Dynamax Adventure result must not be sent as a kept catch. A successful request creates one shiny catch on the profile and checks both Living Dex and Shiny Dex. It does not manufacture a second shiny for the same event on retry.

clientId identifies the app installation/source and must remain stable when rotating a token. externalId identifies this particular catch. Each is 1–120 letters/numbers/dots/colons/dashes/underscores. Retries must preserve IDs and field values (JSON object key order may differ); duplicate:true confirms an already accepted request. Reusing an ID with changed data returns 409. Use a fresh catch ID for a genuinely different Pokémon, even when species/game match. The v1 endpoints never return the token in a response.

Track an Automation hunt

POST /hunts:

{
  "clientId":"my-reader",
  "externalId":"console0-eevee-hunt-001",
  "pokemonId":133,
  "game":"Shield",
  "method":"dynamax",
  "charm":true,
  "encounters":450,
  "durationMs":3600000,
  "status":"active",
  "observedAt":"2026-11-08T13:00:00Z",
  "location":"Max Lair"
}

The same hunt externalId updates its tracker using absolute lifetime encounter/time totals, not increments. This makes ordinary progress resends safe. Keep observedAt stable on retry; older snapshots return 409. Timer time is supplied by the app, so a separate web timer is not automatically started. status may be active or paused. Methods: full, masuda, dynamax, radar, sv-outbreak, pla-outbreak, custom, go. Custom requires a denominator customOdds; ordinary base may be 4096 or 8192, with optional charm. Method rates have the same setup requirements as web-created hunts. For Pokémon GO use game: "Pokémon GO" and method: "go" to track unknown-rate encounters, or method: "custom" with a verified customOdds. GO never applies Shiny Charm or Masuda bonuses. Unknown-rate segments return null odds and null cumulative chance, while counts and time still work.

To finish the imported hunt, POST /catches with a new catch externalId and huntExternalId:"console0-eevee-hunt-001". The hunt must belong to this client/user and target the same species. The hunt is marked completed and its counter/time are retained in the catch, unless explicit encounter/time values are supplied. The shiny appears in the profile catch grid and both dexes automatically. A completed hunt cannot accept further progress snapshots. Deleted imported hunts require a new externalId if the app intentionally starts another hunt.

Automation event API

POST /events accepts the same fields as above plus eventType:

* hunt-progress: externalId is a unique progress-event ID, and huntExternalId identifies the persistent hunt. Requires events:write and hunts:write.

* shiny-caught: externalId is a unique event ID; optional catchExternalId is the stable catch ID. Set huntExternalId to complete a previously imported hunt. Requires events:write and catches:write, as well as explicit shiny:true, kept:true confirmation.

{"clientId":"automation-reader","externalId":"progress-000001","huntExternalId":"console0-hunt-001","eventType":"hunt-progress","name":"Eevee","game":"Sword / Shield","method":"dynamax","charm":true,"encounters":50,"durationMs":120000,"status":"active","observedAt":"2026-11-08T13:00:00Z"}

The future app should watch only user-selected local Automation logs/screenshots. Parse detections locally, let users correct species and confirm it was kept, attach a stable source ID, then enqueue a catch. Retain a small local pending queue and accepted IDs. Retry exactly on transient network failures; never generate a fresh catch ID just because a request times out. Keep counters/time separate from shiny confirmation. The server accepts structured data, not arbitrary command execution, remote file paths, webhook credentials, raw screenshots or logs. No live Automation control or capture stream has been added.

Python standard-library example

import json
import os
import urllib.request

base = os.environ.get("POKEMON_HUB_URL", "http://localhost:8765").rstrip("/")
token = os.environ["POKEMON_HUB_TOKEN"]

def call(path, payload=None):
    request = urllib.request.Request(
        base + "/api/v1" + path,
        data=None if payload is None else json.dumps(payload).encode("utf-8"),
        headers={"Authorization": "Bearer " + token,
                 "Content-Type": "application/json"},
        method="GET" if payload is None else "POST",
    )
    with urllib.request.urlopen(request, timeout=20) as response:
        return json.load(response)

print(call("/me")["user"]["handle"])
print(call("/living-dex", {"entries": ["Bulbasaur", "Ivysaur"]}))
print(call("/shiny-dex", {"entries": [{"name": "Eevee", "caught": True}]}))

Do not print the token. A deployed app should use its credential vault rather than asking users to paste secrets into a chat. Read exports use cursor pagination; /catches currently returns snake_case database field names, while write bodies use camelCase. Pass its nextCursor as after on the next request until the returned list is empty.

Machine-readable schema: /openapi.json. Human-readable documentation: /api-docs.html. All private endpoints require a valid scoped token. Token administration is intentionally session-only: app tokens cannot create more tokens, reset passwords, access admin pages, read the owner’s screenshot archive or read Discord.

Scan a Pokémon Screenshot

POST /images/scan with an images:scan token permission and JSON { "image": "data:image/png;base64,..." }. PNG, JPEG and WebP images up to 5 MB are accepted (JSON up to 7 MB). Limit: six scans per minute per user. The response includes candidates, highlightedText, engine and requiresConfirmation: true. A candidate with selected: true was read from the highlighted row, which this archive uses to indicate the shiny. Approximate matches must be reviewed. This does not classify shiny colours or change a dex. The companion app should ask the user to confirm species and that the Pokémon was kept, then POST /catches using its catches:write permission. Discord images are displayed and saved privately without automatic OCR.

Capture-card box scanner contract

The scanner runs on the PC in a separate program; this web server does not access capture-card hardware. Use HTTPS and a per-user Bearer API token from Settings. Minimum permissions: dex:read, living:write, shiny:write. Add profile:read to select a linked trainer. Existing images:scan only reads highlighted-row text; it is not a full storage-box grid detector. The new program must recognise each occupied slot locally and review ambiguous species, forms and shininess before submitting.

GET /api/v1/catalog returns all 1,025 species and form IDs. GET /api/v1/trainers returns your own trainer IDs. GET /api/v1/living-dex and /shiny-dex return existing collection maps.

POST /api/v1/box-imports accepts:

{
  "clientId": "switch-box-reader",
  "externalId": "scan-session-001-box-01-review-01",
  "saveId": "my-violet-save",
  "box": 1,
  "game": "Violet",
  "confirmed": true,
  "entries": [
    {"slot": 1, "name": "Pikachu", "shiny": false},
    {"slot": 2, "pokemonId": 133, "shiny": true, "gender": "Female", "ball": "Poké Ball", "nickname": "Eevee"}
  ]
}

Optionally add trainerId to link the snapshot to one of your trainers; that trainer determines the game. This does not mark the trainer’s manual living checklist. Optional dexGame selects an existing collection from GET /living-dex; otherwise the first collection is used. Optional per-slot formKey uses species:form::form with a valid form ID from /catalog. Slots are unique, start at 1, and support up to 120 occupied slots per request. Skip empty slots. Every entry must explicitly say shiny:true or shiny:false; unknown values require review, not a guess.

The server validates all entries before changing anything. Normal entries tick the living dex. Shiny entries tick living and shiny dexes. It stores the latest box observation with nickname, gender, ball and trainer for that saveId/box. It never creates catch-history records, invents hunt times, clears other entries, or removes historical shinies when a normal Pokémon is scanned. GET /box-imports returns your own latest box snapshots.

Retry an identical request with the same clientId/externalId: duplicate:true is returned without repeating writes. A different payload using that same ID gets HTTP 409. Give a corrected or newly reviewed scan a new externalId. Keep saveId and box stable across rescans. A 401 means an invalid/expired token, 403 means missing scope, 400 means validation failed, and 429 means rate limited; back off and retry later. Do not embed tokens in screenshots, source code or logs.

To create a genuinely new kept shiny catch, use the separately documented POST /catches endpoint with shiny:true and kept:true after explicit confirmation. Do not submit every shiny you merely observe in a box to catch history: it may already be recorded. The capture-card application owns recognition, deduplication and review; this API owns authenticated validated storage.