Developers

Developer API

Read your numbers from your own tools. Read-only by default.

Developer API

Version 1 · November 2026

The Joinvoo API lets your own tools read your numbers: clicks, joins, sign-ups, deposits and revenue, per day, per channel and per campaign, ad set and ad. Use it for your own dashboard, a spreadsheet, a CRM or a reporting tool for your clients. A key can also record sign-ups and deposits, for programs that can’t send a postback.

Want each event the moment it happens instead? Use webhooks (Conversions → Integrations → Send events to your own server). The API is for reading when you want; webhooks push to you.

1. Get a key

The API is part of Pro (and the free trial). Only the account owner can make keys; team members can’t. You can have up to 10 keys.

2. What a key can do

PermissionEndpoints
Stats, campaigns and channels/me, /channels, /stats, /breakdown, /profit
People who joined/joins (includes Telegram names and usernames)
Sign-ups and depositsGET /conversions
Record sign-ups and depositsPOST /conversions

Keys for some channels only. When you make a key you can pick which channels it sees. Perfect for an agency client or a partner: their key sees only their channels’ numbers, people and deposits, and can only record deposits for people who joined those channels.

A new key can only read stats unless you tick more. You can take a permission away from a key later, but not add one: for more power, make a new key. That way a key never quietly gains access.

3. Send the key with every request

All addresses start with https://joinvoo.com/api/v1. Put your key in the Authorization header:

curl https://joinvoo.com/api/v1/stats?from=2026-10-01&to=2026-10-31 \
  -H "Authorization: Bearer jv_live_YOUR_KEY"

X-Api-Key: jv_live_YOUR_KEY works too. A key in the address (?api_key=) is always refused, because addresses end up in logs and browser history.

Call the API from your server, not from a web page: browsers on other websites are not allowed to call it, so the key never sits in someone’s browser.

4. Limits

5. Dates, money and time zones

6. Endpoints

GET/api/v1/meany key

Your account, this key’s permissions and your limits. Good for checking a key works.

GET/api/v1/channelsstats

Your channels, groups, bots and DM links with their tracking links and which ad platforms are connected.

{ "channels": [ { "id": 12, "title": "Signal Room", "type": "channel", "status": "active",
    "tracking_url": "https://joinvoo.com/c/signal7", "ad_platforms": { "meta": true, "tiktok": false, "snapchat": false },
    "created_at": "2026-09-14T10:02:11.000Z" } ] }
GET/api/v1/statsstats · from, to, tz, channel

Totals for the dates, each day, and each channel.

{ "from": "2026-10-01", "to": "2026-10-31", "tz": -60,
  "totals": { "clicks": 18240, "joins": 6120, "organic_joins": 410, "leaves": 380, "filtered_joins": 52,
    "registrations": 940, "first_deposits": 212, "repeat_deposits": 301, "sales": 0,
    "revenue_cents": 2864000, "spend_cents": 1450000, "meta_sent": 6098, "meta_failed": 3 },
  "not_tracked": { "registrations": 14, "first_deposits": 3, "repeat_deposits": 1, "sales": 0, "revenue_cents": 21000 },
  "days": [ { "day": "2026-10-01", "clicks": 612, "joins": 198, "first_deposits": 7, "revenue_cents": 91000 } ],
  "channels": [ { "id": 12, "title": "Signal Room", "clicks": 18240, "joins": 6120, "organic_joins": 410, "leaves": 380 } ] }

not_tracked counts sign-ups and deposits that reached your account but couldn’t be tied to someone who joined from a Joinvoo link (for example a link without the Telegram ID). They are kept out of the ad numbers.

GET/api/v1/breakdownstats · dim, from, to, tz, channel

The same numbers split by dim: campaign (default), adset, ad, source, platform, country, lang or channel. For campaign and platform, rows include your ad spend, cost per join, cost per first deposit and ROAS.

{ "dim": "campaign", "rows": [ { "key": "Spring Promo", "clicks": 5120, "joins": 1730, "join_rate": 0.3379,
    "registrations": 260, "first_deposits": 61, "revenue_cents": 812000, "spend_cents": 410000,
    "cost_per_join_cents": 237, "cost_per_first_deposit_cents": 6721, "roas": 1.98 } ] }
GET/api/v1/profitstats · dim, from, to, tz, channel

The same numbers as the Profit page: ad spend, what you earned (your payouts, or your real commission), profit, ROI and cost per first deposit, per campaign (default), adset or ad, with each day and the previous period of the same length.

{ "dim": "campaign", "from": "2026-10-01", "to": "2026-10-31", "payouts_set": true,
  "totals": { "joins": 6120, "first_deposits": 212, "spend_cents": 1450000, "earned_cents": 2120000, "profit_cents": 670000, "roi": 0.4621 },
  "rows": [ { "key": "Spring Promo", "platform": "Meta", "first_deposits": 61, "spend_cents": 410000, "earned_cents": 600000, "profit_cents": 190000, "roi": 0.4634 } ] }
GET/api/v1/joinspeople · from, to, tz, channel, type, q, limit, offset

The people who joined, newest first, with the ad they came from and their sign-ups and deposits. type: tracked (from an ad), organic, left, ftd or filtered. q searches names, usernames and Telegram IDs. Visitors’ IP addresses, devices and ad cookies are never included.

{ "total": 6120, "limit": 50, "offset": 0, "next_offset": 50, "joins": [ {
    "id": 88121, "channel_id": 12, "channel": "Signal Room", "tg_user_id": 6510981724, "username": "ada_fx", "first_name": "Ada",
    "language": "en", "premium": false, "joined_at": "2026-10-31T19:40:02.000Z", "left_at": null, "from_ad": true, "country": "BR",
    "ad": { "platform": "meta", "campaign": "Spring Promo", "adset": "Women 25-34", "ad": "Video 1", "source": "facebook", "clicked_at": "2026-10-31T19:39:21.000Z" },
    "conversions": [ { "event": "ftd", "value_cents": 4999, "currency": "USD", "at": "2026-10-31T21:12:40.000Z" } ] } ] }
