Publisher API

Add Cordvertise ad delivery to your existing Discord bot and earn on every confirmed view, click, join, and stay. Two endpoints, plain JSON, no SDK.

Every request is authenticated via an Authorization: Bearer <api_key> header. Your api_key comes from the dashboard. Your external_bot_id is 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 — and call our two endpoints to deliver an ad.

Your bot requests an ad, your bot sends the ad embed, your bot confirms delivery. Our API handles everything else: ad selection, billing, targeting, and payout. Your existing commands and features stay 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

All requests must include an Authorization header with your API key as a Bearer token. Your external_bot_id goes in the request body.

Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
FieldWhereDescription
AuthorizationHeaderBearer <api_key>. Your API key from the dashboard. Never expose this client-side.
external_bot_idBodyYour bot's Discord application ID — a 17–20 digit snowflake from the Discord Developer Portal.
Missing or invalid Authorization header returns HTTP 401. Suspended key returns 403 with "error":"key_not_active".

The 2-step flow

1. ads/requestGet ad + view token
Send embedDiscord message sent
2. 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

  • Embed v1
  • Embed v2
  • 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 two 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.

Ephemeral vs public: All snippets default to ephemeral: true so only the requesting user sees the ad. You can remove the ephemeral flag to show ads publicly in a channel — both work. Billing is the same either way.
DMs: guild is null inside DMs. All snippets require a guild context — block your ad command in DMs or the API call will fail. Check that interaction.guildId (or equivalent) is not null before calling cvServeAd.

        
POST/v1/ads/request

Returns an ad and a signed view token. Always check if ad is null before sending anything to Discord.

HeaderValue
AuthorizationrequiredBearer YOUR_API_KEY
Content-Typerequiredapplication/json
FieldDescription
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 member viewing the ad. Used for targeting, deduplication, and fraud checks — must be accurate.
guild_idrequiredDiscord guild ID. Required for per-guild rate limiting and join/stay campaign tracking.
guild_namerequiredServer name from guild.name. Used for AI-based ad targeting classification.
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 none.
member_data_providedoptionalSet true if you are also sending user_roles and is_booster. Unlocks 45% payout tier. Requires the GuildMembers privileged intent.
user_rolesoptionalArray of role names the requesting user has. 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.
{
  "ok": true,
  "ad": {
    "ad_id": "uuid",
    "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" | "joins" | "stays"
    "join_url": "https://.../j/TOKEN"     // only present for joins/stays campaigns
  },
  "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. For join/stay campaigns use join_url instead.
{
  "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.

House ads: Sometimes ad is present but view_token is null. This means Cordvertise served one of its own promotional ads (house ad). Send the embed normally — it displays fine — but do not call views/confirm. There is nothing to confirm and no token to submit. Check for view_token !== null before confirming.
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.

HeaderValue
AuthorizationrequiredBearer YOUR_API_KEY
Content-Typerequiredapplication/json
FieldDescription
external_bot_idrequiredYour bot's application ID.
event_idrequiredSame value used in ads/request.
view_tokenrequiredJWT from ads/request. Single-use, 120s TTL.
display_statusrequired"sent" if message delivered successfully. Anything else = not billed.
guild_idrequiredDiscord guild ID. Same server the ad was delivered in.
user_idrequiredDiscord user ID of the member who received the ad. Used for fraud checks.
message_idrequired*Discord message ID returned after sending. Required when display_status is "sent".
// Billed
{ "ok": true, "billed": true, "ad_id": "uuid", "auto_paused": false, "daily_cap_reached": false }

// Not billed
{ "ok": true, "billed": false, "reason": "token_expired" }

auto_paused: true means the advertiser's balance hit zero on this confirm — the ad has been automatically paused. Informational only, no action needed from your bot. daily_cap_reached: true means the advertiser's daily spend limit was hit — same, informational only.

Campaign types

The ad.target_type field in the ads/request response tells you what kind of campaign was served. Your core integration is the same for all types — request, send embed, confirm. 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 the tracking redirect. Billing happens when the user clicks.
joinsUser joins the advertiser's serverUse join_url as the button URL. Join confirmation is handled by our backend — 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.

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 only differs in which URL you use as the button — everything else is the same.

1. ads/requestGet ad + join_url
Send embedButton = join_url
2. views/confirmRecord delivery
User clicksjoin_url redirect
User joins serverPayout automatic
For join/stay campaigns, ad.join_url is the tracking redirect (/j/TOKEN). Use it as your Discord button URL instead of click_url. When the user clicks it, we record their Discord ID 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%.

  • JavaScript (discord.js)
  • TypeScript (discord.js)
  • Python (discord.py)
  • Java (JDA)
  • C# (Discord.Net)
  • Go (discordgo)
  • Rust (serenity)
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. 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 and your earnings. Inference only runs once per user per server. 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 covers 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.
401missing_auth / invalid_keyAuthorization header missing or API key invalid.
403key_not_activeKey suspended.
403bad_signatureview_token tampered.
403token_identity_mismatchToken belongs to different key/bot.
429rate_limitedRequest rate exceeded. Check the Rate limits section for your tier's limit. Back off and retry after retry_after_seconds if present.
denied_reasonWhat to do
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.
ads_disabledAd serving disabled for your key — contact support.
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_expiredToken TTL is 120 seconds from the ads/request response. If Discord's API was slow and the token expired before you could confirm, pass display_status: "failed" — no billing occurs and the event is closed cleanly.
token_usedToken already confirmed.
already_confirmedSame event_id already confirmed — idempotent, safe to ignore.
ad_unavailableAd paused between request and confirm.
account_too_newUser's Discord account is less than 30 days old — not billed.
advertiser_insufficient_fundsAdvertiser ran out of credits between request and confirm.

Rate limits & delivery caps

TierRequests / minRequirement
Probation100 × bot countDefault for new keys
Standard250 × bot count50+ confirmed clicks in 30 days
Pro500 × bot count250+ confirmed clicks in 30 days
Enterprise1000 × bot count750+ confirmed clicks in 30 days
Bot count is the number of distinct external_bot_id values seen under your API key in the past 24 hours. One key, one bot = multiplier of 1. Multiple bots under one key scale the limit 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.