# Organizer API

Connect your organizer bot to Hatenna to manage reserves, change supported server
settings, and create ready-up checklists across the servers you run.

**Base URL:** `https://res.hatenna.xyz` · **Version:** `v1` · **Access:** approved integrations with an API key.

## Before you start

Ask the Hatenna team for an organizer API key bound to **your bot's Discord user
ID** and the scopes your integration needs. This is a Hatenna API key, not your
Discord bot token. One organizer key works across multiple servers.

For every request, both Hatenna and your organizer bot must be in the target
server. Your organizer bot must have Administrator, Manage Server, or a role
authorized through `h!roles` there. Reading or changing settings additionally
requires Manage Server or Administrator. Hatenna checks membership and permissions
against Discord for each request. Removing the bot or its permissions stops access
to that server. A key never grants permissions the bot does not already have.

Some integrations may instead receive a single-server key; that key accepts only
its assigned server ID. Confirm your key type, expiry and scopes with its issuer.

## Choose the server for each request

Every authenticated endpoint includes the target server ID:

```text
https://res.hatenna.xyz/v1/servers/{server_id}/{resource}
```

When processing a Discord command, use `ctx.guild.id`; for an interaction, use
`interaction.guild_id` in discord.py. Scheduled jobs should store the intended
server ID with the job. Reject commands in DMs, where no server ID exists.

Use the **same organizer key** for requests in server A and server B. Set the URL's
server ID to A or B as appropriate. Hatenna looks up your key's bot in that server
and checks its permissions there. It does not guess the server from an IP address,
a channel name, or the key alone. Sending an unrelated server ID grants no access.

Send all Discord IDs in JSON as **decimal strings**, including user and channel
IDs. This avoids JavaScript rounding large Discord IDs.

## Authenticate securely

```http
Authorization: Bearer YOUR_HATENNA_API_KEY
Content-Type: application/json
```

Store the key in your backend's secret environment, for example
`HATENNA_CONTROL_KEY`. Use HTTPS. Never put it in a URL, client-side website code,
source control, public Discord messages, or logs. No Discord token is sent to
this API. Browsers are not supported through CORS; call from your bot/backend.

Keys expire and can be revoked. If a key is lost or exposed, ask the Hatenna team
to revoke it and issue a replacement. To rotate without downtime, switch to a
replacement before revoking the old key. Keep the public key ID for support;
never include the full secret in a support message. An already-running request
may finish after revocation.

## Endpoints and scopes

All paths below follow `/v1/servers/{server_id}`. A valid key must include the
endpoint's scope as well as passing the server permission checks.

| Method | Path | Required scope | Purpose |
| --- | --- | --- | --- |
| GET | `/reserves` | `reserves:read` | Read the server's reserve assignments |
| POST | `/reserves` | `reserves:write` | Add or remove specified reserve assignments |
| GET | `/settings` | `settings:read` | Read supported server defaults |
| PATCH | `/settings` | `settings:write` | Update supported server defaults |
| POST | `/readyup` | `readyup:write` | Send an interactive ready-up checklist |

## Read reserves

```http
GET /v1/servers/123456789012345678/reserves
```

Example response:

```json
{
  "reserves": [
    {"pokemon": "pikachu", "user_ids": ["234567890123456789"]}
  ]
}
```

Names are canonical lowercase names. An empty server returns `{"reserves":[]}`.
Assignments belong to this server; profile-linked alts are not automatically
added to the public reserve list.

## Add or remove reserves

```http
POST /v1/servers/123456789012345678/reserves
```

```json
{
  "action": "add",
  "pokemon": "Pikachu",
  "user_ids": ["234567890123456789"]
}
```

Example response:

```json
{"ok": true, "pokemon_count": 1, "user_count": 1}
```

Use `"action":"remove"` with the same payload shape to remove assignments.
Repeating an identical add or remove is safe. Counts describe the requested
expanded Pokémon and distinct users, rather than newly changed assignments.

- Supply 1–25 user IDs and at most 100 expanded Pokémon per request.
- The `pokemon` string accepts comma-separated names, existing server categories,
  and the normal reserve form syntax, such as `Pikachu, all Flabebe`.
- Unknown names reject the request before writes. Use exact names for predictable
  integrations; a category can change as the server edits it.
- All users must be current server members when adding. Removal also accepts IDs
  of departed users so old assignments can be cleaned up.
- Changes feed Hatenna's existing reserve pings. Copy, transfer, exchange and
  server-wide clear endpoints are not available in v1.

## Read and change settings

Read current supported defaults with:

```http
GET /v1/servers/123456789012345678/settings
```

The response wraps the supported fields in a `settings` object. To change only
specific fields:

```http
PATCH /v1/servers/123456789012345678/settings
```

```json
{"reserve_ping": true, "naming_mode": "assisted", "assisted_embed": true}
```

The response returns the supported settings after the update. Unspecified fields
retain their values. Repeating the same PATCH is safe. Use JSON booleans `true`
and `false`, not the strings `"true"` or `"false"`.

