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

1. Web app → bot: your server posts JSON to a hook URL. 2. Bot → web app: your bot calls 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

  1. Open your bot. Open Integrations (under Configure on desktop). Click New hook.
  2. 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.
  3. 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

StatusMeaning
202Queued. Not yet handled.
400Body is not valid JSON.
401Wrong or missing secret.
404Unknown hook, paused hook, or wrong botId.
413Body over 64 KB.
429Rate limited: 120/min per bot, 300/min per IP. Retry after a short wait.
500Server 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

  1. Create an endpoint in your backend that accepts POST JSON.
  2. Open your bot. Open Integrations (under Configure on desktop), then Environment variables. Add DASHBOARD_URL and DASHBOARD_KEY. Values are never shown back.
  3. Press Put live. New env vars reach the bot only then.
  4. 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') and require('http') do not work.
  • env('NAME') returns undefined for a name you did not add. It never throws.

Mirror pattern (best for Lovable)

The bot pushes events and stats to a table in your own database. Your dashboard reads that table. Nothing in the dashboard talks to VibeBot.

3. Web app → bot data (owner API)

Server only, never in the browser

Call the owner API from your server or an edge function. Never put the key in browser code. Anyone who sees the key can read your bot data. Browsers on other sites cannot call this API.

Set up

  1. 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.
  2. 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.
  3. 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/me

GET /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/me

Example 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_ID

Example 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 type argument in your bot's botData calls, for example coins. The dashboard has no data viewer. To get the exact names, call GET /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 is sharding_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 under global. 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 1250 or {"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 sortField with the name of a numeric field. Rows with the same rank sort by discordUserId in 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 discordUserId only. It returns no names.

Responses

StatusMeaning
200OK. JSON body as listed above.
400Bad 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"}.
429A body like {"error":"Rate limit exceeded","retryAfter":12,"resetAt":"2026-10-12T15:05:00.000Z"}. Wait for the seconds in Retry-After.
500Server error. Retry after a short wait.

Every 400 the API sends. Check error, not message.

errormessage
Missing guildId (also sent for a malformed guildId)Add ?guildId=<Discord server id>, or ?guildId=global for DM and bot-wide data.
Invalid sortFieldUse 1 to 64 letters, digits or underscores.
Invalid sortOrderUse asc or desc.
Invalid limitUse a whole number from 1 to 100.
Invalid cursorPass nextCursor from the previous page unchanged, with the same guildId, sortField and sortOrder.
Query too longKeep 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.

LimitValue
Per key60 requests/min
Per bot (all its keys)120 requests/min
Per account300 requests/min (60/min on a Pro trial)
Per IP120 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 botMax 5
New keys per account10 per hour
Query stringAt 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 botId in 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.

Next Steps