Connect a web app
Send events from your app to your bot. Push bot data to your own backend. Give this page to your app builder.
Three paths work today
fetch() to your backend. 3. Web app → bot data: your server reads bot data and status with an API key. The owner API is read only. A web app cannot change bot data or settings directly.Prompt for your app builder
Paste this into Lovable or any agent.
Prompt
Read https://www.vibebot.gg/docs/integrations/web-apps and connect my dashboard to my VibeBot bot.
- To send commands to the bot, use path 1 (inbound hook). Call it from a server function, never from the browser. The hook URL and secret are in my env vars VIBEBOT_HOOK_URL and VIBEBOT_HOOK_SECRET.
- To receive data from the bot, use path 2 (mirror pattern). Create an endpoint that accepts POST JSON with header Authorization: Bearer <key> and writes the rows to my database. The dashboard reads my database only.
- To read bot data or bot status, use path 3 (owner API, read only). Call https://www.vibebot.gg/api/v1 from a server function with header Authorization: Bearer <key>. The key is in my env var VIBEBOT_API_KEY.
Do not try to change bot data or settings over an API. That does not exist.1. Web app → bot (inbound hooks)
Set up
- Open your bot. Open Integrations (under Configure on desktop). Click New hook.
- Copy the URL and the secret (
whsec_...). The secret shows once. If you lose it, click New secret on the hook. The URL stays the same. - Store both in your app's server env vars. Never put the secret in browser code.
Endpoint
- Method:
POST https://www.vibebot.gg/api/hooks/{botId}/{slug} - Header:
x-vibebot-secret: whsec_... - Body: a JSON object or array, max 64 KB.
- Reply:
202 {"ok":true}. No data comes back.
Server (Supabase edge function, Node, any)
// Runs on YOUR server. The secret never reaches the browser.
const res = await fetch(process.env.VIBEBOT_HOOK_URL, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-vibebot-secret': process.env.VIBEBOT_HOOK_SECRET,
},
body: JSON.stringify({ type: 'announce', channelId: '123', text: 'Hello' }),
});
// res.status === 202 means queued. The bot handles it on its next heartbeat.Responses
| Status | Meaning |
|---|---|
202 | Queued. Not yet handled. |
400 | Body is not valid JSON. |
401 | Wrong or missing secret. |
404 | Unknown hook, paused hook, or wrong botId. |
413 | Body over 64 KB. |
429 | Rate limited: 120/min per bot, 300/min per IP. Retry after a short wait. |
500 | Server error. Retry after a short wait. |
Limits
- Delivery is not instant. The bot picks up events on its next heartbeat.
- The queue keeps the 50 newest events per bot. Events expire after 1 hour.
- Max 10 hooks per bot.
- Hooks need your own deployed bot. VibeBot Basic (the shared bot) does not receive them.
In the bot
A behavior with the webhook trigger runs. Each behavior belongs to one hook, so give the Bot Builder the hook URL:
Builder prompt
Add a webhook trigger for my hook <paste the hook URL>: when my dashboard sends {type:'announce', channelId, text}, post text in that channel.In custom code, eventData is the posted JSON body. There is no interaction, message or member. Use client.channels to find a channel by ID in any server.
Custom code (webhook trigger)
const { type, channelId, text } = eventData;
if (type !== 'announce') return;
const channel = client.channels.cache.get(channelId);
// parse: [] stops a posted @everyone or role mention from pinging anyone.
if (channel) await channel.send({ content: text, allowedMentions: { parse: [] } });2. Bot → web app (fetch from custom code)
Set up
- Create an endpoint in your backend that accepts POST JSON.
- Open your bot. Open Integrations (under Configure on desktop), then Environment variables. Add
DASHBOARD_URLandDASHBOARD_KEY. Values are never shown back. - Press Put live. New env vars reach the bot only then.
- Ask the Bot Builder to post to it. Read keys with
env('NAME'). Do not hardcode them.
Builder prompt
When someone levels up, POST {event:'level_up', userId, level} to my endpoint from env var DASHBOARD_URL with header Authorization: Bearer <value of env var DASHBOARD_KEY>.Custom code (any trigger)
await fetch(env('DASHBOARD_URL'), {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: 'Bearer ' + env('DASHBOARD_KEY'),
},
// Add the fields you need, e.g. the member's ID and new level.
body: JSON.stringify({ event: 'level_up', at: Date.now() }),
});Rules
- Use a public URL. The Bot Builder refuses code with a private IP or an AI-provider URL in it.
- Use the global
fetch.require('axios')andrequire('http')do not work. env('NAME')returnsundefinedfor a name you did not add. It never throws.
Mirror pattern (best for Lovable)
3. Web app → bot data (owner API)
Server only, never in the browser
Set up
- Open your bot. Open Integrations (under Configure on desktop), then API keys. Click New key, give it a name, then click Create key. API keys need Pro.
- Copy the key (
vbk_...). The key shows once. Click Done when you have it. If you lose it, click Replace key. The old key stops working within a minute. - Store the key in your server env var
VIBEBOT_API_KEY.
A key reads one bot. It has the scopes data:read and status:read, reads every server of the bot, and does not expire. It stops working when you revoke it, when your Pro ends, or when you lose access to the bot.
Endpoints
Use https://www.vibebot.gg. https://vibebot.gg answers with a 308 redirect, and some clients drop the Authorization header on a redirect. botId is the VibeBot bot UUID that /api/v1/me returns. It is not the Discord application ID. Send the key in every request: Authorization: Bearer vbk_.... Every reply from these routes is JSON and is never cached.
Server (Supabase edge function, Node, any)
# Runs on YOUR server. The key never reaches the browser.
curl -H "Authorization: Bearer $VIBEBOT_API_KEY" https://www.vibebot.gg/api/v1/meGET /api/v1/me (scope any). Check that a key works. Reads no bot data. In v1, guildId and expiresAt are always null.
Reply: { botId, scopes, guildId, keyPrefix, expiresAt }
curl
curl -H "Authorization: Bearer $VIBEBOT_API_KEY" https://www.vibebot.gg/api/v1/meExample reply
{"botId":"3f2b8c1e-5d4a-4b7e-9c0d-1a2b3c4d5e6f","scopes":["data:read","status:read"],"guildId":null,"keyPrefix":"vbk_Xy12AbCd","expiresAt":null}GET /api/v1/bots/{botId} (scope status:read). The bot status as the bot last reported it: online, offline, deploying, error or stopped. Anything except online means the bot is not up. guildId is the one server connected in the dashboard, or null.
Reply: { id, name, status, lastHeartbeat, guildId, guildName, configVersion, createdAt }
curl
curl -H "Authorization: Bearer $VIBEBOT_API_KEY" https://www.vibebot.gg/api/v1/bots/$BOT_IDExample reply
{"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"}GET /api/v1/bots/{botId}/data (scope data:read). The data types of one server, by name in byte order, with the rows of each. Query: guildId (required). At most 500 types; truncated is true when the server has more.
Reply: { guildId, types: [{ type, rows }], truncated }
curl
curl -H "Authorization: Bearer $VIBEBOT_API_KEY" "https://www.vibebot.gg/api/v1/bots/$BOT_ID/data?guildId=$GUILD_ID"Example reply
{"guildId":"123456789012345678","types":[{"type":"coins","rows":3},{"type":"profile","rows":1}],"truncated":false}GET /api/v1/bots/{botId}/data/{type} (scope data:read). The top rows of one data type in one server, like a leaderboard. Query: guildId (required), sortField, sortOrder (asc or desc, default desc), limit (default 50), cursor. limit must be a whole number of 1 or more; a value over 100 returns 100 rows. For the next page, send nextCursor as cursor with the same guildId, sortField and sortOrder. nextCursor is null on the last page. On 400 Invalid cursor, start again from the first page with no cursor.
Reply: { guildId, items: [{ discordUserId, data }], nextCursor }
curl
curl -H "Authorization: Bearer $VIBEBOT_API_KEY" "https://www.vibebot.gg/api/v1/bots/$BOT_ID/data/coins?guildId=$GUILD_ID&limit=2"Example reply
{"guildId":"123456789012345678","items":[{"discordUserId":"234567890123456789","data":1250},{"discordUserId":"345678901234567890","data":980}],"nextCursor":"WyI5ODAiLCIzNDU2Nzg5MDEyMzQ1Njc4OTAiXQ.8PI2swCPv-yHL5VBdZ6OjD_r0y-YGjKikTlEAuPL86E"}GET /api/v1/bots/{botId}/data/{type}/{discordUserId} (scope data:read). One member's row of one data type in one server. Query: guildId (required). A member with no row gives exists: false and data: null, with no dates.
Reply: { discordUserId, guildId, exists, data, createdAt, updatedAt }
curl
curl -H "Authorization: Bearer $VIBEBOT_API_KEY" "https://www.vibebot.gg/api/v1/bots/$BOT_ID/data/profile/$USER_ID?guildId=$GUILD_ID"Example reply
{"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"}Data types, servers and rows
- Type: the
typeargument in your bot's botData calls, for examplecoins. The dashboard has no data viewer. To get the exact names, callGET /api/v1/bots/{botId}/data(above). - Unknown type: a type the bot never wrote gives 200 with
"items":[]and"nextCursor":null. The row endpoint gives"exists":false. So an empty list can mean a misspelled type. A 404 comes only for a name that is not 1 to 64 letters, digits,_or-, starts with_, or issharding_warning. - guildId: required on every data call. Use a server ID (5 to 25 digits) or
global. Rows the bot wrote with no server are underglobal. To copy a server ID, turn on Developer Mode in Discord, right-click the server, then click Copy Server ID. - data: any JSON the bot saved, for example
1250or{"coins":1250,"level":7}. Top-level keys that start with_are removed. - Order: rows that are a bare number rank by their value. Object rows rank last unless you pass
sortFieldwith the name of a numeric field. Rows with the same rank sort bydiscordUserIdin text order. - Paging: each page reads live data. If scores change while you page, a row can repeat or be skipped. The cursor stays valid.
- Members: the API returns
discordUserIdonly. It returns no names.
Responses
| Status | Meaning |
|---|---|
200 | OK. JSON body as listed above. |
400 | Bad query. The body is {"error":"...","message":"..."}. The list is below. |
401 | {"error":"Unauthorized"}. No key, a wrong key or a revoked key. |
403 | {"error":"Forbidden"}. The account that made the key has no Pro, or the account is frozen. |
404 | {"error":"Not found"}. Any other UUID as botId (another bot's, or no bot), a bot you cannot reach, an internal data type name, a member ID that is not a Discord ID, or the API is switched off. A botId that is not a UUID gets {"error":"Bot not found"}. |
429 | A body like {"error":"Rate limit exceeded","retryAfter":12,"resetAt":"2026-10-12T15:05:00.000Z"}. Wait for the seconds in Retry-After. |
500 | Server error. Retry after a short wait. |
Every 400 the API sends. Check error, not message.
| error | message |
|---|---|
Missing guildId (also sent for a malformed guildId) | Add ?guildId=<Discord server id>, or ?guildId=global for DM and bot-wide data. |
Invalid sortField | Use 1 to 64 letters, digits or underscores. |
Invalid sortOrder | Use asc or desc. |
Invalid limit | Use a whole number from 1 to 100. |
Invalid cursor | Pass nextCursor from the previous page unchanged, with the same guildId, sortField and sortOrder. |
Query too long | Keep the query string to at most 2048 characters. |
Limits
A 200 reply has X-RateLimit-Remaining and X-RateLimit-Reset for the per-key limit. X-RateLimit-Reset is in milliseconds since the Unix epoch. A 429 reply has them for the limit that refused the call, and Retry-After in seconds.
| Limit | Value |
|---|---|
| Per key | 60 requests/min |
| Per bot (all its keys) | 120 requests/min |
| Per account | 300 requests/min (60/min on a Pro trial) |
| Per IP | 120 requests/min |
| Wrong keys per IP (401, and 404 for a botId the key cannot read, count) | 20 per 10 min, then 429 until the 10 minutes end |
| Wrong tries of one key (401, and 404 for a botId the key cannot read) | 5 per hour, then 429 until the hour ends |
| Keys per bot | Max 5 |
| New keys per account | 10 per hour |
| Query string | At most 2048 characters |
Serverless hosts share IP addresses, so other callers can use up the per-IP limit. Cache replies on your side. A key that worked in the last 10 minutes skips the wrong-keys-per-IP lockout.
The OpenAPI file is at /openapi/owner-api-v1.json.
Troubleshooting
- 401: the secret is wrong. Check your server env var.
- 404: the hook is paused, or the
botIdin the URL is not this bot. - 202 but nothing happens: the bot is offline, no behavior uses this hook, or the bot is VibeBot Basic. After you delete a hook and make a new one, update the behavior to the new URL.
- fetch fails in the bot: the host is not public, the env var name does not match, or you did not press Put live after you added it.