| Field | Accepted values | Controls |
| --- | --- | --- |
| `naming` | boolean | Spawn naming |
| `assisted_embed` | boolean | Embed output in Assisted naming mode |
| `only_pings` | boolean | Only-pings behavior |
| `best_name` | boolean | Best-name behavior |
| `rare_ping` | boolean | Rare pings |
| `regional_ping` | boolean | Regional pings |
| `shinyhunt_ping` | boolean | Shiny-hunt pings |
| `collection_ping` | boolean | Collection pings |
| `reserve_ping` | boolean | Reserve pings |
| `reserve_steal` | boolean | Reserve-steal logging switch |
| `quest_ping` | boolean | Quest pings |
| `naming_mode` | `default`, `assisted` | Naming output mode |
| `starboard_mode` | `default`, `3d`, `gen5` | Starboard visual mode |

These are **server defaults**. Existing channel overrides still apply. Unknown
fields or invalid values reject the whole payload before any field is changed.
Roles, log-channel selection, modules, appearance, channel overrides and deletion
of server data are not exposed through this API version. Configure those through
Hatenna's Discord commands.

## Create a ready-up checklist

```http
POST /v1/servers/123456789012345678/readyup
```

```json
{
  "channel_id": "345678901234567890",
  "user_ids": ["234567890123456789", "456789012345678901"]
}
```

Example response:

```json
{
  "message_id": "567890123456789012",
  "url": "https://discord.com/channels/123456789012345678/345678901234567890/567890123456789012"
}
```

- Supply 1–25 members of the requested server.
- `channel_id` must identify a text channel in that server. Your organizer bot
  must have View Channel and Send Messages there. Hatenna needs permissions to
  send the message and embeds.
- Hatenna sends the normal mentions and interactive checklist. Each user marks
  themselves ready using Discord buttons; the API cannot mark users ready.
- The checklist expires after one hour. There is one active checklist per
  organizer bot **across all servers**, matching the existing Discord flow.
- The normal completion DM goes to the organizer principal. API bots should not
  depend on receiving a DM; this version has no completion webhook/status API.
- Checklists are held in memory and stop working after a bot restart.
- A conflict returns 409. The existing flow can also send explanatory feedback
  into the requested Discord channel when it cannot create a checklist.

**Do not automatically retry ready-up POSTs after a timeout.** The message may
already exist even if your HTTP client did not receive the response. Inspect the
channel first. This version does not support idempotency keys.

## Python integration example

Use an asynchronous client inside your Discord bot so API requests do not block
its event loop. This example uses aiohttp and takes the server ID from context:

```python
import os
import aiohttp

async def set_reserve_pings(ctx, enabled: bool):
    if ctx.guild is None:
        raise ValueError("Run this command inside a server")
    url = f"https://res.hatenna.xyz/v1/servers/{ctx.guild.id}/settings"
    headers = {
        "Authorization": "Bearer " + os.environ["HATENNA_CONTROL_KEY"]
    }
    timeout = aiohttp.ClientTimeout(total=120)
    async with aiohttp.ClientSession(timeout=timeout) as session:
        async with session.patch(
            url, headers=headers, json={"reserve_ping": enabled}
        ) as response:
            result = await response.json()
            if response.status != 200:
                raise RuntimeError(
                    f"Hatenna API {response.status}: {result.get('error', 'Request failed')}"
                )
            return result["settings"]
```

Your bot must also authorize its own users before making API calls. The key
represents your bot's server permissions, not those of whichever user invoked
your command. Do not let an unrestricted command delegate your bot's management
access to everyone. For a production integration, reuse a client session and
handle timeouts and rate limits without logging the key.

## Limits and errors

The request body limit is **16 KiB**. Each key allows **60 authenticated requests
per fixed UTC minute**, shared across all its servers. A 429 response includes
`Retry-After: 60` from the application; edge limits can also return 429. Respect
`Retry-After` when supplied and otherwise wait at least 60 seconds. Do not create
retry bursts. Discord delivery can queue behind Hatenna's shared outbound limiter.

Application errors use `{"error":"description"}`. The reverse proxy may return
its own text/HTML error response, so robust clients also handle non-JSON errors.

| Status | Meaning | What to do |
| --- | --- | --- |
| 200 | Request succeeded | Read the JSON response |
| 400 | Invalid payload, name, ID or target | Correct the request |
| 401 | Missing, invalid, expired or revoked key | Check the key or contact its issuer |
| 403 | Missing scope, membership or server/channel permission | Check the bound bot's permissions in that server |
| 404 | Hatenna absent, or unknown endpoint/method | Check server ID and path; single-server key mismatches return 403 |
| 409 | Ready-up was not created | Check the channel and existing checklist |
| 413 | Request exceeds body limit | Send a smaller request |
| 429 | Rate limit | Wait, then retry with backoff |
| 503 | Reconnecting or operation failed | Read state before retrying a write |

Reads and identical reserve/settings writes can be retried with bounded backoff.
Multi-Pokémon writes and multi-field setting changes are not transactions: a
storage failure can leave partial changes. Read the current state, repeat the
idempotent write if needed, and verify. Ready-up POSTs need manual inspection
after ambiguous failures. API audit entries retain operation metadata for about
90 days, without keys or request bodies. API reserve mutations do not emit the
Discord add/exchange command logs.

## Help and downloads

Contact the [Hatenna support server](https://discord.gg/5XurcUaW83) for key access,
scope changes, expiry, revocation, or integration issues. Include your public key
ID, server ID, endpoint, timestamp and HTTP status. Never send the key secret.

Download the [Markdown guide](api-guide.md), [plain-text guide](api-guide.txt), or
[Discord message pack](api-discord.txt). Each message in the Discord pack is
under 2,000 characters and can be pasted separately in order.
