Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.wanderersguide.app/llms.txt

Use this file to discover all available pages before exploring further.

Every endpoint authenticates the same way: an Authorization: Bearer <token> header. The token is one of two things, and the API picks the right path automatically based on length.
FlowWhen to use itWhat you send
API keyScripts, integrations, third-party toolsA 36-character UUID created in your account settings
JWTEmbedding inside a Wanderer’s Guide-authenticated frontendA Supabase Auth access token
Internally, connect() treats any 36-character token as an API key (UUIDs with hyphens are exactly 36 chars) and anything else as a JWT.

API keys

API keys grant a tool the ability to act as you against the API. They’re the right choice for anything other than the WG web app itself.

Create one

  1. Sign in at wanderersguide.app.
  2. Open account settings and go to Developer → API Clients.
  3. + New client, give it a name (e.g. foundry-importer) and an optional description.
  4. Copy the 36-char api_key.
You can keep multiple clients per account (one per tool, for example), and revoke individual ones from the same screen without affecting others.

Use one

POST /functions/v1/find-spell
Authorization: Bearer 1f2c8b80-4b0d-4d0a-9f17-7f0c3a9c1f9d
Content-Type: application/json

{ "name": "Fireball" }

What an API key actually grants

An API key authenticates a request as the user who created the key. The API generates a short-lived Supabase JWT for that user, then runs your request through the same Postgres row-level security policies as if you’d signed into the web app yourself. There is no “API mode” with elevated access. Anything you can do with a key, you could already do by signing in. What that means in practice:
  • Read all official, published content (this is open to everyone, even unauthenticated browsing).
  • Read and write your own homebrew content sources and the content within them.
  • Read and write your own campaigns and encounters.
What it deliberately can not do:
  • Read or modify your characters via find-character or update-character without an explicit per-character grant. See Character access grants.
  • Act on another user’s data, even if you share a campaign with them. RLS enforces this regardless of how you authenticate.
If a key leaks, anyone holding it can act as you (within the limits above) until you revoke it. Treat keys like passwords. Use a separate key per tool so you can revoke one without locking out the rest.

Errors

StatusJSendMeaning
401{ status: "fail", data: { message: "Invalid API Key" } }Key doesn’t match any registered client.
401{ status: "fail", data: { message: "Invalid API Key, no client found" } }Public user row was found but the client entry is missing. Usually a stale key after deletion.
403{ status: "fail", data: { message: "You do not have access to this character" } }Hit a character endpoint without a grant. See below.

JWTs

If you’re building a frontend that signs users in via Supabase Auth, send the access token. The Wanderer’s Guide web app does this; most third-party integrations don’t need to.
POST /functions/v1/find-spell
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
JWT requests are subject to the same Postgres row-level security policies as direct database access. You can only read what your role permits.

Character access grants

Character data is sensitive, so even a valid API key gets 403 from find-character, update-character, and the rest unless the character’s owner has explicitly granted that key access. The grant is per-character; one key with grants on three characters works for those three and 403s on every other. The grant flow is OAuth-style: the API client (the tool/integration) hands the character owner a URL, the owner reviews and clicks Authorize, the grant is written.

Granting access (as the integration)

  1. Sign in to Wanderer’s Guide and open Account → Developer → API Clients.
  2. Find the client you want to grant access to. Each client has a Character Authorization URL template:
    https://wanderersguide.app/oauth/access?user_id=<your-user-id>&client_id=<client-id>&character_id=<ID>
    
  3. Replace <ID> with the character id you want access to and send that URL to the character owner. (If you’re writing a tool, this is the URL to redirect them to.)

Authorizing access (as the character owner)

  1. Open the URL the integration sent.
  2. WG shows a consent screen describing what access is being requested (read character, edit stats, manage inventory).
  3. Click Authorize.
  4. From here on, calls from that key against that character work. The API-key flow generates a short-lived JWT for the character owner (not the integration’s account) so RLS sees their context.
You authorize each character separately; there is no “all my characters” grant.

Revoking access

The character owner opens the character in the builder, scrolls to the Authorized Clients section in the home tab, finds the integration, and clicks Revoke Access. The next call from that key against that character returns 403. Revocation is per-character. Revoking on one character doesn’t affect others. Revoking the API client itself in Account → Developer → API Clients kills the key entirely, so character-level grants no longer matter.

Rate limits

Every request runs through a per-token sliding window limiter. The bucket is the auth token itself, so multiple keys on the same account each get their own budget.
BucketWindowLimit
API key (Authorization: Bearer <36-char-key>)60s120 requests
Signed-in user (Supabase JWT)60s240 requests
Service webhook (bypassAuth)60s600 requests
No / unrecognized auth (per IP)60s30 requests
Every response includes:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 60
When you blow through the budget you get HTTP 429 with JSend fail and a Retry-After header (in seconds). Failed requests count too, so repeatedly hitting an endpoint with a bad key won’t let you bypass the limit. Other limits not enforced by the rate limiter:
  • Supabase Edge Function quotas on the hosting tier (request count and execution time per month). The hosted instance at wanderersguide.app runs on Supabase’s production limits; if you self-host, your limits are whatever your own Supabase project enforces.
  • Patreon-tier slot caps on resource creation, not request rate: free accounts get 6 character slots and zero campaign / encounter creates, tier 1+ unlocks campaigns and encounters, tier 2+ removes the character cap. These are checked inside the relevant create-* endpoints.
Tips for staying under the cap: cache content responses (spells, items, feats rarely change), batch by passing arrays of ids where the endpoint accepts them (e.g. find-spell with id: [1, 2, 3]), and avoid tight polling loops.

Endpoint with a different auth model

A handful of internal endpoints use a shared service secret instead. Most consumers never call them. They exist for the Discord moderation bot:
  • /update-content-update: Authorization: Bearer $CONTENT_UPDATE_KEY. Not a user token; it’s a process-to-process secret. The route uses connect()’s bypassAuth option so the value isn’t mistaken for a JWT or API key.

Want to look at the source?

The auth flow lives entirely in supabase/functions/_shared/helpers.ts. connect() and handleApiRouting() are short and worth a read if you’re wiring up automated tests or building a client SDK.