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
- In your dashboard, open Conversions → Integrations → Developer API and tap Make a key.
- Give it a name, pick what it can do, and (optional) the IP addresses it may be used from and when it expires.
- Copy the key straight away. It looks like
jv_live_…and is shown only once. Joinvoo keeps only a scrambled (hashed) copy, so nobody, including us, can show it to you again. Lost it? Revoke it and make a new one.
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
| Permission | Endpoints |
|---|---|
| Stats, campaigns and channels | /me, /channels, /stats, /breakdown, /profit |
| People who joined | /joins (includes Telegram names and usernames) |
| Sign-ups and deposits | GET /conversions |
| Record sign-ups and deposits | POST /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
- 120 requests a minute and 20,000 a day per key. Writing conversions: 60 a minute per key.
- Every answer carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset(a Unix time). Over the limit you get429withRetry-Afterin seconds. - Lists return up to 200 rows at a time; use
offset(orsince_idfor conversions) to get the next page.
5. Dates, money and time zones
fromandtoare days like2026-11-01, both included. Without them you get the last 7 days.tzis your time zone as minutes behind UTC, the same as JavaScript’sgetTimezoneOffset(). Paris in winter is-60, London in winter is0, New York in winter is300.- Money is in cents (whole numbers):
revenue_cents: 4999is 49.99. Times are ISO 8601 in UTC. - Add
channel=IDto most endpoints for one channel.
6. Endpoints
/api/v1/meany keyYour account, this key’s permissions and your limits. Good for checking a key works.
/api/v1/channelsstatsYour 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" } ] }
/api/v1/statsstats · from, to, tz, channelTotals 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.
/api/v1/breakdownstats · dim, from, to, tz, channelThe 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 } ] }
/api/v1/profitstats · dim, from, to, tz, channelThe 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 } ] }
/api/v1/joinspeople · from, to, tz, channel, type, q, limit, offsetThe 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" } ] } ] }
/api/v1/conversionsconversions · from, to, event, channel, tracked, limit, offset, since_idSign-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" } ] }
/api/v1/conversionswriteRecord 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_id | Required. The person’s Telegram user ID. |
event | Required. reg, ftd (first deposit), dep (repeat deposit), sale, qualified, rejected (reverses a deposit) or payout (your commission). |
amount | The amount, like 49.99. Optional for sign-ups. |
currency | 3 letters. USD if you leave it out. |
txid | Your payment or event ID. Send it. The same txid twice is counted once, so retries are safe. |
commission | Optional. 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.
- Make a key with the Stats permission (for a client, pick only their channels).
- Open a Google Sheet → Extensions → Apps Script. Delete what’s there and paste this script. Tap Save.
- Reload the sheet. In the new Joinvoo menu choose Set API key and paste your key. Google asks you to allow the script once.
- 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" }
| Status | Codes |
|---|---|
400 | bad_date, bad_dim, bad_event, bad_amount, key_in_url … the message says what to fix |
401 | no_key, bad_key, key_revoked, key_expired, account_unavailable |
402 | plan: the account is no longer on Pro. Keys come back when it is. |
403 | missing_scope, ip_not_allowed |
404 / 405 | not_found, method_not_allowed |
429 | rate_limited, too_many_bad_keys (wait for Retry-After) |
503 | api_off: switched off for maintenance. Try again later. |
10. Keeping your key safe
- Treat a key like a password. Keep it on your server, in an environment variable, never in a web page, an app you hand out, or a public code repository.
- Give each tool its own key, with only the permissions it needs. Read-only is the default.
- Giving a key to someone else (a client, a freelancer)? Limit it to their channels.
- If your server has a fixed IP address, set it on the key. A stolen key then doesn’t work anywhere else.
- Each key shows when and from where it was last used, and your dashboard lists the last 50 requests. Something you don’t recognise? Revoke the key. It stops at once.
- We email you every time a key is made. Securing your account from a “Wasn’t you?” email also revokes every key.
- After 20 requests with a wrong key, that network is blocked for 15 minutes, so keys can’t be guessed.
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.