{
  "openapi": "3.1.0",
  "info": {
    "title": "VibeBot owner API",
    "version": "1.0.0",
    "description": "Read one bot's data and status from your own server. Read only. Use https://www.vibebot.gg: https://vibebot.gg answers with a 308 redirect, and some clients drop the Authorization header on a redirect. A query string is at most 2048 characters; a longer one answers 400. Send the key from a server or an edge function, never from browser code. Docs: https://www.vibebot.gg/docs/integrations/web-apps"
  },
  "servers": [
    {
      "url": "https://www.vibebot.gg"
    }
  ],
  "security": [
    {
      "ownerKey": []
    }
  ],
  "paths": {
    "/api/v1/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Check that a key works",
        "description": "Any valid key. Reads no bot data. In v1, guildId and expiresAt are always null.",
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "example": {
                  "botId": "3f2b8c1e-5d4a-4b7e-9c0d-1a2b3c4d5e6f",
                  "scopes": [
                    "data:read",
                    "status:read"
                  ],
                  "guildId": null,
                  "keyPrefix": "vbk_Xy12AbCd",
                  "expiresAt": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/bots/{botId}": {
      "get": {
        "operationId": "getBot",
        "summary": "Bot status as last reported",
        "description": "Scope status:read.",
        "parameters": [
          {
            "$ref": "#/components/parameters/botId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BotStatus"
                },
                "example": {
                  "id": "3f2b8c1e-5d4a-4b7e-9c0d-1a2b3c4d5e6f",
                  "name": "Coin Bot",
                  "status": "online",
                  "lastHeartbeat": "2026-10-12T15:04:05.000Z",
                  "guildId": "123456789012345678",
                  "guildName": "My Server",
                  "configVersion": 12,
                  "createdAt": "2026-09-01T10:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/bots/{botId}/data": {
      "get": {
        "operationId": "listDataTypes",
        "summary": "The data types of one server",
        "description": "Scope data:read. Each type with its rows: member rows only, the rows the list call can return. By name, in byte order. Internal types never appear. At most 500 types, the first ones by name; truncated is true when the server has more.",
        "parameters": [
          {
            "$ref": "#/components/parameters/botId"
          },
          {
            "$ref": "#/components/parameters/guildId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataTypes"
                },
                "example": {
                  "guildId": "123456789012345678",
                  "types": [
                    {
                      "type": "coins",
                      "rows": 3
                    },
                    {
                      "type": "profile",
                      "rows": 1
                    }
                  ],
                  "truncated": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/bots/{botId}/data/{type}": {
      "get": {
        "operationId": "listData",
        "summary": "Top rows of one data type in one server",
        "description": "Scope data:read. Ranked like the bot's own leaderboard. Rows that are a bare number rank by their value. Object rows rank last unless sortField names a numeric field. Rows with the same rank sort by discordUserId in text order. A type the bot never wrote answers 200 with items [] and nextCursor null, so an empty list can mean a misspelled type. Each page reads live data: while scores change, a row can repeat or be skipped, and the cursor stays valid.",
        "parameters": [
          {
            "$ref": "#/components/parameters/botId"
          },
          {
            "$ref": "#/components/parameters/type"
          },
          {
            "$ref": "#/components/parameters/guildId"
          },
          {
            "name": "sortField",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_]{1,64}$"
            },
            "description": "A numeric field of object rows to rank by. Bare-number rows rank by their value. Rows with no numeric value rank last."
          },
          {
            "name": "sortOrder",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 50
            },
            "description": "A whole number of 1 or more; anything else answers 400 Invalid limit. A value over 100 returns 100 rows."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "nextCursor from the previous page, unchanged. Send the same guildId, sortField and sortOrder. A changed cursor, or one from another query, answers 400 Invalid cursor. Then start again from the first page with no cursor."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataList"
                },
                "example": {
                  "guildId": "123456789012345678",
                  "items": [
                    {
                      "discordUserId": "234567890123456789",
                      "data": 1250
                    },
                    {
                      "discordUserId": "345678901234567890",
                      "data": 980
                    }
                  ],
                  "nextCursor": "WyI5ODAiLCIzNDU2Nzg5MDEyMzQ1Njc4OTAiXQ.8PI2swCPv-yHL5VBdZ6OjD_r0y-YGjKikTlEAuPL86E"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/bots/{botId}/data/{type}/{discordUserId}": {
      "get": {
        "operationId": "getDataRow",
        "summary": "One member's row of one data type in one server",
        "description": "Scope data:read. A member with no row answers 200 with exists false, data null and no dates.",
        "parameters": [
          {
            "$ref": "#/components/parameters/botId"
          },
          {
            "$ref": "#/components/parameters/type"
          },
          {
            "name": "discordUserId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^\\d{5,25}$"
            },
            "description": "A Discord user ID. Anything else answers 404."
          },
          {
            "$ref": "#/components/parameters/guildId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DataRow"
                },
                "examples": {
                  "exists": {
                    "summary": "A member with a row",
                    "value": {
                      "discordUserId": "234567890123456789",
                      "guildId": "123456789012345678",
                      "exists": true,
                      "data": {
                        "coins": 1250,
                        "level": 7
                      },
                      "createdAt": "2026-09-02T18:30:00.000Z",
                      "updatedAt": "2026-10-12T14:59:12.000Z"
                    }
                  },
                  "missing": {
                    "summary": "A member with no row",
                    "value": {
                      "discordUserId": "234567890123456789",
                      "guildId": "123456789012345678",
                      "exists": false,
                      "data": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ownerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An owner API key, vbk_ followed by 43 characters. Make one in the bot's Integrations, API keys. Pro only. Shown once."
      }
    },
    "parameters": {
      "botId": {
        "name": "botId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "The VibeBot bot UUID that GET /api/v1/me returns. It is not the Discord application ID. Any other UUID (another bot's, or no bot) answers 404 {\"error\":\"Not found\"} and counts toward the lockout. A value that is not a UUID answers 404 {\"error\":\"Bot not found\"}."
      },
      "type": {
        "name": "type",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{1,64}$"
        },
        "description": "The type argument in the bot's botData calls. GET /api/v1/bots/{botId}/data lists them. A name that does not match the pattern, starts with _, or is sharding_warning answers 404."
      },
      "guildId": {
        "name": "guildId",
        "in": "query",
        "required": true,
        "schema": {
          "type": "string",
          "pattern": "^(\\d{5,25}|global)$"
        },
        "description": "A Discord server ID (5 to 25 digits), or global. Rows the bot wrote with no server are under global. A missing or malformed value answers 400 Missing guildId. To copy a server ID: turn on Developer Mode in Discord, right-click the server, then click Copy Server ID."
      }
    },
    "headers": {
      "RateLimitRemaining": {
        "schema": {
          "type": "integer"
        },
        "description": "Requests left in the current window: the per-key limit on a 200, the limit that refused the call on a 429."
      },
      "RateLimitReset": {
        "schema": {
          "type": "integer"
        },
        "description": "When that window resets, in milliseconds since the Unix epoch."
      },
      "RetryAfter": {
        "schema": {
          "type": "integer"
        },
        "description": "Seconds to wait."
      }
    },
    "schemas": {
      "Me": {
        "type": "object",
        "required": [
          "botId",
          "scopes",
          "guildId",
          "keyPrefix",
          "expiresAt"
        ],
        "properties": {
          "botId": {
            "type": "string",
            "format": "uuid"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "data:read",
                "status:read"
              ]
            }
          },
          "guildId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Always null in v1."
          },
          "keyPrefix": {
            "type": "string"
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Always null in v1. Keys do not expire."
          }
        }
      },
      "BotStatus": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "lastHeartbeat",
          "guildId",
          "guildName",
          "configVersion",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "As the bot last reported it. It can lag. The code writes online, offline, deploying, error or stopped. Anything except online means the bot is not up."
          },
          "lastHeartbeat": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "guildId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The one server connected in the dashboard, or null."
          },
          "guildName": {
            "type": [
              "string",
              "null"
            ]
          },
          "configVersion": {
            "type": "integer"
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "DataTypes": {
        "type": "object",
        "required": [
          "guildId",
          "types",
          "truncated"
        ],
        "properties": {
          "guildId": {
            "type": "string"
          },
          "types": {
            "type": "array",
            "maxItems": 500,
            "items": {
              "type": "object",
              "required": [
                "type",
                "rows"
              ],
              "properties": {
                "type": {
                  "type": "string"
                },
                "rows": {
                  "type": "integer",
                  "description": "Member rows of this type in this server."
                }
              }
            }
          },
          "truncated": {
            "type": "boolean",
            "description": "True when the server has more than 500 types. types then holds the first 500 by name."
          }
        }
      },
      "DataList": {
        "type": "object",
        "required": [
          "guildId",
          "items",
          "nextCursor"
        ],
        "properties": {
          "guildId": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "discordUserId",
                "data"
              ],
              "properties": {
                "discordUserId": {
                  "type": "string"
                },
                "data": {
                  "description": "Any JSON the bot saved: a bare number, a string, an object, an array. Top-level keys that start with _ are removed. The API returns discordUserId only, with no names.",
                  "examples": [
                    1250,
                    {
                      "coins": 1250,
                      "level": 7
                    }
                  ]
                }
              }
            }
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as cursor to read the next page. null on the last page."
          }
        }
      },
      "DataRow": {
        "type": "object",
        "required": [
          "discordUserId",
          "guildId",
          "exists",
          "data"
        ],
        "properties": {
          "discordUserId": {
            "type": "string"
          },
          "guildId": {
            "type": "string"
          },
          "exists": {
            "type": "boolean"
          },
          "data": {
            "description": "null when exists is false. Otherwise any JSON the bot saved: a bare number, a string, an object, an array. Top-level keys that start with _ are removed. The API returns discordUserId only, with no names.",
            "examples": [
              1250,
              {
                "coins": 1250,
                "level": 7
              }
            ]
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "BadRequest": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "RateLimitError": {
        "type": "object",
        "required": [
          "error",
          "retryAfter",
          "resetAt"
        ],
        "properties": {
          "error": {
            "type": "string",
            "const": "Rate limit exceeded"
          },
          "retryAfter": {
            "type": "integer",
            "description": "Seconds to wait."
          },
          "resetAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the window resets, as an ISO date."
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Bad query. Sent only after the key passed. Check error, not message. Each example is one 400 the API can send.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/BadRequest"
            },
            "examples": {
              "MissingguildId": {
                "summary": "Every data call. Also sent for a malformed guildId.",
                "value": {
                  "error": "Missing guildId",
                  "message": "Add ?guildId=<Discord server id>, or ?guildId=global for DM and bot-wide data."
                }
              },
              "InvalidsortField": {
                "summary": "The list call.",
                "value": {
                  "error": "Invalid sortField",
                  "message": "Use 1 to 64 letters, digits or underscores."
                }
              },
              "InvalidsortOrder": {
                "summary": "The list call.",
                "value": {
                  "error": "Invalid sortOrder",
                  "message": "Use asc or desc."
                }
              },
              "Invalidlimit": {
                "summary": "The list call. A value over 100 is not an error: it returns 100 rows.",
                "value": {
                  "error": "Invalid limit",
                  "message": "Use a whole number from 1 to 100."
                }
              },
              "Invalidcursor": {
                "summary": "The list call. Check error === \"Invalid cursor\", then start again from page 1 with no cursor.",
                "value": {
                  "error": "Invalid cursor",
                  "message": "Pass nextCursor from the previous page unchanged, with the same guildId, sortField and sortOrder."
                }
              },
              "Querytoolong": {
                "summary": "Every call.",
                "value": {
                  "error": "Query too long",
                  "message": "Keep the query string to at most 2048 characters."
                }
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No key, a malformed key, a wrong key or a revoked key. Body is always {\"error\":\"Unauthorized\"}.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Unauthorized"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The account that made the key has no Pro, or the account is frozen. Body is always {\"error\":\"Forbidden\"}.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Forbidden"
            }
          }
        }
      },
      "NotFound": {
        "description": "Any other UUID as botId (another bot's, or no bot), a bot the key cannot reach, an internal data type name, a member ID that is not a Discord ID, or the API is switched off: {\"error\":\"Not found\"}. A botId that is not a UUID: {\"error\":\"Bot not found\"}.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "notFound": {
                "summary": "Every 404 the route handlers send",
                "value": {
                  "error": "Not found"
                }
              },
              "botNotFound": {
                "summary": "A botId that is not a UUID (bot paths only)",
                "value": {
                  "error": "Bot not found"
                }
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Over a limit. Wait for Retry-After.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RateLimitError"
            },
            "example": {
              "error": "Rate limit exceeded",
              "retryAfter": 12,
              "resetAt": "2026-10-12T15:05:00.000Z"
            }
          }
        }
      },
      "ServerError": {
        "description": "Server error. Retry after a short wait.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Internal server error"
            }
          }
        }
      }
    }
  }
}
