Publisher API
Add Cordvertise ad delivery to your existing Discord bot and earn on every confirmed view, click, join, stay, and survey completion. Three endpoints, plain JSON, no SDK.
Every request requires two identifiers: your api_key (from the dashboard) and your external_bot_id (your bot's Discord application ID — the 17–20 digit number from the Discord Developer Portal under your app's General Information page).
You already have a Discord bot with commands, buttons, and interaction handlers. You do not replace your bot with ours. You hook into your own interactions — whenever it makes sense in your bot's flow (a command, a button, a menu) — and call our three endpoints to deliver an ad.
Your bot sends the captcha, your bot sends the ad embed, your bot confirms delivery. Our API handles everything else: ad selection, billing, targeting, and payout. You keep your existing commands and features completely intact.
Standard payout is 35% of what the advertiser pays per event. Send optional member data (member_data_provided, user_roles, is_booster) and earn 45%. Payout applies to views, clicks, joins, and stays — whatever campaign type the ad uses.
Authentication
There are no auth headers. Pass your api_key and external_bot_id in the body of every request.
| Field | Description |
|---|---|
| api_key | Your API key from the dashboard. Never expose this client-side. |
| external_bot_id | Your bot's Discord application ID — a 17–20 digit snowflake from the Discord Developer Portal. |
401 with "error":"auth_failed". Suspended key returns 403 with "error":"key_not_active".
The 3-step flow
views/confirm after the Discord message is sent — pass display_status: "sent" and the message_id Discord returns. If sending fails, pass display_status: "failed" and no billing occurs.
Integration snippets
- JavaScript (discord.js)
- TypeScript (discord.js)
- Python (discord.py)
- Java (JDA)
- C# (Discord.Net)
- Go (discordgo)
- Rust (serenity)
Drop-in snippets for your existing bot. Copy the helper and the three handlers into your existing interaction handler — do not replace your bot. All snippets use a cv: prefix on customIds so they never clash with your own. The API key and base URL are pre-filled if you arrived from the dashboard.
Generates a captcha challenge. Returns a PNG image (base64 data URI) containing 6 characters the user must type to prove they're human.
Request body| Field | Description | |
|---|---|---|
| api_key | required | Your API key. |
| external_bot_id | required | Your bot's Discord application ID (snowflake). |
| event_id | required | Unique ID for this interaction. Use interaction.id. Max 256 chars. |
| user_id | required | Discord user ID of the person solving the captcha. |
{
"ok": true,
"captcha_token": "a3f8c2...", // pass this to ads/request
"challenge_text": "data:image/png;base64,...", // render as an image
"prompt_text": "Type the 6 characters shown (left to right).",
"expires_in_seconds": 60
}
429 with "error":"captcha_locked" and a lockout_until_utc timestamp.
captcha_token is a 48-character hex string — safe to embed directly in a Discord button customId (Discord's 100-character limit is not a concern). The integration snippets use cv:cap:<token> (55 chars total).
guild_id — ad delivery only works in servers. Block your ad command in DMs, or check for a null guild before calling any endpoint.
Verifies the captcha answer and returns an ad. Always check if ad is null before sending anything to Discord.
| Field | Description | |
|---|---|---|
| api_key | required | Your API key. |
| external_bot_id | required | Your bot's Discord application ID (snowflake). |
| event_id | required | Same value as captcha/start. Use interaction.id. |
| user_id | required | Discord user ID of the member requesting the ad. Used for user profiling and stay campaign tracking — must be accurate. |
| captcha_token | required | Token from captcha/start. |
| answer | required | The user's typed captcha answer. Case-insensitive. Max 64 chars. |
| guild_id | required | Discord guild ID. Required for per-guild rate limiting and stay campaign tracking. |
| guild_name | required | Server name from guild.name. Used for AI-based ad targeting classification — determines which ads match your server. |
| guild_description | required | Server description from guild.description. Use empty string "" if none. |
| language | required | 2-letter ISO code from guild.preferredLocale (e.g. "en", "de"). Only ads matching this language are served — wrong value = missed revenue. |
| guild_roles | required | Array of role name strings from the server. Pass guild.roles.cache.map(r => r.name).filter(r => r !== '@everyone'). Send empty array [] if guild has none. No extra bot permissions needed. |
| channel_id | optional | Discord channel ID. Logged for analytics. |
| member_data_provided | optional | Set true if you are also sending user_roles and is_booster. Unlocks 45% payout tier. Must only be set if your bot has the GuildMembers privileged intent. |
| user_roles | optional | Array of role names the requesting user has. Used for demographic profiling (gender, device, language inference via AI). Only send if member_data_provided: true. |
| is_booster | optional | Boolean — whether the user is a Nitro booster of this server. Only send if member_data_provided: true. |
| event_type | optional | slash_command / button / select_menu. Logged for analytics. |
{
"ok": true,
"ad": {
"ad_id": "123",
"banner_url": "https://...", // embed image
"button_name": "Visit site", // link button label
"click_url": "https://.../c/TOKEN", // link button URL — tracks clicks
"embed_color": "#6D5EFC",
"target_type": "views" // "views" | "clicks" | "surveys"
},
"view_token": "eyJ...", // pass to views/confirm
"expires_in_seconds": 120
}
click_url as your Discord button URL — never button_url. click_url is the tracking redirect that logs clicks and credits your earnings.
{
"ok": true,
"ad": null,
"view_token": null,
"denied_reason": "user_daily_cap" // see Errors section
}
When ad is null, skip silently — do not send any embed.
Records the delivery and triggers billing. Call this after the Discord message is sent. This is when your earnings are recorded.
Request body| Field | Description | |
|---|---|---|
| api_key | required | Your API key. |
| external_bot_id | required | Your bot's application ID. |
| event_id | required | Same value used throughout the flow. |
| view_token | required | JWT from ads/request. Single-use. |
| display_status | required | "sent" if message delivered. Anything else = not billed. |
| message_id | required* | Discord message ID. Required when display_status is "sent". |
| guild_id / channel_id / user_id | optional | Logged for analytics. |
| language | optional | ISO 639-1 language code of the server. Updates the language registry for this guild. |
// Billed
{ "ok": true, "billed": true, "ad_id": "123", "auto_paused": false, "daily_cap_reached": false }
// Not billed
{ "ok": true, "billed": false, "reason": "token_expired" }
Campaign types
The ad.target_type field in the ads/request response tells you what kind of campaign was served. Your integration code does not need to change per type — the flow is always the same. Payouts differ because advertisers pay different rates per action.
| target_type | What the advertiser pays for | Your action |
|---|---|---|
| views | Ad displayed to the user | Send the embed. Confirm with display_status: "sent". Billing happens on confirm. |
| clicks | User clicks the ad button | Same as views — send embed, confirm delivery. The click_url is a tracking redirect. Billing happens when the user clicks. |
| surveys | User completes a survey | Same flow. The button opens a survey. Billing happens on survey completion, not on view. |
| joins | User joins the advertiser's server | Same flow — serve the embed. The click_url is a tracking redirect that records the click and sends the user to the invite. Join confirmation is handled by our infrastructure — no extra code needed. |
| stays | User joins and stays for the required duration | Same as joins. Stay tracking is handled entirely by our backend once the user clicks through — no extra code needed. |
Joins & stays
Join and stay campaigns pay when a user joins the advertiser's Discord server (and optionally stays for a set duration). Your integration is identical to any other campaign — run the normal 3-step flow. No extra code needed.
How it worksclick_url for join campaigns is a /j/TOKEN tracking redirect. When the user clicks it, we record their Discord user ID (from the user_id you sent in ads/request) and redirect them to the invite. Join and stay confirmation is handled entirely by our backend — your bot does nothing extra.
A stay campaign requires the user to remain in the server for a set duration before payout is recorded. This is tracked automatically by our backend after the join — no additional code or events needed from your bot.
Errors & denials
HTTP errors| Status | error | Meaning & action |
|---|---|---|
| 400 | bad_request | Missing or invalid fields. |
| 401 | auth_failed | API key invalid. |
| 403 | key_not_active | Key suspended. |
| 403 | bad_signature | view_token tampered. |
| 403 | token_identity_mismatch | Token belongs to different key/bot. |
| 429 | rate_limited | Request rate exceeded. Back off and retry. |
| 429 | captcha_locked | User locked 10 min. Includes lockout_until_utc. |
denied_reason)
| denied_reason | What to do |
|---|---|
| captcha_required | Missing token or answer — restart from captcha/start. |
| captcha_failed | Wrong answer — let user retry (3 attempts before lockout). |
| captcha_expired | Token TTL elapsed (60s) — restart from captcha/start. |
| captcha_already_used | Token already consumed — restart from captcha/start. |
| user_minute_cap | User saw an ad <1 min ago — skip. Includes retry_after_seconds. |
| user_daily_cap | User hit 10 ads/day — skip until UTC midnight. |
| guild_minute_cap | Server hit 100 ads/min — skip. Includes retry_after_seconds. |
| partner_minute_cap | Your key hit its per-minute cap — back off. Includes retry_after_seconds. |
| no_ads | No ads available right now — skip silently. |
reason)
| reason | Meaning |
|---|---|
| not_sent | display_status was not "sent" — correct for failed sends. |
| missing_message_id | Forgot to pass message_id. |
| token_expired | Confirm faster — within expires_in_seconds. |
| token_used | Token already confirmed. |
| already_confirmed | Same event_id already confirmed — idempotent, safe to ignore. |
| ad_unavailable | Ad paused between request and confirm. |
| advertiser_insufficient_credits | Advertiser ran out of credits. |
Rate limits & delivery caps
Request rate limits (per publisher key)| Tier | Requests / min | Requirement |
|---|---|---|
| Probation | 100 × bot count | Default for new keys |
| Standard | 250 × bot count | 50+ clicks in 30 days |
| Pro | 500 × bot count | 250+ clicks in 30 days |
| Enterprise | 1000 × bot count | 750+ clicks in 30 days |
external_bot_id values registered under your API key. One key, one bot = multiplier of 1. If you register multiple bots under one key, the limit scales accordingly.
ad: null, not HTTP errors)
| Cap | Limit | Scope |
|---|---|---|
| User per-minute | 1 ad / min | Per (your key + user) |
| User per-day | 10 ads / UTC day | Per (your key + user) |
| Guild per-minute | 100 ads / min | Per (your key + guild) |
Idempotency
Calling views/confirm multiple times with the same event_id returns already_confirmed and does not double-bill. Safe to retry on network failure.
(api_client_id, external_bot_id, event_id). Using interaction.id as your event_id guarantees uniqueness since every Discord interaction has a unique ID.