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
| Field | Where | Description |
|---|---|---|
| Authorization | Header | Bearer <api_key>. Your API key from the dashboard. Never expose this client-side. |
| external_bot_id | Body | Your bot's Discord application ID — a 17–20 digit snowflake from the Discord Developer Portal. |
401. Suspended key returns 403 with "error":"key_not_active".
The 2-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
- 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: 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.
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.
Returns an ad and a signed view token. Always check if ad is null before sending anything to Discord.
| Header | Value | |
|---|---|---|
| Authorization | required | Bearer YOUR_API_KEY |
| Content-Type | required | application/json |
| Field | Description | |
|---|---|---|
| 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 member viewing the ad. Used for targeting, deduplication, and fraud checks — must be accurate. |
| guild_id | required | Discord guild ID. Required for per-guild rate limiting and join/stay campaign tracking. |
| guild_name | required | Server name from guild.name. Used for AI-based ad targeting classification. |
| 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 none. |
| member_data_provided | optional | Set true if you are also sending user_roles and is_booster. Unlocks 45% payout tier. Requires the GuildMembers privileged intent. |
| user_roles | optional | Array of role names the requesting user has. 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. |
{
"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
}
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.
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.
Records the delivery and triggers billing. Call this after the Discord message is sent. This is when your earnings are recorded.
Request headers| Header | Value | |
|---|---|---|
| Authorization | required | Bearer YOUR_API_KEY |
| Content-Type | required | application/json |
| Field | Description | |
|---|---|---|
| external_bot_id | required | Your bot's application ID. |
| event_id | required | Same value used in ads/request. |
| view_token | required | JWT from ads/request. Single-use, 120s TTL. |
| display_status | required | "sent" if message delivered successfully. Anything else = not billed. |
| guild_id | required | Discord guild ID. Same server the ad was delivered in. |
| user_id | required | Discord user ID of the member who received the ad. Used for fraud checks. |
| message_id | required* | 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_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 the tracking redirect. Billing happens when the user clicks. |
| joins | User joins the advertiser's server | Use join_url as the button URL. Join confirmation is handled by our backend — 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. |
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.
How it worksad.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.
Errors & denials
HTTP errors| Status | error | Meaning & action |
|---|---|---|
| 400 | bad_request | Missing or invalid fields. |
| 401 | missing_auth / invalid_key | Authorization header missing or 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. Check the Rate limits section for your tier's limit. Back off and retry after retry_after_seconds if present. |
denied_reason)
| denied_reason | What to do |
|---|---|
| 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. |
| ads_disabled | Ad serving disabled for your key — contact support. |
| 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 | Token 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_used | Token already confirmed. |
| already_confirmed | Same event_id already confirmed — idempotent, safe to ignore. |
| ad_unavailable | Ad paused between request and confirm. |
| account_too_new | User's Discord account is less than 30 days old — not billed. |
| advertiser_insufficient_funds | Advertiser ran out of credits between request and confirm. |
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+ confirmed clicks in 30 days |
| Pro | 500 × bot count | 250+ confirmed clicks in 30 days |
| Enterprise | 1000 × bot count | 750+ confirmed clicks in 30 days |
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.
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.