﻿# GGBRO Bot + API Reference (AI)

**This file (`/developers/reference.md`) is the single URL another AI should fetch** for a complete, machine-readable mirror of the GGBRO developer docs.

Humans should use the interactive docs at **`/developers`**.

Related assets (full runnable examples live there / in these files):

- Human docs: `/developers` (`index.html`)
- Python helper / SDK-style example: `/developers/ggbro.py` (**v1.2.0** — `ctx.get_option()`, typed `options`, slash payload docs)
- Music bot example: `/developers/music_bot.py`
- Welcome bot example: `/developers/welcome_bot.py`

---

## Base URLs

| Name | Value |
|------|-------|
| Public base URL | `https://ggbro.app` |
| REST API base | `https://ggbro.app/api/v1` (`/api/v1`) |
| Socket.IO gateway | `https://ggbro.app` (or `https://ws.ggbro.app`) |
| Discovery | `GET https://ggbro.app/api/v1` / `GET /api/v1/platform/config` |
| Developer Portal | `https://ggbro.app/portal/` |

**API version:** The REST surface is **`/api/v1/...`**. Breaking changes in the future will add `/api/v2` while keeping v1 stable. Channel webhook **execute** URLs use `/api/webhooks/:id/:token` (not versioned in the path).

All example URLs below use `https://ggbro.app` / `https://ggbro.app/api/v1` (server-templated at serve time).

---

## Table of contents

