{
  "openapi": "3.1.0",
  "info": {
    "title": "DonoLink Developer API",
    "version": "1.4.0",
    "description": "Personal owner-bound API for activity, DonoLink bot configuration, alerts and donation totals. Explicit scopes: activity:read, bot:read, bot:write, alerts:write, donations:read. Existing keys retain activity:read only. 120 requests per account and 300 per IP per 60 seconds; unavailable shared limiter returns 503. Store keys server-side or in personal native secure storage. No payment writes, multi-customer OAuth, browser CORS or guaranteed activity delivery/retention. Imported donations retain their source platform."
  },
  "servers": [
    {
      "url": "https://donolink.nl/api/v1"
    }
  ],
  "security": [
    {
      "ApiKey": []
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Read the key owner and expiry",
        "responses": {
          "200": {
            "description": "Success. Private, no-store.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "username",
                        "display_name",
                        "scopes",
                        "key"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "username": {
                          "type": "string"
                        },
                        "display_name": {
                          "type": "string"
                        },
                        "scopes": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "activity:read",
                              "bot:read",
                              "bot:write",
                              "alerts:write",
                              "donations:read"
                            ]
                          }
                        },
                        "key": {
                          "type": "object",
                          "properties": {
                            "prefix": {
                              "type": "string"
                            },
                            "expires_at": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "owner": {
                    "value": {
                      "data": {
                        "id": "abcdefghijklmno",
                        "username": "streamer",
                        "display_name": "Streamer",
                        "scopes": [
                          "activity:read",
                          "bot:read",
                          "bot:write"
                        ],
                        "key": {
                          "prefix": "dl_live_123456789",
                          "expires_at": "2026-12-06 12:00:00.000Z"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/activity": {
      "get": {
        "operationId": "listActivity",
        "summary": "Read stored personal activity",
        "description": "Current paid donations including imports and stored stream events. Pagination uses recorded_at, not occurred_at. Empty pages retain the previous cursor. Deduplicate by event ID. Updates and deletes are not events.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "Insertion time, then internal ID and source. Use asc for polling.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "One event type.",
            "schema": {
              "type": "string",
              "enum": [
                "donation",
                "follow",
                "subscription",
                "giftsub",
                "cheer",
                "raid"
              ]
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "One platform.",
            "schema": {
              "type": "string",
              "enum": [
                "donolink",
                "twitch",
                "kick",
                "youtube",
                "tiktok",
                "tipeeestream",
                "streamlabs",
                "streamelements",
                "kofi",
                "csv"
              ]
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Inclusive recorded_at boundary. UTC ISO 8601 with Z.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "Exclusive recorded_at boundary. UTC ISO 8601 with Z.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Signed opaque cursor, bound to account, order and filters. Keep these fixed.",
            "schema": {
              "type": "string",
              "maxLength": 2048
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success. Private, no-store.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "pagination"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Activity"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "required": [
                        "has_more",
                        "next_cursor"
                      ],
                      "properties": {
                        "has_more": {
                          "type": "boolean"
                        },
                        "next_cursor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-required-scope": "activity:read"
      }
    },
    "/bot": {
      "get": {
        "operationId": "getBotConfiguration",
        "summary": "Read your DonoLink bot configuration",
        "description": "Requires explicit bot:read. No query parameters. Existing activity-only keys do not grant access.",
        "x-required-scope": "bot:read",
        "responses": {
          "200": {
            "description": "Owner configuration and current revision. Private, no-store.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/BotSnapshot"
                    }
                  }
                },
                "examples": {
                  "snapshot": {
                    "value": {
                      "data": {
                        "revision": 7,
                        "config": {
                          "version": 1,
                          "enabled": true,
                          "commands": [
                            {
                              "id": "cmd_socials",
                              "name": "!socials",
                              "message": "Follow the stream at {channel}!",
                              "enabled": true,
                              "role": "everyone",
                              "cooldownSeconds": 30,
                              "alias": ""
                            }
                          ],
                          "timers": [
                            {
                              "id": "timer_rules",
                              "name": "Rules",
                              "message": "Be kind in chat.",
                              "enabled": true,
                              "intervalSeconds": 900,
                              "minChatLines": 5,
                              "alias": ""
                            }
                          ],
                          "filters": {
                            "words": {
                              "enabled": true,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 1,
                              "minLength": 0
                            },
                            "caps": {
                              "enabled": false,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 50,
                              "minLength": 10
                            },
                            "emotes": {
                              "enabled": false,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 20,
                              "minLength": 10
                            },
                            "links": {
                              "enabled": true,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 1,
                              "minLength": 10
                            },
                            "symbols": {
                              "enabled": false,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 30,
                              "minLength": 10
                            },
                            "repetitions": {
                              "enabled": false,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 10,
                              "minLength": 10
                            }
                          },
                          "blockedWords": [
                            "exampleword"
                          ],
                          "allowedHosts": [
                            "youtube.com"
                          ],
                          "regulars": [
                            {
                              "platform": "twitch",
                              "userId": "123456789",
                              "userName": "ExampleViewer"
                            }
                          ],
                          "builtins": {
                            "!commands": {
                              "enabled": true,
                              "role": "everyone",
                              "cooldownSeconds": 5
                            }
                          },
                          "replyLanguage": "nl"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-error-codes": {
          "400": [
            "invalid_parameter"
          ],
          "401": [
            "invalid_api_key"
          ],
          "403": [
            "insufficient_scope",
            "account_unavailable"
          ],
          "429": [
            "rate_limit_exceeded"
          ],
          "503": [
            "temporarily_unavailable"
          ]
        }
      },
      "put": {
        "operationId": "replaceBotConfiguration",
        "summary": "Replace your DonoLink bot configuration",
        "description": "Requires explicit bot:write. Send the complete config and last-read revision. A stale revision returns 409 revision_conflict. Invalid config returns 422 invalid_config. Body must arrive within 10 seconds and contain at most 2000000 bytes of valid UTF-8 JSON. No query parameters or owner fields. Read again after an uncertain result; never blindly overwrite a newer revision. Bot write permission does not imply bot read permission.",
        "x-required-scope": "bot:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BotUpdate"
              },
              "examples": {
                "replace": {
                  "summary": "Replace the full configuration at the latest read revision",
                  "value": {
                    "revision": 7,
                    "config": {
                      "version": 1,
                      "enabled": true,
                      "commands": [
                        {
                          "id": "cmd_socials",
                          "name": "!socials",
                          "message": "Follow the stream at {channel}!",
                          "enabled": true,
                          "role": "everyone",
                          "cooldownSeconds": 30,
                          "alias": ""
                        }
                      ],
                      "timers": [
                        {
                          "id": "timer_rules",
                          "name": "Rules",
                          "message": "Be kind in chat.",
                          "enabled": true,
                          "intervalSeconds": 900,
                          "minChatLines": 5,
                          "alias": ""
                        }
                      ],
                      "filters": {
                        "words": {
                          "enabled": true,
                          "exemptRole": "moderator",
                          "timeoutSeconds": 600,
                          "silent": false,
                          "message": "",
                          "threshold": 1,
                          "minLength": 0
                        },
                        "caps": {
                          "enabled": false,
                          "exemptRole": "moderator",
                          "timeoutSeconds": 600,
                          "silent": false,
                          "message": "",
                          "threshold": 50,
                          "minLength": 10
                        },
                        "emotes": {
                          "enabled": false,
                          "exemptRole": "moderator",
                          "timeoutSeconds": 600,
                          "silent": false,
                          "message": "",
                          "threshold": 20,
                          "minLength": 10
                        },
                        "links": {
                          "enabled": true,
                          "exemptRole": "moderator",
                          "timeoutSeconds": 600,
                          "silent": false,
                          "message": "",
                          "threshold": 1,
                          "minLength": 10
                        },
                        "symbols": {
                          "enabled": false,
                          "exemptRole": "moderator",
                          "timeoutSeconds": 600,
                          "silent": false,
                          "message": "",
                          "threshold": 30,
                          "minLength": 10
                        },
                        "repetitions": {
                          "enabled": false,
                          "exemptRole": "moderator",
                          "timeoutSeconds": 600,
                          "silent": false,
                          "message": "",
                          "threshold": 10,
                          "minLength": 10
                        }
                      },
                      "blockedWords": [
                        "exampleword"
                      ],
                      "allowedHosts": [
                        "youtube.com"
                      ],
                      "regulars": [
                        {
                          "platform": "twitch",
                          "userId": "123456789",
                          "userName": "ExampleViewer"
                        }
                      ],
                      "builtins": {
                        "!commands": {
                          "enabled": true,
                          "role": "everyone",
                          "cooldownSeconds": 5
                        }
                      },
                      "replyLanguage": "nl"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Owner configuration and current revision. Private, no-store.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/BotSnapshot"
                    }
                  }
                },
                "examples": {
                  "snapshot": {
                    "value": {
                      "data": {
                        "revision": 7,
                        "config": {
                          "version": 1,
                          "enabled": true,
                          "commands": [
                            {
                              "id": "cmd_socials",
                              "name": "!socials",
                              "message": "Follow the stream at {channel}!",
                              "enabled": true,
                              "role": "everyone",
                              "cooldownSeconds": 30,
                              "alias": ""
                            }
                          ],
                          "timers": [
                            {
                              "id": "timer_rules",
                              "name": "Rules",
                              "message": "Be kind in chat.",
                              "enabled": true,
                              "intervalSeconds": 900,
                              "minChatLines": 5,
                              "alias": ""
                            }
                          ],
                          "filters": {
                            "words": {
                              "enabled": true,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 1,
                              "minLength": 0
                            },
                            "caps": {
                              "enabled": false,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 50,
                              "minLength": 10
                            },
                            "emotes": {
                              "enabled": false,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 20,
                              "minLength": 10
                            },
                            "links": {
                              "enabled": true,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 1,
                              "minLength": 10
                            },
                            "symbols": {
                              "enabled": false,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 30,
                              "minLength": 10
                            },
                            "repetitions": {
                              "enabled": false,
                              "exemptRole": "moderator",
                              "timeoutSeconds": 600,
                              "silent": false,
                              "message": "",
                              "threshold": 10,
                              "minLength": 10
                            }
                          },
                          "blockedWords": [
                            "exampleword"
                          ],
                          "allowedHosts": [
                            "youtube.com"
                          ],
                          "regulars": [
                            {
                              "platform": "twitch",
                              "userId": "123456789",
                              "userName": "ExampleViewer"
                            }
                          ],
                          "builtins": {
                            "!commands": {
                              "enabled": true,
                              "role": "everyone",
                              "cooldownSeconds": 5
                            }
                          },
                          "replyLanguage": "nl"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "408": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "$ref": "#/components/responses/Error"
          },
          "415": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-error-codes": {
          "400": [
            "invalid_parameter",
            "invalid_json",
            "invalid_request"
          ],
          "401": [
            "invalid_api_key"
          ],
          "403": [
            "insufficient_scope",
            "account_unavailable"
          ],
          "408": [
            "request_timeout"
          ],
          "409": [
            "revision_conflict"
          ],
          "413": [
            "body_too_large"
          ],
          "415": [
            "json_required"
          ],
          "422": [
            "invalid_config"
          ],
          "429": [
            "rate_limit_exceeded"
          ],
          "503": [
            "temporarily_unavailable"
          ]
        }
      }
    },
    "/bot/music": {
      "get": {
        "operationId": "getBotMusic",
        "summary": "Read your music queue, playlist and controls",
        "x-required-scope": "bot:read",
        "description": "No query parameters. State is private and owner-bound. Player state indicates a lease, not verified audible playback.",
        "responses": {
          "200": {
            "description": "Current music state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/BotMusicSnapshot"
                    }
                  }
                },
                "examples": {
                  "snapshot": {
                    "value": {
                      "data": {
                        "revision": 3,
                        "state": {
                          "version": 1,
                          "settings": {
                            "enabled": true,
                            "role": "everyone",
                            "providers": [
                              "youtube"
                            ],
                            "searchProvider": "youtube",
                            "queueLimit": 20,
                            "userLimit": 2,
                            "exemptRole": "moderator",
                            "maxDurationSeconds": 600,
                            "playlistOnly": false,
                            "youtubeMusicOnly": false,
                            "fallback": true,
                            "publicLists": false
                          },
                          "queue": [
                            {
                              "id": "entry_1",
                              "track": {
                                "provider": "youtube",
                                "id": "abcdefghijk",
                                "url": "https://www.youtube.com/watch?v=abcdefghijk",
                                "title": "Example song",
                                "author": "Example artist",
                                "durationSeconds": 213,
                                "music": true
                              },
                              "requester": {
                                "platform": "dashboard",
                                "userId": "abcdefghijklmno",
                                "userName": "streamer"
                              }
                            }
                          ],
                          "playlist": [
                            {
                              "provider": "youtube",
                              "id": "abcdefghijk",
                              "url": "https://www.youtube.com/watch?v=abcdefghijk",
                              "title": "Example song",
                              "author": "Example artist",
                              "durationSeconds": 213,
                              "music": true
                            }
                          ],
                          "current": null,
                          "playbackId": 0,
                          "paused": false,
                          "volume": 80,
                          "positionSeconds": 0,
                          "playlistCursor": 0
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "408": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "$ref": "#/components/responses/Error"
          },
          "415": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-error-codes": {
          "400": [
            "invalid_parameter"
          ],
          "401": [
            "invalid_api_key"
          ],
          "403": [
            "insufficient_scope",
            "account_unavailable"
          ],
          "429": [
            "rate_limit_exceeded"
          ],
          "503": [
            "temporarily_unavailable"
          ]
        }
      },
      "post": {
        "operationId": "changeBotMusic",
        "summary": "Change your music queue, playlist or controls",
        "x-required-scope": "bot:write",
        "description": "No query parameters. Maximum 16000 UTF-8 JSON bytes within 10 seconds. Each operation needs a unique operationKey and latest revision. Heartbeat-only revisions do not invalidate an editor. Repeat the exact body after uncertain delivery: the operation applies once, and appliedRevision identifies the original write while revision/state reflect the latest state. Reusing a key with a different action returns 409 music_operation_conflict. Resolve music_revision_conflict by reading and reconciling before a new operation. Provider metadata and identity are server-controlled; request attribution uses the owner's dashboard identity. No player lease/ended operations. Playback requires an active dashboard player. YouTube metadata requires BOT_YOUTUBE_API_KEY; SoundCloud metadata requires BOT_SOUNDCLOUD_CLIENT_ID and BOT_SOUNDCLOUD_CLIENT_SECRET; absent credentials return music_provider_unconfigured. Tokens are shared and refreshed server-side.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BotMusicUpdate"
              },
              "examples": {
                "request": {
                  "summary": "Add a YouTube request to the queue",
                  "value": {
                    "operationKey": "42eed350-2ab8-46ea-8ae4-c02dfe296f9e",
                    "revision": 3,
                    "action": {
                      "kind": "request",
                      "query": "https://www.youtube.com/watch?v=abcdefghijk"
                    }
                  }
                },
                "volume": {
                  "summary": "Change the volume",
                  "value": {
                    "operationKey": "a9b8c7d6-e5f4-4321-a1b2-c3d4e5f60718",
                    "revision": 3,
                    "action": {
                      "kind": "volume",
                      "volume": 80
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Original applied revision and latest music state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "appliedRevision",
                        "revision",
                        "state"
                      ],
                      "properties": {
                        "appliedRevision": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 9007199254740991
                        },
                        "revision": {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 9007199254740991
                        },
                        "state": {
                          "$ref": "#/components/schemas/BotMusicState"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "changed": {
                    "value": {
                      "data": {
                        "appliedRevision": 4,
                        "revision": 4,
                        "state": {
                          "version": 1,
                          "settings": {
                            "enabled": true,
                            "role": "everyone",
                            "providers": [
                              "youtube"
                            ],
                            "searchProvider": "youtube",
                            "queueLimit": 20,
                            "userLimit": 2,
                            "exemptRole": "moderator",
                            "maxDurationSeconds": 600,
                            "playlistOnly": false,
                            "youtubeMusicOnly": false,
                            "fallback": true,
                            "publicLists": false
                          },
                          "queue": [
                            {
                              "id": "entry_1",
                              "track": {
                                "provider": "youtube",
                                "id": "abcdefghijk",
                                "url": "https://www.youtube.com/watch?v=abcdefghijk",
                                "title": "Example song",
                                "author": "Example artist",
                                "durationSeconds": 213,
                                "music": true
                              },
                              "requester": {
                                "platform": "dashboard",
                                "userId": "abcdefghijklmno",
                                "userName": "streamer"
                              }
                            }
                          ],
                          "playlist": [
                            {
                              "provider": "youtube",
                              "id": "abcdefghijk",
                              "url": "https://www.youtube.com/watch?v=abcdefghijk",
                              "title": "Example song",
                              "author": "Example artist",
                              "durationSeconds": 213,
                              "music": true
                            }
                          ],
                          "current": null,
                          "playbackId": 0,
                          "paused": false,
                          "volume": 80,
                          "positionSeconds": 0,
                          "playlistCursor": 0
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "408": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "$ref": "#/components/responses/Error"
          },
          "415": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        },
        "x-error-codes": {
          "400": [
            "invalid_parameter",
            "invalid_json"
          ],
          "401": [
            "invalid_api_key"
          ],
          "403": [
            "insufficient_scope",
            "account_unavailable",
            "music_forbidden"
          ],
          "408": [
            "request_timeout"
          ],
          "409": [
            "music_revision_conflict",
            "music_operation_conflict",
            "music_requests_disabled",
            "music_queue_full",
            "music_playlist_full",
            "music_playlist_duplicate",
            "music_track_disallowed",
            "music_playlist_only",
            "music_user_limit",
            "music_duplicate_entry",
            "music_track_missing",
            "music_entry_missing",
            "music_playback_changed",
            "music_busy"
          ],
          "413": [
            "body_too_large"
          ],
          "415": [
            "json_required"
          ],
          "422": [
            "music_invalid_input",
            "music_invalid_operation",
            "music_invalid_revision",
            "music_invalid_requester",
            "music_invalid_query",
            "music_provider_disabled",
            "music_track_unplayable"
          ],
          "429": [
            "rate_limit_exceeded",
            "music_provider_busy"
          ],
          "503": [
            "temporarily_unavailable",
            "music_provider_unavailable",
            "music_provider_unconfigured"
          ]
        }
      }
    },
    "/alerts/wachtrij": {
      "post": {
        "operationId": "controlAlertQueue",
        "summary": "Pauzeer, hervat of sla de alert-wachtrij over",
        "description": "Requires explicit alerts:write. Broadcasts to all open alert overlays of the key owner. Rate limit 30 per minute per owner.",
        "x-required-scope": "alerts:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AlertsWachtrijRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Opdracht verstuurd.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "ok"
                      ],
                      "properties": {
                        "ok": {
                          "const": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/alerts/replay": {
      "post": {
        "operationId": "replayDonationAlert",
        "summary": "Speel de laatste donatie opnieuw als alert",
        "description": "Requires explicit alerts:write. The replayed alert is marked test and does not count toward the goal, recent feed or stats. 404 if there is no paid donation or the given donation belongs to another account.",
        "x-required-scope": "alerts:write",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AlertsReplayRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Alert verstuurd.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "ok"
                      ],
                      "properties": {
                        "ok": {
                          "const": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/alerts/test": {
      "post": {
        "operationId": "sendTestAlert",
        "summary": "Stuur een test-alert (donatie)",
        "description": "Requires explicit alerts:write. Body must be an empty JSON object. Rate limit 30 per minute per owner.",
        "x-required-scope": "alerts:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "maxProperties": 0
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Testalert verstuurd.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "ok"
                      ],
                      "properties": {
                        "ok": {
                          "const": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/stats/totaal": {
      "get": {
        "operationId": "getDonationTotal",
        "summary": "Lees je donatie-totaal per periode",
        "description": "Requires explicit donations:read. Sums paid donations (refunds deducted) since the period start. Periode doel falls back to vandaag when the goal has no start date.",
        "x-required-scope": "donations:read",
        "parameters": [
          {
            "name": "periode",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "vandaag",
                "24uur",
                "doel",
                "altijd"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Totaal in cents en aantal donaties.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/StatsTotaalResponse"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "dl_live_<64 lowercase hex characters>",
        "description": "Personal API key with explicit scopes: activity:read, bot:read, bot:write, alerts:write, donations:read. Send as Authorization: Bearer dl_live_... . Existing keys without a stored scope list retain activity:read only. Never place keys in URLs or frontend bundles."
      }
    },
    "schemas": {
      "Activity": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "type",
          "platform",
          "occurred_at",
          "recorded_at",
          "actor",
          "message",
          "value",
          "unit",
          "currency",
          "refunded_amount_cents"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^(don|evt)_[a-z0-9]{15}$"
          },
          "type": {
            "type": "string",
            "enum": [
              "donation",
              "follow",
              "subscription",
              "giftsub",
              "cheer",
              "raid"
            ]
          },
          "platform": {
            "type": "string",
            "enum": [
              "donolink",
              "twitch",
              "kick",
              "youtube",
              "tiktok",
              "tipeeestream",
              "streamlabs",
              "streamelements",
              "kofi",
              "csv"
            ]
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "recorded_at": {
            "type": "string",
            "format": "date-time"
          },
          "actor": {
            "type": "object",
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "message": {
            "type": [
              "string",
              "null"
            ]
          },
          "value": {
            "type": "integer",
            "minimum": 0
          },
          "unit": {
            "type": "string",
            "enum": [
              "cents",
              "none",
              "months",
              "subscriptions",
              "bits",
              "diamonds",
              "viewers"
            ]
          },
          "currency": {
            "type": [
              "string",
              "null"
            ]
          },
          "refunded_amount_cents": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          }
        }
      },
      "BotRole": {
        "type": "string",
        "enum": [
          "everyone",
          "subscriber",
          "regular",
          "vip",
          "moderator",
          "admin",
          "owner"
        ]
      },
      "BotCommand": {
        "type": "object",
        "required": [
          "id",
          "name",
          "message",
          "enabled",
          "role",
          "cooldownSeconds",
          "alias"
        ],
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed.",
            "pattern": "^[a-zA-Z0-9_-]+$"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "Command name; normalized to lowercase. Letters, digits, underscore and hyphen, optionally starting with !."
          },
          "message": {
            "type": "string",
            "minLength": 0,
            "maxLength": 2000,
            "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed."
          },
          "enabled": {
            "type": "boolean"
          },
          "role": {
            "$ref": "#/components/schemas/BotRole"
          },
          "cooldownSeconds": {
            "type": "integer",
            "minimum": 5,
            "maximum": 86400
          },
          "userCooldownSeconds": {
            "type": "integer",
            "minimum": 0,
            "maximum": 86400,
            "description": "Optional per-viewer cooldown in seconds on top of the global cooldown. 0 disables it; omit the field entirely to remove it."
          },
          "alias": {
            "type": "string",
            "minLength": 0,
            "maxLength": 64,
            "description": "Empty or a command/builtin name. Target must exist; cycles and chains deeper than eight are rejected."
          }
        }
      },
      "BotTimer": {
        "type": "object",
        "required": [
          "id",
          "name",
          "message",
          "enabled",
          "intervalSeconds",
          "minChatLines",
          "alias"
        ],
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed.",
            "pattern": "^[a-zA-Z0-9_-]+$"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed."
          },
          "message": {
            "type": "string",
            "minLength": 0,
            "maxLength": 2000,
            "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed."
          },
          "enabled": {
            "type": "boolean"
          },
          "intervalSeconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 86400
          },
          "minChatLines": {
            "type": "integer",
            "minimum": 0,
            "maximum": 10000
          },
          "alias": {
            "type": "string",
            "minLength": 0,
            "maxLength": 64,
            "description": "Empty or a command/builtin name. Target must exist; cycles and chains deeper than eight are rejected."
          }
        }
      },
      "BotFilters": {
        "type": "object",
        "required": [
          "words",
          "caps",
          "emotes",
          "links",
          "symbols",
          "repetitions"
        ],
        "properties": {
          "words": {
            "type": "object",
            "required": [
              "enabled",
              "exemptRole",
              "timeoutSeconds",
              "silent",
              "message",
              "threshold",
              "minLength"
            ],
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "exemptRole": {
                "$ref": "#/components/schemas/BotRole"
              },
              "timeoutSeconds": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1209600
              },
              "silent": {
                "type": "boolean"
              },
              "message": {
                "type": "string",
                "minLength": 0,
                "maxLength": 450,
                "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed."
              },
              "threshold": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000
              },
              "minLength": {
                "type": "integer",
                "minimum": 0,
                "maximum": 2000
              }
            }
          },
          "caps": {
            "type": "object",
            "required": [
              "enabled",
              "exemptRole",
              "timeoutSeconds",
              "silent",
              "message",
              "threshold",
              "minLength"
            ],
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "exemptRole": {
                "$ref": "#/components/schemas/BotRole"
              },
              "timeoutSeconds": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1209600
              },
              "silent": {
                "type": "boolean"
              },
              "message": {
                "type": "string",
                "minLength": 0,
                "maxLength": 450,
                "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed."
              },
              "threshold": {
                "type": "integer",
                "minimum": 1,
                "maximum": 100
              },
              "minLength": {
                "type": "integer",
                "minimum": 0,
                "maximum": 2000
              }
            }
          },
          "emotes": {
            "type": "object",
            "required": [
              "enabled",
              "exemptRole",
              "timeoutSeconds",
              "silent",
              "message",
              "threshold",
              "minLength"
            ],
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "exemptRole": {
                "$ref": "#/components/schemas/BotRole"
              },
              "timeoutSeconds": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1209600
              },
              "silent": {
                "type": "boolean"
              },
              "message": {
                "type": "string",
                "minLength": 0,
                "maxLength": 450,
                "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed."
              },
              "threshold": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000
              },
              "minLength": {
                "type": "integer",
                "minimum": 0,
                "maximum": 2000
              }
            }
          },
          "links": {
            "type": "object",
            "required": [
              "enabled",
              "exemptRole",
              "timeoutSeconds",
              "silent",
              "message",
              "threshold",
              "minLength"
            ],
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "exemptRole": {
                "$ref": "#/components/schemas/BotRole"
              },
              "timeoutSeconds": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1209600
              },
              "silent": {
                "type": "boolean"
              },
              "message": {
                "type": "string",
                "minLength": 0,
                "maxLength": 450,
                "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed."
              },
              "threshold": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000
              },
              "minLength": {
                "type": "integer",
                "minimum": 0,
                "maximum": 2000
              }
            }
          },
          "symbols": {
            "type": "object",
            "required": [
              "enabled",
              "exemptRole",
              "timeoutSeconds",
              "silent",
              "message",
              "threshold",
              "minLength"
            ],
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "exemptRole": {
                "$ref": "#/components/schemas/BotRole"
              },
              "timeoutSeconds": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1209600
              },
              "silent": {
                "type": "boolean"
              },
              "message": {
                "type": "string",
                "minLength": 0,
                "maxLength": 450,
                "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed."
              },
              "threshold": {
                "type": "integer",
                "minimum": 1,
                "maximum": 100
              },
              "minLength": {
                "type": "integer",
                "minimum": 0,
                "maximum": 2000
              }
            }
          },
          "repetitions": {
            "type": "object",
            "required": [
              "enabled",
              "exemptRole",
              "timeoutSeconds",
              "silent",
              "message",
              "threshold",
              "minLength"
            ],
            "properties": {
              "enabled": {
                "type": "boolean"
              },
              "exemptRole": {
                "$ref": "#/components/schemas/BotRole"
              },
              "timeoutSeconds": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1209600
              },
              "silent": {
                "type": "boolean"
              },
              "message": {
                "type": "string",
                "minLength": 0,
                "maxLength": 450,
                "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed."
              },
              "threshold": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000
              },
              "minLength": {
                "type": "integer",
                "minimum": 0,
                "maximum": 2000
              }
            }
          }
        }
      },
      "BotRegular": {
        "type": "object",
        "required": [
          "platform",
          "userId",
          "userName"
        ],
        "properties": {
          "platform": {
            "type": "string",
            "enum": [
              "twitch",
              "youtube",
              "discord"
            ]
          },
          "userId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed.",
            "pattern": "^[a-zA-Z0-9_-]+$"
          },
          "userName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Trimmed on input. Control characters are rejected; tab, CR and LF are allowed."
          }
        }
      },
      "BotBuiltin": {
        "type": "object",
        "required": [
          "enabled",
          "role",
          "cooldownSeconds"
        ],
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "role": {
            "$ref": "#/components/schemas/BotRole"
          },
          "cooldownSeconds": {
            "type": "integer",
            "minimum": 5,
            "maximum": 86400
          },
          "userCooldownSeconds": {
            "type": "integer",
            "minimum": 0,
            "maximum": 86400,
            "description": "Optional per-viewer cooldown in seconds on top of the global cooldown. 0 disables it; omit the field entirely to remove it."
          }
        }
      },
      "BotConfig": {
        "type": "object",
        "required": [
          "version",
          "enabled",
          "commands",
          "timers",
          "filters",
          "blockedWords",
          "allowedHosts"
        ],
        "properties": {
          "version": {
            "const": 1
          },
          "enabled": {
            "type": "boolean"
          },
          "commands": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BotCommand"
            },
            "maxItems": 500
          },
          "timers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BotTimer"
            },
            "maxItems": 100
          },
          "filters": {
            "$ref": "#/components/schemas/BotFilters"
          },
          "blockedWords": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 0,
              "maxLength": 150,
              "description": "Plain entries are normalized (NFKC, lowercase) and match anywhere in the message. * is a wildcard that does not cross spaces (test* also blocks testing). Regex entries use ~/pattern/flags with flags from imsu only; patterns with nested quantifiers such as (a+)* are rejected."
            },
            "maxItems": 500
          },
          "allowedHosts": {
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 0,
              "maxLength": 253,
              "description": "Domain hostnames only, or blank lines which are removed. Lowercased; no scheme, port or path."
            },
            "maxItems": 200
          },
          "regulars": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BotRegular"
            },
            "maxItems": 10000
          },
          "builtins": {
            "type": "object",
            "required": [],
            "properties": {
              "!permit": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!winner": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!commands": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!regulars": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!filters": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!poll": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!songs": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!title": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!game": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!tags": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!marker": {
                "$ref": "#/components/schemas/BotBuiltin"
              },
              "!commercial": {
                "$ref": "#/components/schemas/BotBuiltin"
              }
            }
          },
          "replyLanguage": {
            "type": "string",
            "enum": [
              "nl",
              "en",
              "fr"
            ],
            "default": "nl"
          }
        },
        "description": "Full owner configuration. Unknown config fields are discarded. Command IDs/names and timer IDs must be unique; regulars are unique per platform/userId. Omitted regulars/builtins default to empty. Templates may contain private data; do not publish or share-cache this response. Discord transport and external variables are not yet implemented."
      },
      "BotSnapshot": {
        "type": "object",
        "required": [
          "revision",
          "config"
        ],
        "properties": {
          "revision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "config": {
            "$ref": "#/components/schemas/BotConfig"
          }
        }
      },
      "BotUpdate": {
        "type": "object",
        "required": [
          "revision",
          "config"
        ],
        "properties": {
          "revision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "config": {
            "$ref": "#/components/schemas/BotConfig"
          }
        },
        "additionalProperties": false
      },
      "BotMusicTrack": {
        "type": "object",
        "required": [
          "provider",
          "id",
          "url",
          "title",
          "author",
          "durationSeconds",
          "music"
        ],
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "youtube",
              "soundcloud"
            ]
          },
          "id": {
            "type": "string",
            "maxLength": 32
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string",
            "maxLength": 300
          },
          "author": {
            "type": "string",
            "maxLength": 200
          },
          "durationSeconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 86400
          },
          "music": {
            "type": "boolean"
          }
        }
      },
      "BotMusicEntry": {
        "type": "object",
        "required": [
          "id",
          "track",
          "requester"
        ],
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "pattern": "^[a-zA-Z0-9_-]+$"
          },
          "track": {
            "$ref": "#/components/schemas/BotMusicTrack"
          },
          "requester": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": [
                  "platform",
                  "userId",
                  "userName"
                ],
                "properties": {
                  "platform": {
                    "enum": [
                      "dashboard",
                      "twitch",
                      "youtube",
                      "discord"
                    ]
                  },
                  "userId": {
                    "type": "string"
                  },
                  "userName": {
                    "type": "string"
                  }
                }
              }
            ]
          }
        }
      },
      "BotMusicSettings": {
        "type": "object",
        "required": [
          "enabled",
          "role",
          "providers",
          "searchProvider",
          "queueLimit",
          "userLimit",
          "exemptRole",
          "maxDurationSeconds",
          "playlistOnly",
          "youtubeMusicOnly",
          "fallback"
        ],
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "role": {
            "type": "string",
            "enum": [
              "everyone",
              "subscriber",
              "regular",
              "vip",
              "moderator",
              "admin",
              "owner"
            ]
          },
          "providers": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "youtube",
                "soundcloud"
              ]
            },
            "maxItems": 2,
            "minItems": 1,
            "uniqueItems": true
          },
          "searchProvider": {
            "type": "string",
            "enum": [
              "youtube",
              "soundcloud"
            ],
            "description": "Must be included in providers."
          },
          "queueLimit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "userLimit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "exemptRole": {
            "type": "string",
            "enum": [
              "everyone",
              "subscriber",
              "regular",
              "vip",
              "moderator",
              "admin",
              "owner"
            ]
          },
          "maxDurationSeconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 86400
          },
          "playlistOnly": {
            "type": "boolean"
          },
          "youtubeMusicOnly": {
            "type": "boolean"
          },
          "fallback": {
            "type": "boolean"
          },
          "publicLists": {
            "type": "boolean",
            "default": false,
            "description": "Allow public queue and playlist pages. Opt-in; omitted values default to false."
          }
        },
        "additionalProperties": false
      },
      "BotMusicState": {
        "type": "object",
        "required": [
          "version",
          "settings",
          "queue",
          "playlist",
          "current",
          "playbackId",
          "paused",
          "volume",
          "positionSeconds",
          "playlistCursor"
        ],
        "properties": {
          "version": {
            "const": 1
          },
          "settings": {
            "$ref": "#/components/schemas/BotMusicSettings"
          },
          "queue": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BotMusicEntry"
            },
            "maxItems": 100
          },
          "playlist": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BotMusicTrack"
            },
            "maxItems": 500
          },
          "current": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/BotMusicEntry"
              },
              {
                "type": "null"
              }
            ]
          },
          "playbackId": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "paused": {
            "type": "boolean"
          },
          "volume": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "positionSeconds": {
            "type": "integer",
            "minimum": 0,
            "maximum": 86400
          },
          "playlistCursor": {
            "type": "integer",
            "minimum": 0,
            "maximum": 500
          },
          "player": {
            "anyOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": [
                  "id",
                  "generation",
                  "expiresAt"
                ],
                "properties": {
                  "id": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 128,
                    "pattern": "^[a-zA-Z0-9_-]+$"
                  },
                  "generation": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 9007199254740991
                  },
                  "expiresAt": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 9007199254740991
                  }
                }
              }
            ]
          },
          "controlRevision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          }
        }
      },
      "BotMusicSnapshot": {
        "type": "object",
        "required": [
          "revision",
          "state"
        ],
        "properties": {
          "revision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "state": {
            "$ref": "#/components/schemas/BotMusicState"
          }
        }
      },
      "BotMusicAction": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "kind",
              "query"
            ],
            "properties": {
              "kind": {
                "const": "request"
              },
              "query": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500,
                "description": "Provider URL or search text. Metadata is resolved on the server; no control characters."
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind",
              "query"
            ],
            "properties": {
              "kind": {
                "const": "playlist-add"
              },
              "query": {
                "type": "string",
                "minLength": 1,
                "maxLength": 500,
                "description": "Provider URL or search text. Metadata is resolved on the server; no control characters."
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind",
              "settings"
            ],
            "properties": {
              "kind": {
                "const": "settings"
              },
              "settings": {
                "$ref": "#/components/schemas/BotMusicSettings"
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind",
              "provider",
              "id"
            ],
            "properties": {
              "kind": {
                "const": "playlist-remove"
              },
              "provider": {
                "type": "string",
                "enum": [
                  "youtube",
                  "soundcloud"
                ]
              },
              "id": {
                "type": "string",
                "maxLength": 32
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind",
              "entryId"
            ],
            "properties": {
              "kind": {
                "const": "remove"
              },
              "entryId": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128,
                "pattern": "^[a-zA-Z0-9_-]+$"
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind",
              "entryId"
            ],
            "properties": {
              "kind": {
                "const": "promote"
              },
              "entryId": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128,
                "pattern": "^[a-zA-Z0-9_-]+$"
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind",
              "entryId",
              "position"
            ],
            "properties": {
              "kind": {
                "const": "move"
              },
              "entryId": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128,
                "pattern": "^[a-zA-Z0-9_-]+$"
              },
              "position": {
                "type": "integer",
                "minimum": 1,
                "maximum": 100
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind",
              "volume"
            ],
            "properties": {
              "kind": {
                "const": "volume"
              },
              "volume": {
                "type": "integer",
                "minimum": 0,
                "maximum": 100
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind",
              "playbackId",
              "positionSeconds"
            ],
            "properties": {
              "kind": {
                "const": "seek"
              },
              "playbackId": {
                "type": "integer",
                "minimum": 1,
                "maximum": 9007199254740991
              },
              "positionSeconds": {
                "type": "integer",
                "minimum": 0,
                "maximum": 86400
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "const": "play"
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "const": "pause"
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "const": "skip"
              }
            },
            "additionalProperties": false
          },
          {
            "type": "object",
            "required": [
              "kind"
            ],
            "properties": {
              "kind": {
                "const": "clear"
              }
            },
            "additionalProperties": false
          }
        ]
      },
      "BotMusicUpdate": {
        "type": "object",
        "required": [
          "operationKey",
          "revision",
          "action"
        ],
        "properties": {
          "operationKey": {
            "type": "string",
            "minLength": 16,
            "maxLength": 100,
            "pattern": "^[a-zA-Z0-9_-]+$",
            "description": "Unique per logical operation within this owner's API namespace. Reuse unchanged on an uncertain retry."
          },
          "revision": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "action": {
            "$ref": "#/components/schemas/BotMusicAction"
          }
        },
        "additionalProperties": false
      },
      "AlertsWachtrijRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "actie"
        ],
        "properties": {
          "actie": {
            "type": "string",
            "enum": [
              "pauzeer",
              "hervat",
              "skip"
            ]
          },
          "duurMinuten": {
            "type": "integer",
            "minimum": 1,
            "maximum": 60,
            "description": "Alleen bij pauzeer: auto-hervatten na deze duur. Weglaten = pauzeren tot hervat."
          }
        }
      },
      "AlertsReplayRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "donatieId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 30,
            "pattern": "^[a-zA-Z0-9]+$",
            "description": "Laat weg om de laatste paid-donatie te herspelen."
          }
        }
      },
      "StatsTotaalResponse": {
        "type": "object",
        "required": [
          "periode",
          "totaalCent",
          "aantal"
        ],
        "properties": {
          "periode": {
            "type": "string",
            "enum": [
              "vandaag",
              "24uur",
              "doel",
              "altijd"
            ]
          },
          "totaalCent": {
            "type": "integer",
            "minimum": 0
          },
          "aantal": {
            "type": "integer",
            "minimum": 0
          }
        }
      }
    },
    "responses": {
      "Error": {
        "description": "API error. Retry 429 and 503 after 60 seconds.",
        "headers": {
          "X-Request-Id": {
            "schema": {
              "type": "string"
            }
          },
          "Retry-After": {
            "description": "Present for 429 and 503",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "object",
                  "required": [
                    "code",
                    "message",
                    "request_id"
                  ],
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "request_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