GET/api/v1/conversionsconversions · from, to, event, channel, tracked, limit, offset, since_id

Sign-ups, deposits and sales, newest first, with the ad each came from. No names. event: reg, ftd, dep, sale, qualified, rejected, payout, install or lead. tracked=1 for only the ones matched to a person from your links.

Polling for new ones: send since_id= the last id you saw. You get everything newer, oldest first, and last_id to send next time. Start with since_id=0.

{ "since_id": 5120, "last_id": 5122, "conversions": [ { "id": 5121, "event": "ftd", "value_cents": 4999, "commission_cents": null,
    "currency": "USD", "txid": "pay-1001", "source": "postback", "program": "affstore", "tracked": true, "reversed": false,
    "tg_user_id": 6510981724, "join_id": 88121, "channel_id": 12, "sent": { "meta": "sent", "tiktok": "none", "snapchat": "none" },
    "ad": { "platform": "meta", "campaign": "Spring Promo", "adset": "Women 25-34", "ad": "Video 1" }, "created_at": "2026-10-31T21:12:40.000Z" } ] }
POST/api/v1/conversionswrite

Record a sign-up or deposit for a Telegram user, for programs that can’t send a postback. Joinvoo matches it to the person and their ad, and sends it to Meta, TikTok, Snapchat and Google exactly like a postback.

curl -X POST https://joinvoo.com/api/v1/conversions \
  -H "Authorization: Bearer jv_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"tg_user_id": 6510981724, "event": "ftd", "amount": 49.99, "currency": "USD", "txid": "pay-1001"}'
Field
tg_user_idRequired. The person’s Telegram user ID.
eventRequired. reg, ftd (first deposit), dep (repeat deposit), sale, qualified, rejected (reverses a deposit) or payout (your commission).
amountThe amount, like 49.99. Optional for sign-ups.
currency3 letters. USD if you leave it out.
txidYour payment or event ID. Send it. The same txid twice is counted once, so retries are safe.
commissionOptional. What you earned, for your Profit report.

Answers 201 when it’s new and 200 with "duplicate": true when it was already recorded. "tracked": false means nobody with that Telegram ID joined through your Joinvoo links; it is saved but kept out of your ad numbers.

7. Google Sheets: a report that updates itself

No code needed. Your numbers land in a Google Sheet and refresh every hour: a daily sheet, a campaigns sheet (with spend, cost per first deposit and ROAS) and a totals sheet. Share it with a client or your team like any sheet.

  1. Make a key with the Stats permission (for a client, pick only their channels).
  2. Open a Google Sheet → Extensions → Apps Script. Delete what’s there and paste this script. Tap Save.
  3. Reload the sheet. In the new Joinvoo menu choose Set API key and paste your key. Google asks you to allow the script once.
  4. Choose Joinvoo → Refresh every hour. Done.

Your key is saved in your own Google account’s script settings, never in a cell, so sharing the sheet doesn’t share the key.

8. Zapier and Make

Zapier: the Joinvoo app has two triggers, New Join and New Deposit or Sign-up (each with the ad it came from), and one action, Record a Deposit or Sign-up. Connect it with an API key. Send every new depositor to your CRM, a Google Sheet, Slack or your own bot.

Make: use the HTTP → Make a request module with your key in the header: GET https://joinvoo.com/api/v1/conversions?since_id={{last id}}, header Authorization: Bearer jv_live_…, “Parse response” on.

Want events pushed to you the moment they happen instead? Use webhooks (Conversions → Integrations → Send events to your own server).

9. Errors

Errors are JSON with a plain-English error and a code your program can check:

{ "error": "This key can’t do that. It needs the “People who joined” permission.", "code": "missing_scope" }
StatusCodes
400bad_date, bad_dim, bad_event, bad_amount, key_in_url … the message says what to fix
401no_key, bad_key, key_revoked, key_expired, account_unavailable
402plan: the account is no longer on Pro. Keys come back when it is.
403missing_scope, ip_not_allowed
404 / 405not_found, method_not_allowed
429rate_limited, too_many_bad_keys (wait for Retry-After)
503api_off: switched off for maintenance. Try again later.

10. Keeping your key safe

11. Examples

JavaScript (Node 18+): yesterday’s first deposits per campaign.

const KEY = process.env.JOINVOO_KEY;
const day = new Date(Date.now() - 864e5).toISOString().slice(0, 10);
const r = await fetch(`https://joinvoo.com/api/v1/breakdown?dim=campaign&from=${day}&to=${day}&tz=-60`, {
  headers: { Authorization: `Bearer ${KEY}` } });
if (!r.ok) throw new Error((await r.json()).error);
for (const row of (await r.json()).rows) console.log(row.key, row.first_deposits, row.revenue_cents / 100);

Python: new deposits since last time.

import os, requests
last_id = 0  # keep this between runs
r = requests.get("https://joinvoo.com/api/v1/conversions",
                 params={"since_id": last_id, "event": "ftd"},
                 headers={"Authorization": "Bearer " + os.environ["JOINVOO_KEY"]}, timeout=20)
r.raise_for_status()
data = r.json()
for c in data["conversions"]:
    print(c["id"], c["value_cents"] / 100, c["currency"], (c["ad"] or {}).get("campaign"))
last_id = data["last_id"]

12. Questions

Use the chat in your dashboard. Tell us what you’re building and we’ll help.