> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/benz206/SpotifyMacroBoard/llms.txt
> Use this file to discover all available pages before exploring further.

# API endpoints

> Complete reference for all API endpoints used by the Spotify MacroBoard

## Base URL

All endpoints are accessed via HTTPS on your personal server:

```http theme={null}
https://benzhou.tech/api/
```

<Note>
  Replace `benzhou.tech` with your own domain when setting up your personal server.
</Note>

## Authentication

All endpoints use password-based authentication via URL parameter:

```http theme={null}
/api/{endpoint}/{password}/{...}
```

The password is defined in `SampleCredentials.h` and validated by the server before processing requests.

## Endpoints

### Manage playback state

Controls Spotify playback by executing various actions.

```http theme={null}
GET /api/manageState/{password}/{action}
```

<ParamField path="password" type="string" required>
  Authentication password matching server configuration
</ParamField>

<ParamField path="action" type="string" required>
  Action to perform. Must be one of: `playPause`, `skip`, `back`, `loop`, `vdec`, `vinc`, `shuffle`
</ParamField>

#### Actions

<ParamField path="playPause" type="action">
  Toggle play/pause state. Mapped from pause/play button (GPIO 25).

  **Source:** src/src.ino:366-369

  ```cpp theme={null}
  void pausePlay() {
      updateState('p');
      nextCurrentCheck = millis() + 100;
  }
  ```
</ParamField>

<ParamField path="skip" type="action">
  Skip to next track. Mapped from skip button (GPIO 26).

  **Source:** src/src.ino:371-375

  ```cpp theme={null}
  void skip() {
      updateState('s');
      nextCurrentCheck = millis() + 100;
      updateCurrent();
  }
  ```
</ParamField>

<ParamField path="back" type="action">
  Go to previous track or restart current track. Mapped from back button (GPIO 14).

  **Source:** src/src.ino:361-364

  ```cpp theme={null}
  void back() {
      updateState('b');
      nextCurrentCheck = millis() + 100;
  }
  ```
</ParamField>

<ParamField path="loop" type="action">
  Toggle repeat/loop mode. Mapped from loop button (GPIO 13).

  **Source:** src/src.ino:359

  ```cpp theme={null}
  void repeat() { updateState('r'); }
  ```
</ParamField>

<ParamField path="vdec" type="action">
  Decrease volume. Mapped from volume decrease button (GPIO 5).

  **Source:** src/src.ino:349-352

  ```cpp theme={null}
  void volumeDecrease() {
      updateState('v', 0);
      updateScreen();
  }
  ```
</ParamField>

<ParamField path="vinc" type="action">
  Increase volume. Mapped from volume increase button (GPIO 12).

  **Source:** src/src.ino:354-357

  ```cpp theme={null}
  void volumeIncrease() {
      updateState('v', 1);
      updateScreen();
  }
  ```
</ParamField>

<ParamField path="shuffle" type="action">
  Toggle shuffle mode. Mapped from shuffle button (GPIO 4).

  **Source:** src/src.ino:347

  ```cpp theme={null}
  void shuffle() { updateState('f'); }
  ```
</ParamField>

#### Request example

```cpp theme={null}
// From src/src.ino:202-205
wifiClient.print("GET /api/manageState/" + PASSWORD + "/" + actionString +
                 " HTTP/1.1\r\n" + "Host: benzhou.tech\r\n" +
                 "Connection: Keep-Alive\r\n\r\n");
wifiClient.flush();
```

**HTTP request:**

```http theme={null}
GET /api/manageState/your-password/playPause HTTP/1.1
Host: benzhou.tech
Connection: Keep-Alive
```

#### Response

The server should return an HTTP 200 status on success. The ESP32 does not parse the response body for this endpoint.

<CodeGroup>
  ```json Success theme={null}
  {
    "success": true
  }
  ```

  ```json Error theme={null}
  {
    "error": "Invalid password"
  }
  ```
</CodeGroup>

***

### Get current playback

Retrieves the current playback state including track information, progress, and album art color.

```http theme={null}
GET /api/getCurrent/{password}
```

<ParamField path="password" type="string" required>
  Authentication password matching server configuration
</ParamField>

#### Request example

```cpp theme={null}
// From src/src.ino:218-220
wifiClient.print("GET /api/getCurrent/" + PASSWORD + " HTTP/1.1\r\n" +
                 "Host: benzhou.tech\r\n" +
                 "Connection: Keep-Alive\r\n\r\n");
```

**HTTP request:**

```http theme={null}
GET /api/getCurrent/your-password HTTP/1.1
Host: benzhou.tech
Connection: Keep-Alive
```

#### Response

Returns JSON with current playback information:

