Funnels by bot
Your API key
Treat it like a password: whoever has it acts as you and earns to your balance. Keep it in an environment variable on your server — never in code that ships to users.
Webhooks
Get a signed callback when a join is credited or reverted (member left), so your economy stays in sync. We POST JSON to your URL with an HMAC-SHA256 signature in the X-Vemoni-Signature header — see the docs below.
Payout details (LTC address)
Sign in to open your cabinet
Use the Discord account that should receive the money. Your key, balance, payout details and withdrawals all live here.
How it works, start to finish
Create a key
Sign in with Discord and generate your key above. It is how we know which account to pay.
Offer a sponsor
Ask GET /api/ad which server to promote, then show your user its invite next to the reward you are giving.
Confirm, then reward
After they join, call POST /api/join-check. We confirm it with Discord and credit you — then you hand over the reward.
The base URL and your key
Every endpoint sits under one base URL. Send your key with each request — either as a bearer token or in the X-API-Key header. Because calls come from your server, the key never touches a browser.
# Base URL https://api.vemoni.info # Send your key one of these two ways Authorization: Bearer <your-key> X-API-Key: <your-key>
A working example in one screen
The full reward loop (Node.js). Three IDs, all required: userId — the Discord user you're rewarding; serverId — the server your bot runs in (we pick a sponsor for it); botId — your bot's application (client) ID.
const BASE = 'https://api.vemoni.info'; const h = { 'Authorization': 'Bearer ' + process.env.VEMONI_KEY, 'Content-Type': 'application/json' }; // 1) Ask which sponsor to show, then show it to your user const ad = await fetch(`${BASE}/api/ad?serverId=${serverId}&botId=${botId}&userId=${userId}`, { headers: h }).then(r => r.json()); if (ad.sponsor) showToUser(ad.sponsor.name, ad.sponsor.invite, 'Join for +100 coins!'); // 2) When they say they joined, confirm it — we check Discord and pay you const out = await fetch(`${BASE}/api/join-check`, { method: 'POST', headers: h, body: JSON.stringify({ userId, botId, sponsorId: ad.sponsor && ad.sponsor.guildId }) }).then(r => r.json()); // One field to switch on — reward ONLY on 'credited'. if (out.status === 'credited') grantBonus(userId, 100); else if (out.status === 'not_joined') tellUser('Join the server first, then press Check.'); else if (out.status === 'already_counted') tellUser('You already got the reward for this server.'); // 'no_ad' → nothing to promote right now → let them through, no reward.
Examples: discord.py & discord.js
The same three steps in a real bot — ask for an ad, show it, verify when the user presses your button.
# discord.py import os, aiohttp BASE = "https://api.vemoni.info" H = { "Authorization": "Bearer " + os.environ["VEMONI_KEY"] } async def api(method, path, **kw): async with aiohttp.ClientSession() as s: async with s.request(method, BASE + path, headers=H, **kw) as r: return await r.json() # 1) on your command: get an ad and show it ad = await api("GET", f"/api/ad?serverId={gid}&botId={bid}&userId={uid}") if ad.get("sponsor"): sp = ad["sponsor"] await show_button(f"Join {sp['name']}: {sp['invite']}", sponsor_id=sp["guildId"]) # 2) on the "I joined" button: verify + reward out = await api("POST", "/api/join-check", json={"userId": uid, "botId": bid, "sponsorId": sponsor_id}) if out["status"] == "credited": grant_bonus(uid, 100) elif out["status"] == "not_joined": reply("Join the server first, then press Check.") elif out["status"] == "already_counted": reply("You already got the reward for this server.") # 'no_ad' → let them through, no reward
// discord.js const BASE = 'https://api.vemoni.info'; const H = { Authorization: 'Bearer ' + process.env.VEMONI_KEY, 'Content-Type': 'application/json' }; const api = (m, p, body) => fetch(BASE + p, { method: m, headers: H, body: body && JSON.stringify(body) }).then(r => r.json()); // 1) on your slash command const ad = await api('GET', `/api/ad?serverId=${gid}&botId=${bid}&userId=${uid}`); if (ad.sponsor) showButton(`Join ${ad.sponsor.name}: ${ad.sponsor.invite}`, ad.sponsor.guildId); // 2) on the "I joined" button const out = await api('POST', '/api/join-check', { userId: uid, botId: bid, sponsorId }); if (out.status === 'credited') grantBonus(uid, 100); else if (out.status === 'not_joined') reply('Join the server first, then press Check.'); else if (out.status === 'already_counted') reply('You already got the reward for this server.');
The endpoints, one by one
Parameters (all required): ?serverId=<guild>&botId=<bot>&userId=<user>. Returns { sponsor: { guildId, name, invite } | null, fallbackText } — build your message from sponsor.name and sponsor.invite. If sponsor is null, there's nothing to show: let the user through with no reward. 400 if a parameter is missing.
Body: { userId, botId, sponsorId? }. sponsorId is the guildId from /api/ad — pass it to pin the check to that exact sponsor (recommended if your bot runs several ad commands at once); omit it and we use the most recent ad shown to this user. Every response has a status field — switch on it and reward only on "credited": 200 "credited" — join verified and you were paid → give the reward; 200 "not_joined" (joined:false) — not in the sponsor yet → ask them to join, no reward; 200 "already_counted" — already credited for this sponsor → no reward; 200 "no_ad" — nothing was shown → no reward; 503 "uncertain" — couldn't check right now, retry. Counts once per membership; leaving reverses the credit, a real rejoin counts again.
Errors, limits & rules
- The sponsor must run the Vemoni bot. A join is credited only when our bot is on the sponsor server — that's how we verify membership. Servers without it are never offered by
/api/ad. - Errors are JSON
{ error, code }:401 invalid_key— bad or missing key;400— a required field is missing (codenames which);429— rate limited (honourRetry-After);503 uncertain— retry. - Rate limits are per-IP. Ask
/api/adonce per user action, and call/api/join-checkonly when the user presses your check button. Back off on429. - Safe to retry. A membership is credited once — a double click or repeated call returns
already_counted, never a double reward. - userId is verified. We check the user's real Discord membership of the shown sponsor, so passing IDs that didn't actually join earns nothing.
Webhooks
Set a URL in your cabinet (above) and we POST a signed event when a join is credited or reversed. Dedupe on eventId. Body:
{
"event": "credited", // or "reverted" (member left → undo the reward)
"eventId": "a1b2c3…", // unique per delivery
"timestamp": 1730000000000,
"user": "<discord user id>",
"sponsorId": "<guild id>",
"botId": "<your bot id>",
"amount": 0.05
}
Verify the X-Vemoni-Signature: sha256=… header against the raw body before trusting it:
// Node — verify the signature const crypto = require('crypto'); const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex'); if (expected !== req.headers['x-vemoni-signature']) return res.sendStatus(401);
Changelog
Latest changes (machine-readable at GET /api/changelog). Prefer the versioned path /v1/api/….
- 2026-07-22 — 200 (not 403) for not-joined;
statusenum; optionalsponsorId; errorcodes; signed webhooks;/v1alias. - 2026-07-19 —
join-checkno longer needsserverId; a real leave-then-rejoin counts again. - 2026-07-17 —
botId(anduserIdon/api/ad) became required.
Questions? We are around
Drop into our support server and we will help you wire it up.
