The Subreddit Audit API
Send a Reddit Ads report by subreddit, as JSON rows or raw CSV text, and get back a verdict for every subreddit: cut, scale, keep testing or no clicks, with a likely cost per conversion and a range. An audit costs 1 credit and a written action plan costs 10. The same key works on the website.
Quickstart
Get a key (see how to get a key), then audit some rows. The base URL for everything below is:
https://mpalkhuqlmfxypfifitr.supabase.co/functions/v1/api
curl
curl -X POST https://mpalkhuqlmfxypfifitr.supabase.co/functions/v1/api/v1/audit \
-H "Authorization: Bearer sa_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"target_cpa": 60,
"rows": [
{"subreddit": "r/SaaS", "spend": 1500.50, "clicks": 1000, "conversions": 5},
{"subreddit": "r/startups", "spend": 450, "clicks": 300, "conversions": 15},
{"subreddit": "r/webdev", "spend": 140, "clicks": 95, "conversions": 0}
]
}'
JavaScript (fetch)
const BASE = "https://mpalkhuqlmfxypfifitr.supabase.co/functions/v1/api";
const res = await fetch(`${BASE}/v1/audit`, {
method: "POST",
headers: { Authorization: "Bearer sa_live_YOUR_KEY", "Content-Type": "application/json" },
body: JSON.stringify({
target_cpa: 60,
rows: [
{ subreddit: "r/SaaS", spend: 1500.5, clicks: 1000, conversions: 5 },
{ subreddit: "r/startups", spend: 450, clicks: 300, conversions: 15 },
],
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
for (const s of data.subreddits) console.log(s.subreddit, s.verdict, s.likely_cpa);
Python (requests)
import requests
BASE = "https://mpalkhuqlmfxypfifitr.supabase.co/functions/v1/api"
HEADERS = {"Authorization": "Bearer sa_live_YOUR_KEY"}
# Rows, or send the raw export as {"csv": text} and let the columns be detected
with open("reddit-report.csv", encoding="utf-8") as f:
body = {"csv": f.read(), "target_cpa": 60}
r = requests.post(f"{BASE}/v1/audit", json=body, headers=HEADERS, timeout=60)
data = r.json()
if not r.ok:
raise SystemExit(f"{data['error']['code']}: {data['error']['message']}")
for s in data["subreddits"]:
print(s["subreddit"], s["verdict"], s["likely_cpa"])
print("credits left:", data["credits_left"])
Write a plan
/v1/plan takes the same body as /v1/audit, plus an optional product, and returns the audit and the plan together:
curl -X POST https://mpalkhuqlmfxypfifitr.supabase.co/functions/v1/api/v1/plan \
-H "Authorization: Bearer sa_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"csv": "Community,Amount Spent (USD),Clicks,Conversions\nr/SaaS,1500.50,1000,5\nr/startups,450,300,15", "product": "invoicing software for freelancers"}'
Check your balance
curl https://mpalkhuqlmfxypfifitr.supabase.co/functions/v1/api/v1/usage \ -H "Authorization: Bearer sa_live_YOUR_KEY"
Authentication
Send your key as a Bearer token on every request except openapi.json: Authorization: Bearer sa_live_…. Keys are sa_live_ followed by 32 lowercase hex characters. Requests and responses are JSON (Content-Type: application/json).
Treat the key like a password: it spends your credits. CORS allows any origin because keys are meant to be held by developers, which also means a key embedded in a public web page can be copied. Call the API from a server or script you control.
Endpoints
| Endpoint | Cost | What it does |
|---|---|---|
POST /v1/audit | 1 credit | Verdict, likely cost per conversion and range for every subreddit. |
POST /v1/plan | 10 credits | The audit, then an action plan and subreddits to test next. |
GET /v1/usage | Free | Your balance and the last 20 credit changes. |
GET /v1/openapi.json | Free, no key | The OpenAPI 3.1 document. |
POST /v1/audit
Send either rows or csv, not both.
Request fields
| Field | Type | Notes |
|---|---|---|
rows | array | 1 to 2,000 objects: subreddit (string, with or without r/), spend, clicks, conversions (numbers, zero or more). Repeated subreddits are added together. |
csv | string | The raw text of a Reddit Ads export by community. Comma, semicolon or tab separated. Columns are detected automatically. |
columns | object | With csv only. Header names to use instead of detection: {"subreddit": "Community", "spend": "Amount Spent (USD)", "clicks": "Clicks", "conversions": "Conversions"}. All four are required and must match a header exactly. |
target_cpa | number | Optional. Your target cost per conversion, above zero. Defaults to the account's overall cost per conversion. |
Requests are limited to 2,000 rows and 2 MB.
Response fields
| Field | Type | Notes |
|---|---|---|
target_cpa | number | The target used, yours or the account average. |
total_spend | number | Spend across all rows with spend. |
total_conversions | number | Conversions across those rows. |
warnings | string[] | Things to know, such as fewer than 20 conversions in total or conversions above clicks. |
subreddits | array | One entry per subreddit, highest spend first (fields below). |
credits_left | integer | Your balance after this call. |
| Subreddit field | Type | Notes |
|---|---|---|
subreddit | string | Always with the r/ prefix. |
spend, clicks, conversions | number | As sent. Conversions are capped at clicks. |
dashboard_cpa | number or null | Spend divided by conversions, as Reddit shows it. Null when there are no conversions. |
likely_cpa | number or null | The audit's middle estimate of the true cost per conversion. Null when there are no clicks. |
likely_cpa_low, likely_cpa_high | number or null | The 80% range for the true cost per conversion. The high end can be very large for subreddits with little data. |
prob_over_target | number or null | Chance, from 0 to 1, that the true cost per conversion is above the target. |
verdict | string | cut, scale, keep_testing or no_clicks. See the methodology. |
over_target | number | Estimated spend above what its conversions were worth at the target. Non-zero only for cut. |
An abridged response:
{
"target_cpa": 60,
"total_spend": 2916.5,
"total_conversions": 39,
"warnings": [],
"subreddits": [
{"subreddit": "r/SaaS", "spend": 1500.5, "clicks": 1000, "conversions": 5,
"dashboard_cpa": 300.1, "likely_cpa": 297.62, "likely_cpa_low": 177.15, "likely_cpa_high": 558.24,
"prob_over_target": 1, "verdict": "cut", "over_target": 1178.44},
{"subreddit": "r/startups", "spend": 450, "clicks": 300, "conversions": 15,
"dashboard_cpa": 30, "likely_cpa": 31.2, "likely_cpa_low": 22.95, "likely_cpa_high": 44.04,
"prob_over_target": 0.0103, "verdict": "scale", "over_target": 0}
],
"credits_left": 299
}
The request that produced this had two more subreddits, which are left out here. Numbers are rounded: money to cents and probabilities to four places.
POST /v1/plan
Takes everything /v1/audit does, plus product (optional string, up to 140 characters: what you sell). It runs the audit, then the plan writer, and costs 10 credits in total. The credits are only kept if both succeed.
| Response field | Type | Notes |
|---|---|---|
audit | object | The same as the /v1/audit response, without credits_left. |
plan.summary | string | A short read on the account. |
plan.actions | array | Objects with do and why: what to do this week. |
plan.try_next | array | Objects with subreddit and why: communities worth testing. These are ideas, not tested results. |
credits_left | integer | Your balance after this call. |
GET /v1/usage
| Response field | Type | Notes |
|---|---|---|
prefix | string | The first 12 characters of your key. |
credits | integer | Credits left. |
created_at | string | When the key was created (ISO 8601). |
last_used_at | string or null | When the key was last used. |
recent | array | The last 20 credit changes: delta (negative for spending), reason (purchase, plan, audit, refund or grant), ref and ts. |
Errors
Errors use the same shape every time: {"error": {"code": "no_credits", "message": "..."}}. Switch on code, not on the message.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | The body isn't valid: both or neither of rows and csv, a negative number, columns that can't be found, and similar. The message says which. |
| 401 | unauthorized | Missing, malformed, unknown or disabled key. |
| 402 | no_credits | Not enough credits for this call. Buy a pack. |
| 413 | too_large | More than 2,000 rows or more than 2 MB. |
| 429 | rate_limited | More than 60 billed calls in a minute for this key. Wait and retry; the response has a Retry-After header. |
| 500 | internal | Something failed on our side. Credits taken for the call are put back. |
A malformed request that fails before it is billed costs nothing. If an audit or plan fails after the credits were taken, they are refunded. Unknown paths return 404 not_found and wrong methods 405 method_not_allowed, in the same error shape.
Credits and pricing
| Pack | Price | Credits | That buys |
|---|---|---|---|
| Starter | $9 | 300 | 300 audits, or 30 plans |
| Pro | $29 | 1,200 | 1,200 audits, or 120 plans |
An audit is 1 credit and a plan is 10. Packs are one-time payments, not subscriptions. Pay by card or crypto. Current prices are on the pricing page.
A pack can be fully refunded within 14 days of purchase if none of its credits have been used; email heckitshim@gmail.com. Credits for a call that fails after they were taken are put back automatically.
Rate limits
60 billed calls per minute per key. Over that you get 429 rate_limited. Requests are limited to 2,000 rows and 2 MB each.
How to get a key
Buy a credit pack on the pricing page by card or crypto. When the payment clears you're shown a key starting sa_live_. Copy it then; there is no account or login. The same key works in the plan writer on the website and here. You can check its balance on the /v1/usage endpoint or on the account page.
OpenAPI
The machine-readable spec, for generating clients or importing into Postman or Insomnia: openapi.json. No key needed.
Try the audit first, free, in your browser.
Run a free audit