<ResponseField name="title" type="string" required>
  Current track title. Displayed on OLED screen (line 2-3).

  **Example:** `"Bohemian Rhapsody"`

  **Extracted at:** src/src.ino:248

  ```cpp theme={null}
  extractValue("title", response, title);
  ```
</ResponseField>

<ResponseField name="artist" type="string" required>
  Artist name. Displayed on OLED screen (line 4).

  **Example:** `"Queen"`

  **Extracted at:** src/src.ino:249

  ```cpp theme={null}
  extractValue("artist", response, artist);
  ```
</ResponseField>

<ResponseField name="album" type="string" required>
  Album name. Stored but not displayed on screen due to space constraints.

  **Example:** `"A Night at the Opera"`

  **Extracted at:** src/src.ino:250

  ```cpp theme={null}
  extractValue("album", response, album);
  ```
</ResponseField>

<ResponseField name="duration" type="integer" required>
  Track duration in seconds.

  **Example:** `354` (5:54)

  **Extracted at:** src/src.ino:251, 259

  ```cpp theme={null}
  extractValue("duration", response, durationRaw);
  duration = durationRaw.toInt();
  ```
</ResponseField>

<ResponseField name="progress" type="integer" required>
  Current playback position in seconds. Used to render progress bar.

  **Example:** `120` (2:00)

  **Extracted at:** src/src.ino:252, 260

  ```cpp theme={null}
  extractValue("progress", response, progressRaw);
  progress = progressRaw.toInt();
  ```
</ResponseField>

<ResponseField name="paused" type="boolean" required>
  Whether playback is currently paused. When true, progress counter stops incrementing.

  **Example:** `false`

  **Extracted at:** src/src.ino:253, 256

  ```cpp theme={null}
  extractValue("paused", response, pausedRaw);
  paused = pausedRaw == "true";
  ```
</ResponseField>

<ResponseField name="volume" type="integer" required>
  Current volume level (0-100). Displayed as icon in top-right corner.

  **Example:** `75`

  **Icon mapping (src/src.ino:140-148):**

  * 67-100: volume\_3 (full)
  * 34-66: volume\_2 (medium)
  * 1-33: volume\_1 (low)
  * 0: volume\_0 (muted)

  **Extracted at:** src/src.ino:254, 258

  ```cpp theme={null}
  extractValue("volume", response, volumeRaw);
  volume = volumeRaw.toInt();
  ```
</ResponseField>

<ResponseField name="color" type="array" required>
  RGB color array `[R, G, B]` extracted from album artwork. Each value is 0-255.

  **Example:** `[45, 52, 71]`

  Used to set RGB LED strip color for ambient lighting that matches the album art.

  **Extracted at:** src/src.ino:263-276

  ```cpp theme={null}
  color = response.substring(response.indexOf("[") + 1,
                             response.indexOf("]"));

  int comma1 = color.indexOf(",");
  int comma2 = color.indexOf(",", comma1 + 1);

  red = color.substring(0, comma1 + 1).toInt();
  green = color.substring(comma1 + 1, comma2 + 1).toInt();
  blue = color.substring(comma2 + 1, color.length()).toInt();
  ```

  The color is faded smoothly over 256 steps when the track changes (src/src.ino:292-304).
</ResponseField>

#### Response example

<CodeGroup>
  ```json Playing theme={null}
  {
    "title": "Bohemian Rhapsody",
    "artist": "Queen",
    "album": "A Night at the Opera",
    "duration": 354,
    "progress": 120,
    "paused": false,
    "volume": 75,
    "color": [45, 52, 71]
  }
  ```

  ```json Paused theme={null}
  {
    "title": "Come Together",
    "artist": "The Beatles",
    "album": "Abbey Road",
    "duration": 259,
    "progress": 45,
    "paused": true,
    "volume": 60,
    "color": [193, 186, 169]
  }
  ```
</CodeGroup>

#### Response parsing

The ESP32 uses a custom JSON parser to extract values:

```cpp theme={null}
// From src/src.ino:158-171
void extractValue(const String& key, const String& json, String& result) {
    String quoteKey = "\"" + key + "\":";
    int start = json.indexOf(quoteKey);
    if (start != -1) {
        int end = json.indexOf(",", start + quoteKey.length());
        if (end == -1) {
            end = json.indexOf("}", start + quoteKey.length());
        }
        result = json.substring(start + quoteKey.length(), end);
        result.trim();
        result.remove(0, 1);
        result.remove(result.length() - 1);
    }
}
```

This lightweight parser avoids the memory overhead of a full JSON library like ArduinoJson.

<Warning>
  The response JSON must be properly formatted with keys in this exact order: title, artist, album, duration, progress, paused, volume, color. The parser is not order-independent.
