# Conventions

## Requests and responses

- Base URL: `https://api.alloymc.dev/v1`. HTTPS only.
- Bodies are JSON (`Content-Type: application/json`), UTF-8. Unknown fields in a request body are rejected with `bad_json`, so typos don't fail silently.
- Field names are `camelCase`. Times are RFC 3339 in UTC, like `2026-10-31T12:00:00Z`.
- Endpoints that succeed without anything to say answer `204 No Content`.

## Errors

Every error has the same shape, with a fitting HTTP status:

```json
{
  "error": {
    "code": "handle_taken",
    "message": "That handle is taken."
  }
}
```

Match on `code`: codes are stable. `message` is written for people, so show it to your users if you like, but don't parse it; it can change.

| Status | Codes you'll see | Meaning |
|---|---|---|
| 400 | `bad_json`, `handle_invalid`, `link_invalid`, `too_long`, `name_required` | The request can't work as sent |
| 401 | `unauthorized` | A token is needed, or the one sent isn't valid |
| 403 | `private` | The profile is private |
| 404 | `not_found` | No such handle, player or endpoint |
| 409 | `handle_taken` | Someone else has it |
| 410 | `link_expired` | A one-time code was already used or ran out |
| 429 | `slow_down`, `handle_cooldown` | Too many requests; see below |

## Rate limits

Each IP address can make **300 requests a minute**. Past that you get `429` with a `Retry-After` header in seconds; wait that long and carry on. Some actions have tighter limits of their own, like trading sign-in codes (20 a minute).

If you need more, cache (see below) or [tell us what you're building](mailto:contact@elchi.dev).

## Pages of results

Lists such as followers come 50 at a time, newest first, with a `next` cursor:

```json
{ "users": [ ... ], "next": 1793451234 }
```

Pass it back as `before` to get the next page. When `next` is `0`, you've seen everything.

```bash
curl "https://api.alloymc.dev/v1/users/elchi/followers?before=1793451234"
```

## Caching

Responses say how long they may be kept, in `Cache-Control`:

| What | Cache |
|---|---|
| Public profiles, player lookups | 60 seconds |
| Avatars and skin renders | 1 hour |
| Status, anything with a token | not cached |

Avatar URLs in responses carry a `v=` value that changes whenever the picture changes, so you can cache those URLs for as long as you like.

## Handles

Handles are 3 to 20 characters (`a` to `z`, `0` to `9`, `_`) and case-insensitive: `/v1/users/Elchi` and `/v1/users/elchi` are the same person. When someone changes their handle, the old one answers `301 Moved Permanently` to the new one for 30 days. Follow redirects (curl: `-L`), and store handles as people change them, or better, keep the `profileUrl`.

## Private and removed profiles

A private profile only shows `handle`, `displayName`, `avatarUrl`, `profileUrl` and `"private": true`. Its lists answer `403 private`. A deleted or suspended account answers `404`, as if it never existed. If you keep copies of profile data, refresh them, and drop what's gone within 30 days.

## CORS

Every origin may call the API from a browser (`Access-Control-Allow-Origin: *`). Credentials travel only in the `Authorization` header, never in cookies.

## Versions

Everything is under `/v1`. Within a version we may add endpoints and fields, so ignore fields you don't know. We don't rename or remove anything within a version. If we ever have to, it will be `/v2`, announced in the [changelog](https://api.alloymc.dev/docs/changelog) with time to move.