1. [Introduction](#introduction)
2. [Quick Start](#quick-start)
3. [Authentication](#authentication)
4. [Creating a Bot](#creating-a-bot)
5. [Adding Bot to a Server](#adding-bot-to-a-server)
6. [Socket.IO Gateway](#socketio-gateway)
7. [Slash Commands](#slash-commands)
8. [Sending Messages](#sending-messages)
9. [Embeds & Components](#embeds--components)
10. [File Uploads & Attachments](#file-uploads--attachments)
11. [Bot REST API](#bot-rest-api)
12. [Events Reference](#events-reference)
13. [REST API Overview](#rest-api-overview)
14. [Servers](#servers)
15. [Audit Log](#audit-log)
16. [Members Moderation](#members-moderation)
17. [Channels](#channels)
18. [Channel & Category Permission Overrides](#channel--category-permission-overrides)
19. [Messages](#messages)
20. [Members & Users](#members--users)
21. [Friends](#friends)
22. [Direct Messages](#direct-messages)
23. [Group DMs](#group-dms)
24. [Roles](#roles)
25. [Permissions Bitfield](#permissions-bitfield)
26. [Emojis](#emojis)
27. [Soundboard](#soundboard)
28. [Reactions & Pins](#reactions--pins)
29. [Invites & Bot Invites](#invites--bot-invites)
30. [Copying IDs](#copying-ids)
31. [Errors & Rate Limits](#errors--rate-limits)
32. [Webhooks](#webhooks)
33. [Stats](#stats)
34. [Condensed Examples](#condensed-examples)

---

## Introduction

GGBRO is a **custom Socket.IO-based** chat platform with its own protocol. Bots connect via **Socket.IO v4** for real-time events and use a **REST API** for data fetching. There is **no official SDK** — use `python-socketio` (Python) or `socket.io-client` (Node.js) directly. Product tour: https://ggbro.app/features

Bots can:

- Respond to **slash commands** (`/play`, `/ping`, `/serverinfo`, …)
- Send **messages** and **rich embeds** in text channels (and voice channels only when `chat_enabled=1`)
- React to **events** (members joining, messages, reactions, voice, …)
- Send **direct messages** to users (shared-server requirement on Bot REST DM)
- Join **voice channels** and stream PCM audio

### Technology stack

| Component | Technology | Package |
|-----------|------------|---------|
| Desktop / Web UI | Electron + React (TypeScript), Tailwind CSS | — |
| Backend | Node.js + Express (TypeScript), SQLite | — |
| Real-time gateway | Socket.IO v4 | Python: `python-socketio[client]` — Node: `socket.io-client` |
| REST API | HTTP JSON | Python: `requests` — Node: `axios` or `fetch` |
| Voice / media | WebRTC | Browser / Electron APIs |
| Auth | Bearer JWT | REST `Authorization` header; Socket.IO handshake `auth.token` |

### Transport model

1. **REST** — CRUD, queries, uploads, application / command management, Bot REST under `/api/v1/oauth2/bot/...`
2. **Socket.IO v4** — chat, interactions, typing, presence, voice, bot message send/edit/delete

Bots are first-class users with `is_bot = 1`. Bot tokens are JWT bearer tokens from the OAuth2 application flow.

### Common response patterns

Success examples:

```json
{ "message": "..." }
```

```json
{ "user": { "id": "..." } }
```

```json
{ "server": { "id": "..." } }
```

```json
{ "messages": [] }
```

Error:

```json
{ "error": "Human readable message" }
```

---

## Quick Start

1. Open `https://ggbro.app/portal/` or app → **User Settings** → **Applications**
2. **Create an Application** — creates a bot user + token
3. **Copy the Bot Token**
4. **Add the bot** to a server you own/admin
5. **Connect via Socket.IO**, register slash commands, respond to `interaction:create`

### Minimal Python bot

```bash
pip install "python-socketio[client]" requests
```

```python
import socketio

sio = socketio.Client()

@sio.event
def connect():
    print("Connected to ggbro!")

sio.connect("https://ggbro.app", auth={"token": "YOUR_BOT_TOKEN"})
sio.wait()
```

### Minimal Node.js bot

```bash
npm install socket.io-client
```

```javascript
const { io } = require("socket.io-client");

const socket = io("https://ggbro.app", {
  auth: { token: "YOUR_BOT_TOKEN" }
});

socket.on("connect", () => console.log("Connected to ggbro!"));
```

---

## Authentication

### REST

```http
Authorization: Bearer YOUR_BOT_TOKEN
```

### Socket.IO

```python
import socketio

sio = socketio.Client(reconnection=True, reconnection_delay=1)
sio.connect("https://ggbro.app", auth={"token": "YOUR_BOT_TOKEN"})
```

```javascript
const { io } = require("socket.io-client");
const socket = io("https://ggbro.app", { auth: { token: "YOUR_BOT_TOKEN" } });
```

**Keep the token secret.** If compromised, regenerate via `POST /api/v1/oauth2/applications/:appId/token` or the Developer Portal.

User tokens and bot tokens both use the same Bearer / handshake pattern. Bot-only routes under `/api/v1/oauth2/bot/...` require `is_bot`.

---

## Creating a Bot

Create via Developer Portal UI or REST (user token + 2FA where required for create).

### `POST /api/v1/oauth2/applications`

Create application + bot user + token.

**Request:**

```json
{
  "name": "My Awesome Bot",
  "description": "A bot that does cool things"
}
```

**Response:**

```json
{
  "application": {
    "id": "abc123...",
    "name": "My Awesome Bot",
    "bot_user_id": "bot456...",
    "client_secret": "hex...",
    "default_permissions": 8895488,
    "invite_permission_locked": false
  },
  "bot_token": "eyJhbGciOi..."
}
```

The bot user starts **offline** until it connects. Default invite permissions are least-privilege (`8895488`); change via portal or `PATCH /api/v1/oauth2/applications/:appId/permissions`.

### Application management

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/oauth2/applications` | List your applications |
| `GET` | `/api/v1/oauth2/applications/:appId` | Application details |
| `PUT` | `/api/v1/oauth2/applications/:appId` | Update name/description |
| `POST` | `/api/v1/oauth2/applications/:appId/token` | Regenerate bot token |
| `DELETE` | `/api/v1/oauth2/applications/:appId` | Delete app + bot user |
| `POST` | `/api/v1/oauth2/applications/:appId/avatar` | Upload avatar (multipart field `avatar`) |
| `POST` | `/api/v1/oauth2/applications/:appId/banner` | Upload banner (multipart field `banner`) |
| `PATCH` | `/api/v1/oauth2/applications/:appId/public-invite` | Toggle public invite |
| `PATCH` | `/api/v1/oauth2/applications/:appId/permissions` | Set `default_permissions` + `invite_permission_locked` (invite defaults only) |
| `PATCH` | `/api/v1/oauth2/applications/:appId/banner-offset` | Banner offset |

### Bot command management (owner / dashboard)

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/oauth2/applications/:appId/commands` | List commands |
| `POST` | `/api/v1/oauth2/applications/:appId/commands` | Create one command |
| `PUT` | `/api/v1/oauth2/applications/:appId/commands/:cmdId` | Update command |
| `DELETE` | `/api/v1/oauth2/applications/:appId/commands/:cmdId` | Delete command |
| `GET` | `/api/v1/oauth2/servers/:serverId/commands` | Commands available in a server |
| `POST` | `/api/v1/oauth2/commands` | **Bot token** bulk register (replaces global commands, max 50) |

---

## Adding Bot to a Server

Discord-like invite permissions: the bitfield in the invite URL (or request body) is what the server admin authorizes. New bots start **offline** with least-privilege defaults (`8895488`), not Administrator.

| Method | Path | Description |
|--------|------|-------------|
| `PATCH` | `/api/v1/oauth2/applications/:appId/permissions` | `{default_permissions, invite_permission_locked?}` — updates invite defaults only |
| `POST` | `/api/v1/oauth2/applications/:appId/servers/:serverId` | Add own bot; optional `{permissions}` |
| `DELETE` | `/api/v1/oauth2/applications/:appId/servers/:serverId` | Remove bot; **keeps** managed role |
| `GET` | `/api/v1/oauth2/applications/:appId/servers` | List servers + `bot_permissions` / labels (read-only) |
| `PATCH` | `/api/v1/oauth2/applications/:appId/servers/:serverId/permissions` | Always **403** — owners cannot remotely edit server bot roles |
| `GET` | `/api/v1/oauth2/invite/:botUserId` | Public invite card (`default_permissions`, lock, labels, flags) |
| `POST` | `/api/v1/oauth2/invite/:botUserId/add/:serverId` | Authorize; body `{permissions}`; respects `public_invite` |

On authorize, ggbro creates (or reuses) a **managed role** named after the bot (without `[BOT]`). That role cannot be assigned to other users. Kick/ban/leave keeps the role until a server admin deletes it. Changing portal defaults does **not** change existing server roles. Bot moderation REST routes enforce the bot's role bits.

---

## Socket.IO Gateway

Standard **Socket.IO v4** — do **not** use raw WebSocket libraries alone.

After connect, join rooms to receive scoped events:

| Emit | Payload | Purpose |
|------|---------|---------|
| `server:join` | `"serverId"` (string) | Member/presence/server events |
| `server:leave` | `"serverId"` | Leave server room |
| `channel:join` | `"channelId"` | Receive `message:new` etc. (requires `VIEW_CHANNEL`) |
| `channel:leave` | `"channelId"` | Leave channel room |

### Python

```python
import socketio

sio = socketio.Client(reconnection=True)

@sio.event
def connect():
    print("Connected!")
    sio.emit("server:join", "YOUR_SERVER_ID")
    sio.emit("channel:join", "YOUR_CHANNEL_ID")

sio.connect("https://ggbro.app", auth={"token": "YOUR_BOT_TOKEN"})
sio.wait()
```

### JavaScript

```javascript
const { io } = require("socket.io-client");
const socket = io("https://ggbro.app", { auth: { token: "YOUR_BOT_TOKEN" } });

socket.on("connect", () => {
  console.log("Connected!");
  socket.emit("server:join", "YOUR_SERVER_ID");
  socket.emit("channel:join", "YOUR_CHANNEL_ID");
});
```

Bots are auto-placed in `user:<botUserId>` room (needed for `interaction:create` delivery).

---

## Slash Commands

Users type `/commandname` with a Discord-style autocomplete: left rail of bots/apps, commands grouped by bot, plus frequently used. Option types mirror Discord Application Command Option Types **3–10**.

Selecting a command with options opens an option builder: options appear **sequentially after the command is selected** (not while browsing); **Tab** cycles option fields; USER / CHANNEL / ROLE / MENTIONABLE options use searchable entity pickers (type to filter, arrows + Enter/Tab to pick).

**Client autocomplete ranking:** exact name → name prefix → name contains → description. Typing `/play` ranks `/play` above description matches like `/np` (“Now playing”).

### Registering (bot token bulk)

`POST /api/v1/oauth2/commands` — replaces all existing **global** commands for the app (**max 50**). Body: array of command objects.

Owner alternatives: `GET/POST /api/v1/oauth2/applications/:appId/commands`, `PUT/DELETE .../commands/:cmdId`.

Client list (authenticated user): `GET /api/v1/oauth2/servers/:serverId/commands` — all commands from bots in that server (includes `bot_id`, `bot_name`, `bot_avatar`, `app_name`).

### Command object

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | 1–32 chars, `^[\w-]{1,32}$`, stored lowercased |
| `description` | string | Shown in autocomplete |
| `options` | array | Up to **25** option definitions |
| `server_id` | string? | Optional server scope (owner registration only) |

### Option types (Discord-compatible)

| Type | Code | Aliases | Parsed value |
|------|------|---------|--------------|
| STRING | `3` | `string`, `str`, `text` | string (mentions unwrapped to UUID) |
| INTEGER | `4` | `integer`, `int` | int when parseable |
| BOOLEAN | `5` | `boolean`, `bool` | `true`/`false` from true/1/yes/y/on or false/0/no/n/off |
| USER | `6` | `user`, `member` | user UUID from `<@id>` or raw id |
| CHANNEL | `7` | `channel` | channel UUID from `<#id>` or raw id |
| ROLE | `8` | `role` | role UUID from `<@&id>` or raw id |
| MENTIONABLE | `9` | `mentionable` | user or role UUID |
| NUMBER | `10` | `number`, `float`, `double` | float when parseable |

Missing/unknown `type` defaults to STRING (`3`). The desktop/web client also infers USER/CHANNEL/ROLE from common option names (`user`, `member`, `channel`, `role`, …) when a bot registered them as plain STRING.

### Option fields

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Required. `^[\w-]{1,32}$`, lowercased |
| `description` | string | Max 100 chars |
| `type` | number \| string | Type code or alias |
| `required` | boolean | Default `false` |
| `choices` | array? | Up to 25 `{name, value}` (`value` string or number) |
| `autocomplete` | boolean? | Stored when `true` (UI hint; live bot autocomplete events not yet sent) |
| `min_value` / `max_value` | number? | INTEGER / NUMBER bounds |
| `max_length` | number? | STRING max length (clamped 1–6000) |
| `channel_types` | number[]? | CHANNEL filter hint (up to 10) |

### How clients send arguments

- **Current desktop/web wire (preferred):** positional values in option order — `/play --__bot <@botId> ovetto 7amra`. Chat history and delivered `args` are `/play ovetto 7amra` (routing flag stripped). A lone multi-word last option stays unquoted.
- **Named (manual):** `/ban --user <@uuid> --reason spamming --days 7`. Server parses these, strips surrounding quotes from values, then **normalizes** `args` to positional form before `interaction:create`.
- **Positional** (option order): `/ban <@uuid> spamming 7`
- **Mentions:** `<@userId>`, `<#channelId>`, `<@&roleId>` → bare UUIDs in `options`. Client also resolves bare member UUIDs / `<@id>` in slash option fields and the message box into `@username` tags when that member is on the server.
- **Choices:** user may pass choice `name` or `value`; delivered value is choice `value`
- **Quotes** when a later option is also filled: `/play "lo-fi hip hop" 80`
- If no option defs: raw args land in `options.value`
- **Bot routing (client):** when multiple bots share a command name, the desktop/web client sends `--__bot <@botUserId>` so the correct app receives the interaction. The server strips this flag before emitting `interaction:create` — bots never see `__bot` in `options` or `args`. Chat history stores the cleaned command text (without `--__bot`).

**Bot handler rule:** always prefer `data.options.<name>` (or SDK `ctx.get_option('name')`) over raw `data.args`. For `/play`, use `options.query` — never search a string that still contains `--query`.

Prefer unique command names across bots in the same server when possible. Do **not** register a user-facing option named `__bot`.

### Register typed commands (Python)

```python
import requests

BOT_TOKEN = "YOUR_BOT_TOKEN"
API_URL = "https://ggbro.app"

commands = [
    {"name": "ping", "description": "Check if the bot is alive", "options": []},
    {
        "name": "play",
        "description": "Play a song",
        "options": [
            {
                "name": "query",
                "description": "YouTube URL or search term",
                "type": "string",  # or 3
                "required": True,
                "max_length": 200,
            },
            {
                "name": "volume",
                "description": "Playback volume",
                "type": 4,  # INTEGER
                "required": False,
                "min_value": 0,
                "max_value": 200,
                "choices": [
                    {"name": "Quiet", "value": 50},
                    {"name": "Normal", "value": 100},
                    {"name": "Loud", "value": 150},
                ],
            },
        ],
    },
    {
        "name": "timeout",
        "description": "Timeout a member",
        "options": [
            {"name": "user", "description": "Member to timeout", "type": "user", "required": True},
            {"name": "minutes", "description": "Duration in minutes", "type": "integer", "required": True, "min_value": 1, "max_value": 40320},
            {"name": "reason", "description": "Moderation reason", "type": 3, "required": False},
        ],
    },
    {
        "name": "announce",
        "description": "Post to a channel",
        "options": [
            {"name": "channel", "type": "channel", "description": "Target channel", "required": True},
            {"name": "role", "type": "role", "description": "Role to ping", "required": False},
            {"name": "message", "type": "string", "description": "Announcement text", "required": True},
            {"name": "silent", "type": "boolean", "description": "Suppress mentions", "required": False},
        ],
    },
]

r = requests.post(
    f"{API_URL}/api/v1/oauth2/commands",
    json=commands,
    headers={"Authorization": f"Bearer {BOT_TOKEN}"},
)
print("Commands registered!" if r.ok else f"Failed: {r.text}")
```

### Register typed commands (JavaScript)

```javascript
const axios = require("axios");

const BOT_TOKEN = "YOUR_BOT_TOKEN";
const API_URL = "https://ggbro.app";

axios.post(`${API_URL}/api/v1/oauth2/commands`, [
  { name: "ping", description: "Check bot latency", options: [] },
  {
    name: "ban",
    description: "Ban a member",
    options: [
      { name: "user", description: "User to ban", type: "user", required: true },
      {
        name: "reason",
        description: "Ban reason",
        type: 3,
        required: false,
        choices: [
          { name: "Spam", value: "spam" },
          { name: "Harassment", value: "harassment" },
          { name: "Other", value: "other" },
        ],
      },
      { name: "silent", description: "Skip public notice", type: "boolean" },
    ],
  },
  {
    name: "move",
    description: "Move a user to a voice channel",
    options: [
      { name: "user", type: 6, description: "Member", required: true },
      { name: "channel", type: 7, description: "Voice channel", required: true, channel_types: [2] },
    ],
  },
], {
  headers: { Authorization: `Bearer ${BOT_TOKEN}` },
}).then(() => console.log("Commands registered!"));
```

### Receiving & responding — `interaction:create` (slash)

When a user runs a slash command, the bot receives `interaction:create` with typed `options`. Respond with `interaction:response`.

**Payload shape (`type: "slash_command"`):**

```json
{
  "id": "interaction-uuid",
  "type": "slash_command",
  "command": "play",
  "args": "ovetto 7amra",
  "options": {
    "query": "ovetto 7amra"
  },
  "channel_id": "channel-uuid",
  "server_id": "server-uuid",
  "message_id": "original-user-message-id",
  "user": { "id": "user-uuid", "username": "JadaDev" }
}
```

Named `--option` wires from older clients are normalized into the same positional `args` + typed `options` before delivery (quotes around named values are stripped).

For multi-option commands (e.g. timeout), prefer `options`:

```json
{
  "command": "timeout",
  "args": "<@abc-...> 10 spam",
  "options": {
    "user": "abc-uuid-here",
    "minutes": 10,
    "reason": "spam"
  }
}
```

**Key fields:**

| Field | Use |
|-------|-----|
| `data.id` | Pass as `interaction_id` in response |
| `data.command` | Slash command name |
| `data.args` | Clean positional text after the command (never includes `--__bot`; named flags normalized away) |
| `data.options` | Typed map (strings / numbers / booleans; IDs unwrapped) — **prefer this** |
| `data.channel_id` / `server_id` / `user` / `message_id` | Context |

Python helper SDK (`/developers/ggbro.py` v1.2+): `ctx.get_option('query', ctx.args)`.

**Python handler:**

```python
@sio.on("interaction:create")
def on_interaction(data):
    if data.get("type") != "slash_command":
        return
    cmd = (data.get("command") or "").lower()
    opts = data.get("options") or {}

    if cmd == "ping":
        sio.emit("interaction:response", {
            "interaction_id": data["id"],
            "channel_id": data["channel_id"],
            "content": "Pong! 🏓",
        })
    elif cmd == "timeout":
        target = opts.get("user")
        minutes = opts.get("minutes")
        reason = opts.get("reason") or "No reason"
        sio.emit("interaction:response", {
            "interaction_id": data["id"],
            "channel_id": data["channel_id"],
            "content": f"Timed out {target} for {minutes}m ({reason})",
        })
```

**Ack:** `interaction:response` returns `{ok, message_id, message}` when an ack callback is used.

---

## Sending Messages

### Socket.IO `bot:send_message`

```python
sio.emit("bot:send_message", {
    "channel_id": "YOUR_CHANNEL_ID",
    "content": "Hello from my bot! 🤖"
})
```

```javascript
socket.emit("bot:send_message", {
  channel_id: "YOUR_CHANNEL_ID",
  content: "Hello from my bot! 🤖"
});
```

Ack: `{ok, message_id, message}`. Store `message_id` to edit later.

### Edit — `bot:edit_message`

```javascript
socket.emit("bot:edit_message", {
  channel_id: channelId,
  message_id: existingMessageId,
  content: "Updated content",
  embeds: [{
    title: "Updated Panel",
    description: "This embed was edited in place.",
    color: "#23A55A"
  }]
});
```

### Delete — `bot:delete_message`

```json
{ "message_id": "...", "channel_id": "..." }
```

### REST alternatives

- Multipart: `POST /api/v1/messages/:channelId/upload`
- Bot REST: `POST /api/v1/oauth2/bot/channels/:channelId/messages`

Need **content and/or embeds** (cannot be empty).

---

## Embeds & Components

Rich embeds: colored sidebar, title, description, fields, images, footer. Use for `/serverinfo`, now-playing, help, etc.

### Embed object

| Field | Type | Description |
|-------|------|-------------|
| `title` | string? | Title (link if `url` set) |
| `description` | string? | Body |
| `url` | string? | Title link |
| `color` | string? | Hex border, e.g. `"#5865F2"` |
| `fields` | array? | `{name, value, inline?}` |
| `thumbnail` | string? | Small image URL |
| `image` | string? | Large image URL |
| `footer` | object? | `{text, icon_url?}` |
| `author` | object? | `{name, url?, icon_url?}` |
| `timestamp` | string? | ISO-8601 next to footer |
| `components` | array? | Interactive components under embed |

### Interactive components

| Type | Required | Notes |
|------|----------|-------|
| `button` | `{type, label, custom_id}` | `style`: `primary`, `secondary`, `success`, `danger`, `link`. Link buttons use `url` instead of `custom_id`. |
| `string_select` | `{type, custom_id, options[]}` | Options: `{label, value, description?, emoji?, default?}` |
| `channel_select` | `{type, custom_id}` | `channel_types` e.g. `["text"]` |

Pass `embeds` on `bot:send_message` or `interaction:response`. Content may be empty if embeds present.

### Example send

```python
sio.emit("bot:send_message", {
    "channel_id": channel_id,
    "content": "",
    "embeds": [{
        "title": "Hello World",
        "description": "This is a rich embed message!",
        "color": "#5865F2",
        "fields": [
            {"name": "Field 1", "value": "Some value", "inline": True},
            {"name": "Field 2", "value": "Another value", "inline": True},
        ],
        "footer": {"text": "Powered by ggbro"}
    }]
})
```

### Example with components (JS)

```javascript
socket.emit("bot:send_message", {
  channel_id: channelId,
  content: "",
  embeds: [{
    title: "Now Playing 🎵",
    description: `Duration: 3:45 | Requested by JadaDev`,
    color: "#FF0000",
    thumbnail: "https://i.ytimg.com/vi/.../hqdefault.jpg",
    author: { name: "Music Bot" },
    footer: { text: "Queue: 5 songs remaining" },
    components: [
      { type: "button", custom_id: "music:pause", label: "Pause", style: "secondary", emoji: { name: "⏸️" } },
      { type: "button", custom_id: "music:skip", label: "Skip", style: "primary", emoji: { name: "⏭️" } },
      {
        type: "string_select",
        custom_id: "music:volume",
        placeholder: "Set volume",
        options: [
          { label: "50%", value: "50" },
          { label: "100%", value: "100", default: true },
          { label: "150%", value: "150" }
        ]
      },
      {
        type: "channel_select",
        custom_id: "music:panel-channel",
        placeholder: "Move panel to a text channel",
        channel_types: ["text"]
      }
    ]
  }]
});
```

### Component interactions — `interaction:create` (`message_component`)

Clients emit `message:component`; the message author bot receives:

```json
{
  "id": "interaction-uuid",
  "type": "message_component",
  "component_type": "button",
  "custom_id": "music:skip",
  "values": [],
  "selected_channels": [],
  "channel_id": "...",
  "server_id": "...",
  "user": { "id": "...", "username": "..." },
  "message_id": "..."
}
```

For `channel_select`, `selected_channels` is `[{id, name, type}, ...]`. For `string_select`, `values` holds selected option values.

```javascript
socket.on("interaction:create", async (data) => {
  if (data.type !== "message_component") return;

  if (data.custom_id === "music:skip") {
    socket.emit("bot:edit_message", {
      channel_id: data.channel_id,
      message_id: data.message_id,
      embeds: [{ title: "Skipped", description: "Queue advanced.", color: "#5865F2" }]
    });
  }

  if (data.custom_id === "music:panel-channel") {
    const selected = data.selected_channels?.[0];
    if (!selected) return;
    socket.emit("bot:send_message", {
      channel_id: selected.id,
      embeds: [{ title: "Panel moved", description: "Controls are now posted in this channel." }]
    });
  }
});
```

---

## File Uploads & Attachments

Prefer Bot REST for text/embeds; use upload for binary attachments.

### `POST /api/v1/messages/:channelId/upload`

Multipart form-data:

- `content` — optional text
- `files` — file parts
- `replyTo` — optional
- `remoteUrl` — optional Tenor / direct GIF URL

```python
import requests

r = requests.post(
    f"{API_URL}/api/v1/messages/{channel_id}/upload",
    headers={"Authorization": f"Bearer {BOT_TOKEN}"},
    data={"content": "Here is a file"},
    files=[("files", ("report.png", open("report.png", "rb"), "image/png"))],
)
print(r.json())
```

### Attachment shape on messages

```json
{
  "id": "attachment-uuid",
  "filename": "/uploads/attachments/...",
  "original_name": "report.png",
  "mime_type": "image/png",
  "size": 12345
}
```

Limits:

- Default max file size: **25 MB**
- Verified servers or GGBRO Plus: **500 MB**
- Metadata: `GET /api/v1/messages/attachment/:attachmentId`
- DM: `POST /api/v1/dm/:targetUserId/upload`
- Group DM: `POST /api/v1/dm/groups/:groupId/upload`

---

## Bot REST API

Base path: **`/api/v1/oauth2/bot`**. Requires bot token (`is_bot`). Prefer these over user routes when writing bots.

### Messages & DMs

| Method | Path | Body / query | Notes |
|--------|------|--------------|-------|
| `POST` | `/api/v1/oauth2/bot/channels/:channelId/messages` | `{content?, embeds?, reply_to?}` | Need content and/or embeds |
| `PUT` | `/api/v1/oauth2/bot/channels/:channelId/messages/:messageId` | `{content?, embeds?}` | Edit own message |
| `DELETE` | `/api/v1/oauth2/bot/channels/:channelId/messages/:messageId` | — | Delete own message |
| `POST` | `/api/v1/oauth2/bot/channels/:channelId/messages/bulk-delete` | `{message_ids: [...]}` | Max 100 |
| `GET` | `/api/v1/oauth2/bot/channels/:channelId/messages` | `?before=&after=&limit=` | limit 1–100, default 50 |
| `POST` | `/api/v1/oauth2/bot/dm/:targetUserId` | `{content?, embeds?}` | Shared server required |

```json
{
  "content": "Hello",
  "embeds": [{ "title": "Status", "description": "Online", "color": "#23A55A" }],
  "reply_to": "optional-message-id"
}
```

### Servers, members, roles, invites, presence, users

| Method | Path | Notes |
|--------|------|-------|
| `GET` | `/api/v1/oauth2/bot/servers/:serverId` | Server + `member_count` |
| `GET` | `/api/v1/oauth2/bot/servers/:serverId/channels` | List channels |
| `GET` | `/api/v1/oauth2/bot/servers/:serverId/members` | List members |
| `GET` | `/api/v1/oauth2/bot/servers/:serverId/members/:userId` | Member + `roles` |
| `DELETE` | `/api/v1/oauth2/bot/servers/:serverId/members/:userId` | Kick → emits `member:left` |
| `POST` | `/api/v1/oauth2/bot/servers/:serverId/bans` | `{user_id, reason?}` |
| `DELETE` | `/api/v1/oauth2/bot/servers/:serverId/bans/:userId` | Unban |
| `GET` | `/api/v1/oauth2/bot/servers/:serverId/roles` | List roles |
| `POST` | `/api/v1/oauth2/bot/servers/:serverId/members/:userId/roles/:roleId` | Assign role |
| `DELETE` | `/api/v1/oauth2/bot/servers/:serverId/members/:userId/roles/:roleId` | Remove role |
| `POST` | `/api/v1/oauth2/bot/servers/:serverId/invites` | `{max_uses?, expires_in?}` seconds |
| `GET` | `/api/v1/oauth2/bot/servers/:serverId/invites` | List invites |
| `DELETE` | `/api/v1/oauth2/bot/servers/:serverId/invites/:inviteId` | Delete invite |
| `PUT` | `/api/v1/oauth2/bot/presence` | See below |
| `GET` | `/api/v1/oauth2/bot/users/:userId` | Public user profile |

**Presence body:**

```json
{
  "status": "online",
  "activity": { "type": "playing", "name": "with slash commands", "details": null, "state": null, "started_at": 1710000000 }
}
```

`status`: `online` | `idle` | `dnd` | `offline`. `activity.type`: `playing` | `listening` | `watching` | `competing` | `custom`. Set `activity` to `null` to clear.

**Important:** Gateway disconnect clears persisted human/bot `activity_json`. After reconnect, call `PUT /api/v1/oauth2/bot/presence` again (or emit activity via your client) if you want activity to show. Human desktop Activity is re-detected from running processes and is never restored blindly from the database.

**Channel messages (Bot REST):** `POST /api/v1/oauth2/bot/channels/:channelId/messages` requires `VIEW_CHANNEL` + `SEND_MESSAGES`. For **voice** channels, `chat_enabled` must be `1` or the API returns `403` (`Text chat is disabled for this voice channel`). The same rule applies to Socket.IO `bot:send_message` and `interaction:response`.

**Timeouts** are on shared routes (`POST/DELETE /api/v1/servers/:id/timeouts...`), not under `/oauth2/bot`. Bots with moderator permissions (or admin bot role) can call them with the bot token.

---

## Events Reference

Join `server:join` / `channel:join` for scoped events.

### Events your bot receives

| Event | When | Payload (summary) |
|-------|------|-------------------|
| `interaction:create` | Slash or component | See Slash / Embeds |
| `message:new` | New channel message | Full message + author, `embeds?`, `attachments?` |
| `message:update` | Edited | Hydrated message |
| `message:deleted` | Deleted | `{channelId, messageId}` |
| `message:reaction` | Reaction add/remove | `{channelId, messageId, emoji, userId, action}` (`added`/`removed`) |
| `message:pin` | Pin toggled | `{channelId, messageId, pinned}` |
| `member:joined` | Join | `{serverId, userId, username, avatar}` |
| `member:left` | Leave / kick | `{serverId, userId}` |
| `member:banned` | Ban | `{serverId, userId}` |
| `member:timeout` | Timeout | `{serverId, userId, expiresAt}` |
| `member:role-updated` | Built-in role change | `{serverId, userId, role}` |
| `member:roles-updated` | Custom role assign/unassign | `{serverId, userId}` |
| `role:created` / `role:updated` / `role:deleted` | Role CRUD | Role object or `roleId` |
| `channel:created` / `channel:updated` / `channel:deleted` | Channel CRUD | Channel payload |
| `channel:permissions-updated` | Channel override | Override or reset |
| `category:permissions-updated` | Category override | Override or reset |
| `user:status` | Presence | `{userId, status, customStatus?}` |
| `typing:start` / `typing:stop` | Channel typing | `{userId, username?, channelId}` |
| `dm:new` | New DM | DM message |
| `dm:reaction` | DM reaction | Reaction payload |
| `dm:typing:start` / `dm:typing:stop` | DM typing | `{userId, ...}` |
| `voice:existing` | After voice join | Participant array |
| `voice:user-joined` / `voice:user-left` | Voice membership | Participant / `{socketId}` (includes `meetingRole`, `handRaised` for meetings) |
| `voice:meeting-update` | Meeting role/hand | `{socketId, userId, meetingRole, handRaised}` |
| `voice:state-update` | Mute/deafen/video | State fields |
| `voice:speaking` | Speaking indicator | `{socketId, speaking}` |
| `voice:admin-mute-update` / `voice:admin-deafen-update` | Server mute/deafen | Target + flags |
| `voice:force-leave` / `voice:force-disconnect` / `voice:force-move` | Moderation / multi-device | Reason / target channel |
| `voice:signal` | WebRTC signal relay | Peer signaling |
| `voice:error` | Voice permission failure | `{error}` |
| `bot:audio-data` | PCM from another bot | `{pcm, sampleRate, channels, botUserId}` |

### Events your bot can emit

| Event | Payload | Description |
|-------|---------|-------------|
| `interaction:response` | `{interaction_id, channel_id, content?, embeds?}` | Reply (ack `{ok, message_id, message}`) |
| `bot:send_message` | `{channel_id, content?, embeds?}` | Send channel message |
| `bot:edit_message` | `{message_id, channel_id?, content?, embeds?}` | Edit own message |
| `bot:delete_message` | `{message_id, channel_id?}` | Delete own message |
| `server:join` / `server:leave` | `serverId` | Server room |
| `channel:join` / `channel:leave` | `channelId` | Channel room |
| `voice:join` | `{channelId}` | Join voice |
| `voice:leave` | (none) | Leave voice |
| `voice:raise-hand` | `{channelId}` | Meeting: raise hand (requires SPEAK) |
| `voice:lower-hand` | `{channelId, targetUserId?}` | Meeting: lower own/others' hand |
| `voice:set-meeting-role` | `{channelId, userId, role}` | Meeting: set `audience` or `speaker` |
| `voice:state` | mute/deafen/video flags | Update own voice state |
| `voice:signal` | WebRTC payload | Peer signaling |
| `bot:voice-query` | `{userId, serverId}` | Where is user in voice? |
| `bot:voice-participants` | `{channelId}` | List participants |
| `bot:audio-data` | `{channelId, pcm, sampleRate?, channels?}` | Relay PCM into voice room |
| `typing:start` / `typing:stop` | `channelId` | Typing indicator |
| `status:set` | `{status}` | Set presence |

### Example message object (`message:new`)

```json
{
  "id": "message-uuid",
  "channel_id": "channel-uuid",
  "user_id": "author-uuid",
  "content": "Hello",
  "created_at": 1710000000,
  "updated_at": null,
  "username": "Author",
  "avatar": "/uploads/avatars/...",
  "is_bot": 0,
  "embeds": [],
  "attachments": [],
  "reply_to": null
}
```

---

## REST API Overview

- Base: `https://ggbro.app/api`
- Auth: `Authorization: Bearer <token>` (except public invite/stats/emoji file where noted)
- Mounted routers: `/api/v1/auth`, `/api/v1/servers`, `/api/v1/channels`, `/api/v1/messages`, `/api/v1/users`, `/api/v1/friends`, `/api/v1/dm`, `/api/v1/dm/groups`, `/api/v1/roles`, `/api/v1/oauth2`, `/api/v1/nitro`, `/api/v1/gifs`, `/api/v1/admin`, `/api/v1/reports`, `/api/v1/payments`, plus `GET /api/v1/stats`, `GET /api/v1/emojis/:id/file`

Bots typically use `/api/v1/oauth2/bot/...` plus shared server/channel/message routes when they have membership and permissions.

---

## Servers

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/servers` | List joined servers |
| `POST` | `/api/v1/servers` | Create server |
| `GET` | `/api/v1/servers/:id` | Server details |
| `PUT` | `/api/v1/servers/:id` | Update server |
| `DELETE` | `/api/v1/servers/:id` | Delete server (2FA) |
| `GET` | `/api/v1/servers/:id/members` | Members + roles/avatars/status |
| `PUT` | `/api/v1/servers/reorder` | Reorder server list |
| `POST` | `/api/v1/servers/:id/icon` | Upload server icon (PNG/JPG/WEBP/**GIF animated**). Manage-server. Keeps original extension |
| `POST` | `/api/v1/servers/:id/banner` | Upload banner (PNG/JPG/WEBP/**GIF animated**). Manage-server. Keeps original extension |
| `PATCH` | `/api/v1/servers/:id/banner-offset` | Banner offset |
| `PATCH` | `/api/v1/servers/:id/settings` | Server settings |
| `POST` | `/api/v1/servers/:id/vanity` | Vanity URL (2FA) |
| `DELETE` | `/api/v1/servers/:id/leave` | Leave server |
| `GET` | `/api/v1/servers/:id/gifs` | Custom GIFs/stickers |
| `POST` | `/api/v1/servers/:id/gifs` | Upload GIF/sticker |
| `DELETE` | `/api/v1/servers/:id/gifs/:gifId` | Delete GIF/sticker |
| `POST` | `/api/v1/servers/:id/mute` | Mute server `{duration}` |
| `DELETE` | `/api/v1/servers/:id/mute` | Unmute |
| `GET` | `/api/v1/servers/me/muted` | Muted servers |
| `POST` | `/api/v1/servers/:serverId/read` | Mark server read |

Bot shortcut: `GET /api/v1/oauth2/bot/servers/:serverId`.

---

## Audit Log

Requires admin / `ADMINISTRATOR` (or `MANAGE_SERVER`) on the bot's managed role. New bots no longer get Administrator by default — request it via the invite bitfield / Developer Portal if needed.

### `GET /api/v1/servers/:id/audit-log`

Query: `?limit=50` (max 100), `?before=<unix_timestamp>` for pagination.

**Response:**

```json
{
  "entries": [{
    "id": "...",
    "server_id": "...",
    "actor_id": "moderator-or-user-uuid",
    "actor_username": "ModName",
    "actor_avatar": "/uploads/...",
    "action": "ROLE_ASSIGN",
    "target_type": "member",
    "target_id": "member-user-uuid",
    "target_username": "Victim",
    "changes": {
      "role": "Member",
      "roleId": "role-uuid",
      "username": "Victim",
      "userId": "member-user-uuid"
    },
    "created_at": 1710000000
  }],
  "hasMore": false
}
```

`target_username` is always resolved from `target_id` when `target_type` is `member` (even for older entries that only stored `role` / `roleId`).

### Recorded actions

| Action | Notes |
|--------|-------|
| `MEMBER_JOIN` | Human or bot; may include invite metadata |
| `MEMBER_LEAVE` | Voluntary leave |
| `MEMBER_KICK` | `changes.moderator` + target username |
| `MEMBER_BAN` / `MEMBER_UNBAN` | Ban includes reason + moderator |
| `MEMBER_TIMEOUT` / `MEMBER_TIMEOUT_REMOVE` | Duration, reason, moderator |
| `ROLE_CREATE` / `ROLE_UPDATE` / `ROLE_DELETE` | Role definition changes |
| `ROLE_ASSIGN` / `ROLE_UNASSIGN` | Custom role add/remove. `target_id` = member user id. `changes`: `{ role, roleId, username, userId }` |
| `MEMBER_ROLE_UPDATE` | Built-in role (`oldRole`/`newRole`) plus `{ username, userId }` |
| `MESSAGE_EDIT` / `MESSAGE_DELETE` / `MESSAGE_PIN` / `MESSAGE_UNPIN` | Messages |
| `INVITE_CREATE` / `INVITE_DELETE` / `INVITE_REGENERATE` | Invites |
| `SERVER_CREATE` / `SERVER_UPDATE` / `SERVER_DELETE` / `SERVER_ICON` / `SERVER_BANNER` / `SERVER_VANITY` | Server settings |
| `SERVER_EMOJI_UPLOAD` / `SERVER_EMOJI_DELETE` / `SERVER_EMOJI_RENAME` | Emojis |
| `SERVER_SOUNDBOARD_UPLOAD` / `SERVER_SOUNDBOARD_UPDATE` / `SERVER_SOUNDBOARD_DELETE` | Soundboard |

---

## Members Moderation

Shared routes (bot token works with membership + permissions). Prefer Bot REST kick/ban when available; **timeouts use these routes**.

| Method | Path | Body / notes |
|--------|------|--------------|
| `DELETE` | `/api/v1/servers/:id/members/:targetUserId` | Kick. Needs mod or `KICK_MEMBERS`. Emits `member:left`. Audit `MEMBER_KICK`. |
| `POST` | `/api/v1/servers/:id/bans` | `{targetUserId, reason?}`. Emits `member:left` + `member:banned`. Audit `MEMBER_BAN`. |
| `DELETE` | `/api/v1/servers/:id/bans/:targetUserId` | Unban. Audit `MEMBER_UNBAN`. |
| `GET` | `/api/v1/servers/:id/bans` | List bans |
| `POST` | `/api/v1/servers/:id/timeouts` | `{targetUserId, duration, reason?}`. Duration **seconds** (min 60, max 28 days). Emits `member:timeout`. Response includes `expiresAt`. |
| `DELETE` | `/api/v1/servers/:id/timeouts/:targetUserId` | Remove timeout. Audit `MEMBER_TIMEOUT_REMOVE`. |
| `PUT` | `/api/v1/servers/:id/members/:targetUserId/role` | Built-in role `{role: "admin"\|...}` |

```python
requests.post(
    f"{API_URL}/api/v1/servers/{server_id}/timeouts",
    headers={"Authorization": f"Bearer {BOT_TOKEN}"},
    json={"targetUserId": user_id, "duration": 600, "reason": "spam"},
)
```

Bot aliases: `DELETE /api/v1/oauth2/bot/servers/:serverId/members/:userId`, `POST .../bans` with `{user_id}`, `DELETE .../bans/:userId`.

---

## Channels

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/channels/:serverId` | List channels (permission-filtered) + categories |
| `GET` | `/api/v1/channels/id/:channelId` | Get by ID |
| `POST` | `/api/v1/channels/:serverId` | Create `{name, type: "text"\|"voice", category_id?, is_meeting?, chat_enabled?}` — voice only for meeting/chat flags; meeting defaults chat on |
| `PUT` | `/api/v1/channels/:id` | Update `{name?, category_id?, topic?, slowmode?, nsfw?, is_meeting?, chat_enabled?}` — cannot change type |
| `POST` | `/api/v1/channels/:channelId/duplicate` | Duplicate (copies overrides) |
| `PUT` | `/api/v1/channels/:id` | Update name/topic/slowmode/etc. |
| `DELETE` | `/api/v1/channels/:id` | Delete |
| `PUT` | `/api/v1/channels/:serverId/reorder` | Reorder channels |
| `POST` | `/api/v1/channels/:serverId/categories` | Create category |
| `PUT` | `/api/v1/channels/categories/:id` | Rename category |
| `DELETE` | `/api/v1/channels/categories/:id` | Delete category (channels uncategorized) |
| `PUT` | `/api/v1/channels/:serverId/categories/reorder` | `{order: ["catId1", ...]}` |

Bot list: `GET /api/v1/oauth2/bot/servers/:serverId/channels`.

---

## Channel & Category Permission Overrides

`allow` and `deny` are bitmasks ([Permissions Bitfield](#permissions-bitfield)). **Deny wins** over allow for a given bit.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/channels/:channelId/permissions` | List channel overrides |
| `PUT` | `/api/v1/channels/:channelId/permissions/:roleId` | `{allow, deny}` → emits `channel:permissions-updated` |
| `DELETE` | `/api/v1/channels/:channelId/permissions/:roleId` | Reset override |
| `GET` | `/api/v1/channels/categories/:categoryId/permissions` | List category overrides |
| `PUT` | `/api/v1/channels/categories/:categoryId/permissions/:roleId` | Set category override → `category:permissions-updated` |
| `DELETE` | `/api/v1/channels/categories/:categoryId/permissions/:roleId` | Reset category override |

**Hide channel from @everyone, allow staff:**

```json
// PUT /api/v1/channels/:channelId/permissions/:everyoneRoleId
{ "allow": 0, "deny": 2048 }

// PUT /api/v1/channels/:channelId/permissions/:staffRoleId
{ "allow": 2048, "deny": 0 }
```

(`2048` = `VIEW_CHANNEL`)

---

## Messages

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/messages/:channelId` | Fetch (~50/page). `?before=messageId` |
| `GET` | `/api/v1/messages/id/:messageId` | Single message |
| `POST` | `/api/v1/messages/:channelId/upload` | Send + optional files (multipart) |
| `POST` | `/api/v1/messages/:channelId/gif` | Send GIF by URL/id |
| `PUT` | `/api/v1/messages/:id` | Edit `{content}`. Author or manage-messages |
| `DELETE` | `/api/v1/messages/:id` | Delete. Author or manage-messages |
| `GET` | `/api/v1/messages/:channelId/search` | `?q=&limit=25` |
| `GET` | `/api/v1/messages/attachment/:attachmentId` | Attachment metadata |
| `POST` | `/api/v1/messages/:channelId/read` | Mark read |

Prefer Bot REST for bot text/embeds without multipart.

---

## Members & Users

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/users/me/info` | Current user/bot: id, username, email, avatar, banner, bio, status, is_bot, created_at |
| `GET` | `/api/v1/users/:id` | Public user |
| `GET` | `/api/v1/users/:id/profile` | Full profile + mutuals |
| `PUT` | `/api/v1/users/me/profile` | `{username?, bio?}` (bio max 500) |
| `POST` | `/api/v1/users/me/avatar` | Multipart `avatar` — PNG/JPG/WEBP/**GIF animated** (≤2MB); original extension kept |
| `POST` | `/api/v1/users/me/banner` | Multipart `banner` — PNG/JPG/WEBP/**GIF animated** (≤5MB); original extension kept |
| `PUT` | `/api/v1/users/me/password` | `{currentPassword, newPassword}` |
| `GET` | `/api/v1/users/me/notifications` | Notifications |
| `POST` | `/api/v1/users/me/notifications` | Create/update prefs (as implemented) |
| `GET` | `/api/v1/users/me/connections` | Linked connections |
| `GET` | `/api/v1/users/me/muted-users` | Muted users |
| `POST` / `DELETE` | `/api/v1/users/me/muted-users/:targetId` | Mute / unmute user |

Bot profile shortcut: `GET /api/v1/oauth2/bot/users/:userId`.

---

## Friends

Primarily human-oriented; bots may call with a bot token. No separate bot-friends API.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/friends` | Accepted friends |
| `GET` | `/api/v1/friends/search` | `?q=username` |
| `POST` | `/api/v1/friends/request` | `{username}` |
| `POST` | `/api/v1/friends/accept/:friendId` | Accept |
| `DELETE` | `/api/v1/friends/:friendId` | Remove / reject |
| `GET` | `/api/v1/friends/voice-activity` | Friends in voice |

---

## Direct Messages

Prefer `POST /api/v1/oauth2/bot/dm/:targetUserId` for bots (shared server required). General DM API:

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/dm/channels` | List DM channels |
| `POST` | `/api/v1/dm/channels/:targetUserId` | Open/create |
| `GET` | `/api/v1/dm/:targetUserId/messages` | `?before=` / `?around=` |
| `POST` | `/api/v1/dm/:targetUserId/messages` | `{content}` |
| `POST` | `/api/v1/dm/:targetUserId/upload` | Multipart |
| `POST` | `/api/v1/dm/:targetUserId/gif` | GIF |
| `PUT` | `/api/v1/dm/:targetUserId/messages/:messageId` | Edit |
| `DELETE` | `/api/v1/dm/:targetUserId/messages/:messageId` | Delete |
| `POST` | `/api/v1/dm/:targetUserId/read` | Mark read |
| `GET` | `/api/v1/dm/:targetUserId/search` | `?q=` |
| `GET` | `/api/v1/dm/:targetUserId/pins` | Pins |
| `POST` / `DELETE` | `/api/v1/dm/:targetUserId/pins/:messageId` | Pin / unpin |

Socket: `dm:new`, `dm:reaction`, `dm:typing:*`, and DM call events (`dm:call-incoming`, etc.).

---

## Group DMs

Under `/api/v1/dm/groups`. User/group feature; bots are not specially provisioned, but a bot that is a member can use the same routes.

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/dm/groups` | List |
| `POST` | `/api/v1/dm/groups` | Create (members / name) |
| `PATCH` | `/api/v1/dm/groups/:groupId` | Update metadata |
| `POST` | `/api/v1/dm/groups/:groupId/icon` | Icon upload |
| `DELETE` | `/api/v1/dm/groups/:groupId/icon` | Remove icon |
| `POST` | `/api/v1/dm/groups/:groupId/members` | Add members |
| `DELETE` | `/api/v1/dm/groups/:groupId/members/:memberId` | Remove member |
| `POST` | `/api/v1/dm/groups/:groupId/owner` | Transfer ownership |
| `GET` | `/api/v1/dm/groups/:groupId/messages` | Fetch |
| `POST` | `/api/v1/dm/groups/:groupId/messages` | Send |
| `POST` | `/api/v1/dm/groups/:groupId/upload` | Attachments |
| `POST` | `/api/v1/dm/groups/:groupId/gif` | GIF |
| `PUT` | `/api/v1/dm/groups/:groupId/messages/:messageId` | Edit |
| `DELETE` | `/api/v1/dm/groups/:groupId/messages/:messageId` | Delete |
| `GET` | `/api/v1/dm/groups/:groupId/search` | Search |
| `POST` | `/api/v1/dm/groups/:groupId/read` | Mark read |
| `POST` | `/api/v1/dm/groups/:groupId/leave` | Leave |
| `DELETE` | `/api/v1/dm/groups/:groupId` | Delete (owner) |

---

## Roles

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/roles/:serverId` | List roles |
| `GET` | `/api/v1/roles/:serverId/role/:roleId` | One role |
| `POST` | `/api/v1/roles/:serverId` | Create `{name, color, permissions, hoist?}` |
| `PUT` | `/api/v1/roles/:serverId/:roleId` | Update |
| `DELETE` | `/api/v1/roles/:serverId/:roleId` | Delete |
| `PUT` | `/api/v1/roles/:serverId/reorder` | Array of `{id, position}` |
| `POST` | `/api/v1/roles/:serverId/assign` | `{targetUserId, roleId}` → `member:roles-updated` |
| `DELETE` | `/api/v1/roles/:serverId/assign/:targetUserId/:roleId` | Unassign |
| `GET` | `/api/v1/roles/:serverId/member/:targetUserId` | Roles on member |

Bot shortcuts: `POST/DELETE /api/v1/oauth2/bot/servers/:serverId/members/:userId/roles/:roleId`, `GET .../roles`.

---

## Permissions Bitfield

| Bit | Name | Value |
|-----|------|-------|
| 0 | `MANAGE_CHANNELS` | 1 |
| 1 | `MANAGE_ROLES` | 2 |
| 2 | `KICK_MEMBERS` | 4 |
| 3 | `BAN_MEMBERS` | 8 |
| 4 | `MANAGE_MESSAGES` | 16 |
| 5 | `MANAGE_SERVER` | 32 |
| 6 | `MUTE_MEMBERS` | 64 |
| 7 | `DEAFEN_MEMBERS` | 128 |
| 8 | `MOVE_MEMBERS` | 256 |
| 9 | `ADMINISTRATOR` | 512 |
| 10 | `ADD_REACTIONS` | 1024 |
| 11 | `VIEW_CHANNEL` | 2048 |
| 12 | `SEND_MESSAGES` | 4096 |
| 13 | `CONNECT` | 8192 |
| 14 | `MANAGE_REACTIONS` | 16384 |
| 15 | `SPEAK` | 32768 | Request/become speaker in meeting voice channels |
| 16 | `ATTACH_FILES` | 65536 | Upload images, videos, files, and GIFs |
| 17 | `EMBED_LINKS` | 131072 | Post rich embeds / link embeds (bots & embed payloads) |
| 18 | `READ_MESSAGE_HISTORY` | 262144 | Fetch/search/pin history (not live send) |
| 19 | `MENTION_EVERYONE` | 524288 | Use `@everyone` / `@here` |
| 20 | `USE_EXTERNAL_EMOJIS` | 1048576 | Custom emoji from other servers |
| 21 | `STREAM` | 2097152 | Camera / screen share in voice |
| 22 | `USE_SOUNDBOARD` | 4194304 | Play soundboard sounds in voice |
| 23 | `USE_APPLICATION_COMMANDS` | 8388608 | Run slash commands |

Voice channels support `is_meeting` (meeting-style audience/speakers) and `chat_enabled` (text chat pane). Sending messages to a voice channel requires `chat_enabled=1` plus `VIEW_CHANNEL` and `SEND_MESSAGES` (humans and bots). Meeting joiners start as **audience** (muted); use raise-hand / invite-to-speak to promote.

Uploading files/GIFs also requires `ATTACH_FILES`. Slash commands require `USE_APPLICATION_COMMANDS`. Video/screen share requires `STREAM`. Soundboard play requires `USE_SOUNDBOARD`.

Invited bots get a dedicated managed role named after the bot, with the **permissions bitfield from the invite link**. Set defaults in the Developer Portal (including Administrator if you want). Default for new bots is least-privilege `8895488`. Changing defaults does **not** change existing server roles — only new invites. Bot owners cannot remotely edit a server's bot role (`PATCH .../servers/:serverId/permissions` returns 403); server admins do that in Server Settings. When the bot leaves, the role remains until deleted. Invite URL: `/invite?bot=<botUserId>&permissions=<bitfield>`. Combine with bitwise OR, e.g. `KICK_MEMBERS | BAN_MEMBERS | MUTE_MEMBERS = 4|8|64 = 76`. Bot REST moderation (kick/ban/bulk-delete/etc.) requires the corresponding bits on the bot's role.

Channel overrides use the same bits in `allow` / `deny`. Default is allow unless an applicable **deny** remains after role **allow** overrides.

---

## Emojis

Format in content/reactions: `<:name:id>`. Also Unicode (`👍`).

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/servers/:id/emojis` | List (membership). Response includes `emojis[]`, `canManage` |
| `GET` | `/api/v1/servers/:id/emojis/public` | Public list (no membership) |
| `POST` | `/api/v1/servers/:id/emojis` | Upload multipart field `emoji` (image, max **256 KB**), optional `name` (max 32). Needs Manage Server. Limit **50**/server |
| `PATCH` | `/api/v1/servers/:id/emojis/:emojiId` | Rename |
| `DELETE` | `/api/v1/servers/:id/emojis/:emojiId` | Delete |
| `GET` | `/api/v1/servers/my-emojis` | All emojis from joined servers (+ `server_name`) |
| `GET` | `/api/v1/emojis/:id/file` | Redirect (302) to image file |

Emoji object fields typically include: `id`, `server_id`, `name`, `file_path`, `original_name`, `mime_type`, `size`, `uploaded_by`, `created_at`, `uploaded_by_username`.

---

## Soundboard

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/servers/:id/soundboard` | List server sounds (includes `icon` emoji) |
| `POST` | `/api/v1/servers/:id/soundboard` | Upload (multipart). Manage-server. Audit `SERVER_SOUNDBOARD_UPLOAD` |
| `PATCH` | `/api/v1/servers/:id/soundboard/:soundId` | Update name / icon / optionally replace audio. Audit `SERVER_SOUNDBOARD_UPDATE` |
| `DELETE` | `/api/v1/servers/:id/soundboard/:soundId` | Delete. Audit `SERVER_SOUNDBOARD_DELETE` |
| `GET` | `/api/v1/nitro/soundboard/catalog` | Playable catalog (GGBRO Plus/soundboard feature); each sound has `icon` |
| `POST` | `/api/v1/nitro/soundboard/play` | Play in caller's active voice channel |

### Upload body (`POST /api/v1/servers/:id/soundboard`)

Multipart form fields:

| Field | Required | Description |
|-------|----------|-------------|
| `sound` | yes | Audio file (≤8MB) |
| `name` | no | Display label (max 40; defaults to filename stem) |
| `icon` | no | Unicode emoji shown in the voice soundboard picker (default `🎶`; custom `<:name:id>` markup rejected) |

### Update body (`PATCH /api/v1/servers/:id/soundboard/:soundId`)

JSON `{ "name"?: string, "icon"?: string }` **or** multipart with optional `sound` (replace audio) plus `name` / `icon` fields.

Response sound object includes: `id`, `server_id`, `name`, `icon`, `file_path`, `original_name`, `mime_type`, `size`, `uploaded_by`, `created_at`, `uploaded_by_username`.

---

## Reactions & Pins

### Channel messages

| Method | Path | Description |
|--------|------|-------------|
| `PUT` | `/api/v1/messages/:channelId/reactions/:messageId` | Toggle reaction. Body `{"emoji": "👍"}` or `{"emoji": "<:name:id>"}`. New emoji type needs `ADD_REACTIONS`; adding to existing type always allowed |
| `DELETE` | `/api/v1/messages/:channelId/reactions/:messageId/:emoji` | Remove your reaction |
| `GET` | `/api/v1/messages/:channelId/reactions/:messageId` | All reactions + users |
| `GET` | `/api/v1/messages/:channelId/pins` | Pinned messages |
| `POST` | `/api/v1/messages/:channelId/pins/:messageId` | Pin |
| `DELETE` | `/api/v1/messages/:channelId/pins/:messageId` | Unpin |

Socket equivalents: `message:reaction`, `message:pin` (and client emits for toggle/pin).

### DM pins

| Method | Path |
|--------|------|
| `GET` | `/api/v1/dm/:targetUserId/pins` |
| `POST` | `/api/v1/dm/:targetUserId/pins/:messageId` |
| `DELETE` | `/api/v1/dm/:targetUserId/pins/:messageId` |

---

## Invites & Bot Invites

### Server invites

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/v1/servers/:id/invites` | `{max_uses?, expires_in?}` |
| `GET` | `/api/v1/servers/:id/invites` | List |
| `DELETE` | `/api/v1/servers/:id/invites/:inviteId` | Delete |
| `GET` | `/api/v1/servers/:id/invites/:inviteId/uses` | Use history |
| `POST` | `/api/v1/servers/:id/invite/regenerate` | Regenerate primary code |
| `POST` | `/api/v1/servers/join/:inviteCode` | Join via code |

### Bot invites

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/oauth2/invite/:botUserId` | Public card: `public_invite`, `default_permissions`, `invite_permission_locked`, `permission_labels`, `permission_flags` |
| `POST` | `/api/v1/oauth2/invite/:botUserId/add/:serverId` | Body `{permissions}`; respects `public_invite`; creates/updates managed bot role |
| `POST` | `/api/v1/oauth2/applications/:appId/servers/:serverId` | Owner adds own bot; optional `{permissions}` |
| `PATCH` | `/api/v1/oauth2/applications/:appId/permissions` | `{default_permissions, invite_permission_locked?}` — invite defaults only |
| `PATCH` | `/api/v1/oauth2/applications/:appId/public-invite` | `{public_invite: true}` |
| `PATCH` | `/api/v1/oauth2/applications/:appId/servers/:serverId/permissions` | Always **403** |

Bot-token invite CRUD: `/api/v1/oauth2/bot/servers/:serverId/invites` (`POST`/`GET`/`DELETE`).

---

## Copying IDs

GGBRO IDs are **UUIDs**. No Developer Mode toggle — copy actions are always available:

- **Channel ID** — right-click channel → *Copy Channel ID* (also *Copy Channel Link*)
- **Server ID** — right-click server icon → *Copy Server ID*
- **User ID** — profile popup copy button, or member list → *Copy User ID*

Use these in REST paths, slash options, and Socket.IO room joins.

---

## Errors & Rate Limits

```json
{ "error": "Human readable message" }
```

| Status | Meaning |
|--------|---------|
| `200` / `201` | Success / created |
| `400` | Invalid body or state |
| `401` | Missing/invalid token |
| `403` | Authenticated but forbidden |
| `404` | Missing resource |
| `409` | Conflict (e.g. duplicate command name) |

There is **no global bot API rate-limit middleware** today. Auth flows (e.g. password reset) use internal rate buckets. Avoid tight polling; prefer Socket.IO events.

Hard limits:

- Upload size: 25 MB / 500 MB (verified/GGBRO Plus)
- Commands: max **50** global via bot bulk register
- Options: max **25** per command
- Bulk delete: max **100** message IDs
- Emoji file: max **256 KB**, **50** per server

---

## Webhooks

Channel webhooks post messages into a text channel without a bot session. Multiple webhooks per channel are supported. Managing webhooks requires **Manage Channels**. The execute URL is an unauthenticated secret — **do not** send an `Authorization` header on execute (the URL token is the credential).

### Manage (user Bearer JWT)

| Method | Path | Notes |
|--------|------|--------|
| `GET` | `/api/v1/channels/:channelId/webhooks` | List — full `token` + `url` for managers. Alias: `GET /api/webhooks/channel/:channelId` |
| `POST` | `/api/v1/channels/:channelId/webhooks` | Create `{ name, avatar? }` — returns full `token` + `url`. Alias: `POST /api/webhooks/channel/:channelId` |
| `PATCH` | `/api/webhooks/:id` | Rename / avatar |
| `POST` | `/api/webhooks/:id/token` | Regenerate token (returns new `token` + `url`) |
| `DELETE` | `/api/webhooks/:id` | Delete |

### Execute (no Bearer auth)

`GET /api/webhooks/:id/:token` — metadata (name, channel, url)

`POST /api/webhooks/:id/:token`

```json
{
  "content": "Hello!",
  "embeds": [
    {
      "title": "Status",
      "description": "Deploy finished",
      "color": 5793266,
      "fields": [{ "name": "Env", "value": "prod", "inline": true }]
    }
  ],
  "username": "optional override",
  "avatar_url": "optional override"
}
```

Requires `content` or `embeds`. Discord-style embed shapes (`image.url`, integer `color`) are normalized to GGBRO’s embed format. Query `?wait=true` returns the created message; otherwise `204 No Content`. Wrong id/token → `401 {"error":"Invalid webhook"}`.

```bash
curl -X POST "https://ggbro.app/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN?wait=true" \
  -H "Content-Type: application/json" \
  -d '{"content":"Hello from a webhook!"}'
```

Also: Developer Portal → **Webhooks**, **Message Builder** (`/portal/webhook-builder`), and **Edit Channel → Integrations** in the app.

Payment provider webhooks exist only for billing internals and are **not** this surface.

---

## Stats

### `GET /api/v1/stats` (public, no auth)

```json
{
  "users": 0,
  "bots": 0,
  "servers": 0,
  "messages": 0
}
```

---

## Condensed Examples

Full copy-paste bots live in:

- `/developers` HTML sections: Ping Pong (Python/JS), `/serverinfo`, Welcome Bot
- `/developers/ggbro.py`
- `/developers/music_bot.py`
- `/developers/welcome_bot.py`

Below are condensed patterns (same APIs).

### Ping / echo / hello (Python sketch)

```python
import socketio, requests

BOT_TOKEN = "YOUR_BOT_TOKEN"
API_URL = "https://ggbro.app"
sio = socketio.Client(reconnection=True)

def reply(ctx, content, embeds=None):
    payload = {"interaction_id": ctx["id"], "channel_id": ctx["channel_id"], "content": content}
    if embeds:
        payload["embeds"] = embeds
    sio.emit("interaction:response", payload)

@sio.on("interaction:create")
def on_interaction(data):
    if data.get("type") and data["type"] != "slash_command":
        return
    cmd = (data.get("command") or "").lower()
    opts = data.get("options") or {}
    if cmd == "ping":
        reply(data, "Pong! 🏓")
    elif cmd == "echo":
        message = opts.get("message") or data.get("args", "")
        reply(data, f"📢 {message}" if message else "❌ Please provide a message!")
    elif cmd == "hello":
        reply(data, "", embeds=[{
            "title": f"Hello, {data['user']['username']}!",
            "description": "Welcome! I'm a bot running on ggbro.",
            "color": "#23a55a",
        }])

requests.post(
    f"{API_URL}/api/v1/oauth2/commands",
    json=[
        {"name": "ping", "description": "Check if the bot is alive", "options": []},
        {"name": "echo", "description": "Repeat your message", "options": [
            {"name": "message", "description": "Text to echo", "required": True}
        ]},
        {"name": "hello", "description": "Say hello with an embed", "options": []},
    ],
    headers={"Authorization": f"Bearer {BOT_TOKEN}"},
)
sio.connect(API_URL, auth={"token": BOT_TOKEN})
sio.wait()
```

### `/serverinfo` with embeds (sketch)

```python
# On /serverinfo:
# GET /api/v1/servers/{server_id}
# GET /api/v1/servers/{server_id}/members
# reply with embed: title=name, fields owner/members/id, thumbnail icon URL under https://ggbro.app/uploads/...
```

### Welcome bot (sketch)

```python
@sio.event
def connect():
    for server_id in WELCOME_CHANNELS:
        sio.emit("server:join", server_id)

@sio.on("member:joined")
def on_member_join(data):
    channel_id = WELCOME_CHANNELS.get(data.get("serverId"))
    if not channel_id:
        return
    sio.emit("bot:send_message", {
        "channel_id": channel_id,
        "content": "",
        "embeds": [{
            "title": f"Welcome, {data.get('username', 'Someone')}! 👋",
            "description": "We're glad to have you here.",
            "color": "#23a55a",
        }],
    })
```

---

## Object shape cheat sheet

### User (public / me)

```json
{
  "id": "uuid",
  "username": "string",
  "email": "string",
  "avatar": "/uploads/avatars/file.png",
  "banner": "/uploads/banners/file.png",
  "banner_offset": 0,
  "bio": "string",
  "status": "online",
  "created_at": 1710000000,
  "is_bot": 0
}
```

### Channel

```json
{
  "id": "uuid",
  "server_id": "uuid",
  "name": "general",
  "type": "text",
  "category_id": null,
  "position": 0,
  "topic": null,
  "slowmode": 0
}
```

### Role

```json
{
  "id": "uuid",
  "server_id": "uuid",
  "name": "Moderator",
  "color": "#5865F2",
  "position": 1,
  "permissions": 76,
  "hoist": 0,
  "is_default": 0
}
```

---

## Route map (quick index)

| Area | Prefix |
|------|--------|
| Auth | `/api/v1/auth` |
| Servers | `/api/v1/servers` |
| Channels | `/api/v1/channels` |
| Messages | `/api/v1/messages` |
| Users | `/api/v1/users` |
| Friends | `/api/v1/friends` |
| DMs | `/api/v1/dm` |
| Group DMs | `/api/v1/dm/groups` |
| Roles | `/api/v1/roles` |
| OAuth2 / apps / bots | `/api/v1/oauth2` |
| Bot REST | `/api/v1/oauth2/bot` |
| GGBRO Plus / soundboard catalog | `/api/v1/nitro` |
| Stats | `/api/v1/stats` |
| Emoji file | `/api/v1/emojis/:id/file` |

---

*End of AI reference. Prefer `/developers` for humans; prefer this file for automated agents.*