</Warning>

## Polling intervals

The ESP32 polls endpoints at different intervals:

| Endpoint | Interval | Constant | Source |
| - | - | - | - |
| `/api/getCurrent` | 5 seconds | `currentDelay = 5000` | src/src.ino:25, 395-398 |
| RSSI check | 10 seconds | `RSSIDelay = 10000` | src/src.ino:24, 389-393 |
| Progress update | 1 second | `timeDelay = 1000` | src/src.ino:26, 400-406 |
| LED fade step | 4 ms | `fadeDelay = 4` | src/src.ino:27, 408-411 |

```cpp theme={null}
// From src/src.ino:395-398
if (millis() > nextCurrentCheck) {
    nextCurrentCheck = millis() + currentDelay;
    updateCurrent();
}
```

## Connection handling

The ESP32 maintains a persistent HTTPS connection using HTTP Keep-Alive:

```cpp theme={null}
// From src/src.ino:194-201
if (!wifiClient.connected()) {
    if (!wifiClient.connect("benzhou.tech", 443)) {
        for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::Red;
        FastLED.show();
        return;
    }
    yield();
}
```

**Connection parameters:**

* **Protocol:** HTTPS (TLS/SSL)
* **Port:** 443
* **Host:** benzhou.tech (configurable)
* **Keep-Alive:** Enabled for connection reuse
* **Timeout:** 5000ms for response (src/src.ino:222-229)

## Error responses

The ESP32 provides visual feedback for errors using the RGB LED strip:

| Error condition | LED color | Source |
| - | - | - |
| Connection failed | Red | src/src.ino:196-198 |
| Response timeout (>5s) | Yellow | src/src.ino:225-228 |
| Invalid response format | Orange | src/src.ino:234-237 |
| WiFi connecting | Pulsing white | src/src.ino:79-87 |
| WiFi connected | Fade to green | src/src.ino:89-95 |

### Timeout handling

```cpp theme={null}
// From src/src.ino:222-229
unsigned long timeout = millis();
while (!wifiClient.available()) {
    if (millis() - timeout > 5000) {
        for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::Yellow;
        FastLED.show();
        return;
    }
}
```

## Button mapping

Buttons are mapped to actions in the following order:

```cpp theme={null}
// From src/src.ino:19-20
const int keys[7] = {SHUFFLE, VOLUME_DEC, VOLUME_INC, LOOP,
                     BACK,    PAUSE_PLAY, SKIP};

// From src/src.ino:377-378
void (*funcs[7])() = {
    shuffle, volumeDecrease, volumeIncrease, repeat, back, pausePlay, skip};
```

| GPIO | Function | Action | API call |
| - | - | - | - |
| 4 | Shuffle | Toggle shuffle | `/api/manageState/{password}/shuffle` |
| 5 | Volume - | Decrease volume | `/api/manageState/{password}/vdec` |
| 12 | Volume + | Increase volume | `/api/manageState/{password}/vinc` |
| 13 | Loop | Toggle repeat | `/api/manageState/{password}/loop` |
| 14 | Back | Previous track | `/api/manageState/{password}/back` |
| 25 | Play/Pause | Toggle playback | `/api/manageState/{password}/playPause` |
| 26 | Skip | Next track | `/api/manageState/{password}/skip` |

Buttons are read with `INPUT_PULLUP`, meaning they read LOW when pressed:

```cpp theme={null}
// From src/src.ino:381-387
for (int i = 0; i < 7; i++) {
    keyState[i] = digitalRead(keys[i]);
    if (keyState[i] == LOW && keyPrevState[i] == HIGH) {
        funcs[i]();
    }
    keyPrevState[i] = keyState[i];
}
```

## Implementation notes

### Why GET instead of POST?

The ESP32 uses GET requests for simplicity. The WiFiClientSecure library makes GET requests straightforward, and since the password is sent over HTTPS, it's encrypted in transit.

### Why custom JSON parsing?

The ESP32 has limited RAM (320KB). Using a full JSON library like ArduinoJson would consume significant memory. The custom `extractValue()` function is lightweight and sufficient for the simple JSON structure.

### Why Keep-Alive?

TLS handshakes are computationally expensive on the ESP32. By maintaining a persistent connection, the device avoids repeated handshakes, significantly improving response time and reducing power consumption.

## Next steps

<CardGroup cols={2}>
  <Card title="Server setup" icon="server" href="/api/server-setup">
    Set up your personal proxy server
  </Card>

  <Card title="Hardware assembly" icon="microchip" href="/hardware/assembly">
    Assemble the MacroBoard hardware
  </Card>
</CardGroup>
