#!/usr/bin/env python3 """gumroad-cli — interface with the Gumroad API v2 (stdlib only). Commands: products list all products (id, name, price, url) product detail for one product sales list recent sales (email, product, amount, date) followers list follower emails (GET /v2/followers — 404 on current API) offer-codes list offer codes per product buyers-csv export all unique buyer emails to CSV Token: env GUMROAD_TOKEN or --token flag. """ import argparse, csv, json, os, sys, time, urllib.parse, urllib.request API = "https://api.gumroad.com/v2" def api_get(path, params, token): params["access_token"] = token url = API + path + "?" + urllib.parse.urlencode(params) try: with urllib.request.urlopen(url, timeout=30) as r: return json.loads(r.read().decode()) except urllib.error.HTTPError as e: return {"http_error": e.code, "body": e.read().decode()[:500]} def api_post(path, params, token): params["access_token"] = token data = urllib.parse.urlencode(params).encode() req = urllib.request.Request(API + path, data=data, method="POST") try: with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read().decode()) except urllib.error.HTTPError as e: return {"http_error": e.code, "body": e.read().decode()[:500]} def money(cents): return f"${cents/100:.2f}" if cents is not None else "-" def cmd_products(d, t): r = api_get("/products", {}, t) for p in r.get("products", []): print(f"{p.get('id')}\t{money(p.get('price_cents'))}\t{p.get('name')}\t{p.get('url')}") def cmd_product(d, t): r = api_get(f"/products/{d.id}", {}, t) p = r.get("product", {}) if not p: print(json.dumps(r, indent=2)[:500]); return print(f"ID: {p.get('id')}") print(f"Name: {p.get('name')}") print(f"Price: {money(p.get('price_cents'))}") print(f"URL: {p.get('url')}") print(f"Desc: {(p.get('description') or '')[:400]}") print(f"Sales: {p.get('sales_count')}") print(f"Custom fields: {[f.get('name') for f in p.get('custom_fields') or []]}") def cmd_sales(d, t): r = api_get("/sales", {}, t) for s in r.get("sales", []): print(f"{s.get('created_at','')}\t{s.get('email','')}\t{s.get('product_name','')}\t{money(s.get('amount_cents'))}") def cmd_followers(d, t): r = api_get("/followers", {}, t) if "http_error" in r: print(f"ERR {r['http_error']}: {r['body'][:200]}") print("(Gumroad API v2 has no followers endpoint — followers exist only in the dashboard UI)") return for f in r.get("followers", []): print(f.get("email", "")) def cmd_offer_codes(d, t): r = api_get("/products", {}, t) for p in r.get("products", []): pid = p.get("id") r2 = api_get(f"/products/{pid}/offer_codes", {}, t) codes = r2.get("offer_codes", []) if codes: print(f"== {p.get('name')} ({pid})") for c in codes: amount = money(c.get("amount_cents")) if c.get("amount_cents") else f"{c.get('percent_off')}%" print(f" {c.get('code')}\t{amount}\t{c.get('name')}") def cmd_buyers_csv(d, t): emails = {} page, total = 1, None while True: r = api_get("/sales", {"page": page, "per_page": 100}, t) sales = r.get("sales", []) for s in sales: if s.get("email"): emails[s["email"].strip().lower()] = True total = r.get("total") if not sales or (total is not None and page * 100 >= total) or page > 50: break page += 1 out = d.out or "gumroad_buyers.csv" with open(out, "w", newline="") as f: w = csv.writer(f) w.writerow(["email"]) for e in sorted(emails): w.writerow([e]) print(f"Wrote {len(emails)} unique buyer emails -> {out}") def main(): ap = argparse.ArgumentParser(prog="gumroad-cli", description="Gumroad API v2 CLI") ap.add_argument("--token", default=os.environ.get("GUMROAD_TOKEN"), help="API token (env GUMROAD_TOKEN)") ap.add_argument("--out", default=None, help="output file for buyers-csv") sub = ap.add_subparsers(dest="cmd", required=True) sub.add_parser("products") sp = sub.add_parser("product"); sp.add_argument("id") sub.add_parser("sales") sub.add_parser("followers") sub.add_parser("offer-codes") bp = sub.add_parser("buyers-csv"); bp.add_argument("--out", default=None) d = ap.parse_args() t = d.token if not t: sys.exit("ERROR: no token. Set GUMROAD_TOKEN env or pass --token.") {"products": cmd_products, "product": cmd_product, "sales": cmd_sales, "followers": cmd_followers, "offer-codes": cmd_offer_codes, "buyers-csv": cmd_buyers_csv}[d.cmd](d, t) if __name__ == "__main__": main()