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.

FieldDescription
api_keyYour API key from the dashboard. Never expose this client-side.
external_bot_idYour bot's Discord application ID — a 17–20 digit snowflake from the Discord Developer Portal.
Invalid key returns HTTP 401 with "error":"auth_failed". Suspended key returns 403 with "error":"key_not_active".

The 3-step flow

1. captcha/startGet challenge image
Show userDiscord message
2. ads/requestSubmit answer, get ad
Send embedDiscord message sent
3. views/confirmRecord delivery, earn
Always call 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.


        
POST/v1/captcha/start

Generates a captcha challenge. Returns a PNG image (base64 data URI) containing 6 characters the user must type to prove they're human.

FieldDescription
api_keyrequiredYour API key.
external_bot_idrequiredYour bot's Discord application ID (snowflake).
event_idrequiredUnique ID for this interaction. Use interaction.id. Max 256 chars.
user_idrequiredDiscord 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
}
After 3 failed attempts, the user is locked out for 10 minutes. You'll get HTTP 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).
This API requires a guild_id — ad delivery only works in servers. Block your ad command in DMs, or check for a null guild before calling any endpoint.
POST/v1/ads/request

Verifies the captcha answer and returns an ad. Always check if ad is null before sending anything to Discord.

FieldDescription
api_keyrequiredYour API key.
external_bot_idrequiredYour bot's Discord application ID (snowflake).
event_idrequiredSame value as captcha/start. Use interaction.id.
user_idrequiredDiscord user ID of the member requesting the ad. Used for user profiling and stay campaign tracking — must be accurate.
captcha_tokenrequiredToken from captcha/start.
answerrequiredThe user's typed captcha answer. Case-insensitive. Max 64 chars.
guild_idrequiredDiscord guild ID. Required for per-guild rate limiting and stay campaign tracking.
guild_namerequiredServer name from guild.name. Used for AI-based ad targeting classification — determines which ads match your server.
guild_descriptionrequiredServer description from guild.description. Use empty string "" if none.
languagerequired2-letter ISO code from guild.preferredLocale (e.g. "en", "de"). Only ads matching this language are served — wrong value = missed revenue.
guild_rolesrequiredArray 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_idoptionalDiscord channel ID. Logged for analytics.
member_data_providedoptionalSet 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_rolesoptionalArray 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_boosteroptionalBoolean — whether the user is a Nitro booster of this server. Only send if member_data_provided: true.
event_typeoptionalslash_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
}
Use 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.

POST/v1/views/confirm

Records the delivery and triggers billing. Call this after the Discord message is sent. This is when your earnings are recorded.

FieldDescription
api_keyrequiredYour API key.
external_bot_idrequiredYour bot's application ID.
event_idrequiredSame value used throughout the flow.
view_tokenrequiredJWT from ads/request. Single-use.
display_statusrequired"sent" if message delivered. Anything else = not billed.
message_idrequired*Discord message ID. Required when display_status is "sent".
guild_id / channel_id / user_idoptionalLogged for analytics.
languageoptionalISO 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_typeWhat the advertiser pays forYour action
viewsAd displayed to the userSend the embed. Confirm with display_status: "sent". Billing happens on confirm.
clicksUser clicks the ad buttonSame as views — send embed, confirm delivery. The click_url is a tracking redirect. Billing happens when the user clicks.
surveysUser completes a surveySame flow. The button opens a survey. Billing happens on survey completion, not on view.
joinsUser joins the advertiser's serverSame 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.
staysUser joins and stays for the required durationSame as joins. Stay tracking is handled entirely by our backend once the user clicks through — no extra code needed.
For views, clicks, and surveys — your integration is identical regardless of type. The three-step flow covers all of them. Joins and stays require one additional step in your bot.

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.

1. Serve adNormal 3-step flow
2. User clicksclick_url redirect
3. User joins serverDiscord invite
4. We detect joinOur infrastructure
5. Payout recordedAutomatic
The click_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.

Premium data — 45% payout

Standard payout is 35% of advertiser spend. If your bot has the Server Members Intent (a privileged intent from the Discord Developer Portal), you can send per-user member data on each ad request and earn 45%.

FieldValueHow to get it
member_data_providedtrueAlways set to true when sending the two fields below.
user_rolesArray of role name stringsinteraction.member.roles.cache.map(r => r.name)
is_boosterBoolean!!interaction.member.premiumSinceTimestamp

        
Only set member_data_provided: true if your bot actually has the Server Members privileged intent enabled and approved. The payout tier is locked at request time — you cannot claim premium payout at confirm time if you didn't send the data at request time.

User roles are passed through an AI classifier that infers demographic signals — gender, age range, device type, language, interests — based on role names (e.g. "He/Him", "Mobile", "PlayStation", "18+"). These signals improve ad targeting accuracy over time, which raises advertiser CPMs, which raises your earnings. The data is never sold and is only used for targeting within Cordvertise.

Privacy: Sending member_data_provided: true means you are transmitting per-user demographic inference data to Cordvertise. Before enabling this, ensure your server's privacy policy or terms of service cover this data sharing. If your users are in the EU/EEA, GDPR may apply. See our Privacy Policy for data retention and deletion policies.

Errors & denials

StatuserrorMeaning & action
400bad_requestMissing or invalid fields.
401auth_failedAPI key invalid.
403key_not_activeKey suspended.
403bad_signatureview_token tampered.
403token_identity_mismatchToken belongs to different key/bot.
429rate_limitedRequest rate exceeded. Back off and retry.
429captcha_lockedUser locked 10 min. Includes lockout_until_utc.
denied_reasonWhat to do
captcha_requiredMissing token or answer — restart from captcha/start.
captcha_failedWrong answer — let user retry (3 attempts before lockout).
captcha_expiredToken TTL elapsed (60s) — restart from captcha/start.
captcha_already_usedToken already consumed — restart from captcha/start.
user_minute_capUser saw an ad <1 min ago — skip. Includes retry_after_seconds.
user_daily_capUser hit 10 ads/day — skip until UTC midnight.
guild_minute_capServer hit 100 ads/min — skip. Includes retry_after_seconds.
partner_minute_capYour key hit its per-minute cap — back off. Includes retry_after_seconds.
no_adsNo ads available right now — skip silently.
reasonMeaning
not_sentdisplay_status was not "sent" — correct for failed sends.
missing_message_idForgot to pass message_id.
token_expiredConfirm faster — within expires_in_seconds.
token_usedToken already confirmed.
already_confirmedSame event_id already confirmed — idempotent, safe to ignore.
ad_unavailableAd paused between request and confirm.
advertiser_insufficient_creditsAdvertiser ran out of credits.

Rate limits & delivery caps

TierRequests / minRequirement
Probation100 × bot countDefault for new keys
Standard250 × bot count50+ clicks in 30 days
Pro500 × bot count250+ clicks in 30 days
Enterprise1000 × bot count750+ clicks in 30 days
Bot count is the number of distinct 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.
CapLimitScope
User per-minute1 ad / minPer (your key + user)
User per-day10 ads / UTC dayPer (your key + user)
Guild per-minute100 ads / minPer (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.

The idempotency key is (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.