API v2 — Overview & conventions¶
/api/v2 is the strict, typed API for managing users ("humans"), their tracking rules, profiles, saved locations, roles and mutes. It replaces the frozen v1 endpoints and is the recommended API for new clients. Every endpoint and field is described in the OpenAPI spec at GET /openapi.json and the interactive docs at GET /docs on your processor.
Authentication is the same as the rest of the API: send X-Poracle-Secret (see API overview).
Resource layout¶
Everything belongs to a human, so everything lives under one prefix:
/api/v2/humans list, create
/api/v2/humans/{id} fetch, delete
/api/v2/humans/{id}/tracking full snapshot
/api/v2/humans/{id}/tracking/{type} list, create, bulk delete
/api/v2/humans/{id}/tracking/{type}/{uid} fetch, replace, delete one rule
/api/v2/humans/{id}/profiles[/{profile_no}] profiles
/api/v2/humans/{id}/locations[/{label}] saved locations
/api/v2/humans/{id}/roles[/{roleId}] Discord subscription roles
/api/v2/humans/{id}/mutes alert mutes
/api/v2/activity recently-seen game data (5.3+)
{id} is the Discord user/channel id, Telegram chat id, or webhook name. Every operation is scoped to that human; you can't read or modify a rule uid that belongs to someone else (you get 404).
| Page | Covers |
|---|---|
| Tracking | The 11 tracking types, snapshot, create/replace/delete, field conventions |
| Humans & locations | Listing, creating, deleting humans; enable/disable; language; areas; location; saved locations |
| Profiles, roles & mutes | Profiles and active_hours, profile switching and copying, Discord roles, mutes |
| Migrating from v1 | v1 → v2 endpoint and field mapping |
Changes in 5.3 (breaking for v2 clients)
In 5.3 v2 stopped reusing v1's wire format for humans and profiles. If you wrote a client against an earlier v2 build:
- Human (
GET /api/v2/humans/{id},POST /api/v2/humans, snapshothuman):enabledandadmin_disableare booleans (were1/0).area,community_membershipandblocked_alertsare arrays,[]when empty (were JSON-encoded strings, andblocked_alertscould benull).area_restrictionis an array ornull(null= unrestricted,[]= no areas allowed).languageis""when unset (wasnull). - Profiles (
GET …/profilesand snapshotprofiles):areais an array. The snapshot's profiles now returnactive_hoursas a typed array too, likeGET …/profiles. - Snapshot
summaries: each entry is{alert_type, active_hours}with typed ISO-weekday entries; theidfield is gone. - Coordinates are
latitude/longitudeeverywhere: the bodies ofPOST …/location,POST …/locationsandPUT …/locations/{label}, and thecheck-locationquery, no longer acceptlat/lon. POST /api/v2/humans:areais an array of area names (was a JSON-encoded string).- Edited rules keep their
uid:PUT …/tracking/{type}/{uid}and aPOSTthat updates an existing rule return the rule under its existing uid (they used to return a new one). The same applies to v1POST /api/tracking/*and bot commands.
v1 endpoints are unchanged.
Conventions¶
Strict input¶
- Request bodies are validated against the schema. Unknown fields are rejected with
422, as are wrong types ("90"is not90,1is nottrue). - Most v2 endpoints also reject unknown query parameters with
422. - Field names are
snake_case. - Game-master ids stay integers (
pokemon_id,form,move,reward_type,lure_id,grunt_id,display_type, …). Small fixed categories are strings (team,gender,fort_type,rsvp_changes).
Omit means "any"¶
To leave a filter unconstrained, omit the field. Don't send v1's sentinel numbers (9000, -1, 4096, …) — the server fills them in. Reads return null for any filter that's at its "any"/default value, so a rule shows only the filters that matter:
{"uid": 812, "pokemon_id": 149, "min_iv": 90, "max_iv": null, "form": null, "distance": null, "clean": true}
Because null and an omitted field mean the same thing on input, you can GET a rule and PUT it straight back without changing it.
distance: 0 (or omitted) is not "zero metres": it means "match by the profile's geofence areas instead of a radius".
Profiles¶
Tracking, location and area endpoints act on the human's active profile unless you pass ?profile=N. The full snapshot also accepts ?all_profiles=true.
Confirmation messages¶
By default a tracking mutation sends the user the same "added/updated/removed" confirmation the bot would. Pass ?silent=true to apply the change without messaging them.
Response shapes¶
| Kind of endpoint | Success body |
|---|---|
| Reads | The resource directly, e.g. {"rules": [...]}, {"human": {...}}, {"profiles": [...]} |
| Tracking create / replace | {"created": [...], "updated": [...], "unchanged": [...]} |
| Tracking delete | {"deleted": [...]} |
| Pure actions (enable, language, location, profile switch, …) | {"status": "ok"} |
There's no {"status":"ok"} wrapper on reads or tracking mutations.
Errors¶
Errors are RFC 9457 application/problem+json:
{
"title": "Unprocessable Entity",
"status": 422,
"detail": "validation failed",
"errors": [
{"message": "expected required property reward_type to be present", "location": "body[0]", "value": {}}
]
}
| Status | Typical cause |
|---|---|
401 |
Missing/wrong X-Poracle-Secret (legacy {"status":"authError"} body) |
403 |
Location outside the human's permitted area (POST …/location) |
404 |
Human, rule, profile, saved location or mute doesn't exist (or isn't this human's) |
409 |
Duplicate (human already exists, location label taken, PUT would duplicate another rule) or a saved location still referenced by a rule |
422 |
Schema validation, unknown field/parameter, or a business rule (e.g. edit: true on a type that doesn't support it) |
503 |
A dependency isn't running (e.g. Discord bot for role endpoints) |
Changes take effect immediately¶
Every v2 mutation triggers a debounced state reload, so new or changed rules start matching within moments. You don't need to call /api/reload.
Quick example¶
Create a human, track 100% Dratini within 1 km of their location, then read everything back:
SECRET="your-secret"
BASE=http://localhost:3030/api/v2
curl -X POST -H "X-Poracle-Secret: $SECRET" -H "Content-Type: application/json" \
-d '{"id": "123456789012345678", "name": "Alice", "type": "discord:user"}' \
$BASE/humans
curl -X POST -H "X-Poracle-Secret: $SECRET" -H "Content-Type: application/json" \
-d '{"latitude": 51.2802, "longitude": 1.0789}' \
$BASE/humans/123456789012345678/location
curl -X POST -H "X-Poracle-Secret: $SECRET" -H "Content-Type: application/json" \
-d '[{"pokemon_id": 147, "min_iv": 100, "distance": 1000}]' \
"$BASE/humans/123456789012345678/tracking/pokemon?silent=true"
curl -H "X-Poracle-Secret: $SECRET" \
"$BASE/humans/123456789012345678/tracking?include_descriptions=true"