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

EndpointCostWhat it does
POST /v1/audit1 creditVerdict, likely cost per conversion and range for every subreddit.
POST /v1/plan10 creditsThe audit, then an action plan and subreddits to test next.
GET /v1/usageFreeYour balance and the last 20 credit changes.
GET /v1/openapi.jsonFree, no keyThe OpenAPI 3.1 document.

POST /v1/audit

Send either rows or csv, not both.

Request fields

FieldTypeNotes
rowsarray1 to 2,000 objects: subreddit (string, with or without r/), spend, clicks, conversions (numbers, zero or more). Repeated subreddits are added together.
csvstringThe raw text of a Reddit Ads export by community. Comma, semicolon or tab separated. Columns are detected automatically.
columnsobjectWith 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_cpanumberOptional. 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

FieldTypeNotes
target_cpanumberThe target used, yours or the account average.
total_spendnumberSpend across all rows with spend.
total_conversionsnumberConversions across those rows.
warningsstring[]Things to know, such as fewer than 20 conversions in total or conversions above clicks.
subredditsarrayOne entry per subreddit, highest spend first (fields below).
credits_leftintegerYour balance after this call.
Subreddit fieldTypeNotes
subredditstringAlways with the r/ prefix.
spend, clicks, conversionsnumberAs sent. Conversions are capped at clicks.
dashboard_cpanumber or nullSpend divided by conversions, as Reddit shows it. Null when there are no conversions.
likely_cpanumber or nullThe audit's middle estimate of the true cost per conversion. Null when there are no clicks.
likely_cpa_low, likely_cpa_highnumber or nullThe 80% range for the true cost per conversion. The high end can be very large for subreddits with little data.
prob_over_targetnumber or nullChance, from 0 to 1, that the true cost per conversion is above the target.
verdictstringcut, scale, keep_testing or no_clicks. See the methodology.
over_targetnumberEstimated 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 fieldTypeNotes
auditobjectThe same as the /v1/audit response, without credits_left.
plan.summarystringA short read on the account.
plan.actionsarrayObjects with do and why: what to do this week.
plan.try_nextarrayObjects with subreddit and why: communities worth testing. These are ideas, not tested results.
credits_leftintegerYour balance after this call.

GET /v1/usage

Response fieldTypeNotes
prefixstringThe first 12 characters of your key.
creditsintegerCredits left.
created_atstringWhen the key was created (ISO 8601).
last_used_atstring or nullWhen the key was last used.
recentarrayThe 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.

StatusCodeMeaning
400bad_requestThe 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.
401unauthorizedMissing, malformed, unknown or disabled key.
402no_creditsNot enough credits for this call. Buy a pack.
413too_largeMore than 2,000 rows or more than 2 MB.
429rate_limitedMore than 60 billed calls in a minute for this key. Wait and retry; the response has a Retry-After header.
500internalSomething 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

PackPriceCreditsThat buys
Starter$9300300 audits, or 30 plans
Pro$291,2001,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