gumroad-cli: Gumroad API v2 CLI (products/sales/followers/offer-codes/buyers-csv)
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# gumroad-cli
|
||||
|
||||
CLI for the Gumroad API v2 (Python 3, stdlib only — no pip deps).
|
||||
|
||||
## Setup
|
||||
```bash
|
||||
export GUMROAD_TOKEN=<token> # or --token flag
|
||||
```
|
||||
|
||||
## Commands
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `products` | List all products (id, name) |
|
||||
| `product <id>` | Product detail (price, description, custom fields) |
|
||||
| `sales` | List sales (email, product, amount, date) |
|
||||
| `followers` | List follower emails — **404: Gumroad API has no followers endpoint** (dashboard UI only) |
|
||||
| `offer-codes` | List offer codes per product |
|
||||
| `buyers-csv` | Export unique buyer emails to CSV (for Listmonk sync) |
|
||||
|
||||
## Notes
|
||||
- Gumroad store `alfalahtech.gumroad.com` is the **archived** store; active store = Polar on fi2.falahos.my/products.
|
||||
- No API path exists to bulk-add followers/subscribers → Listmonk stays the source of truth for blasts.
|
||||
- Buyer sync flow: `gumroad-cli buyers-csv` → import CSV into Listmonk.
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gumroad-cli — interface with the Gumroad API v2 (stdlib only).
|
||||
|
||||
Commands:
|
||||
products list all products (id, name, price, url)
|
||||
product <id> 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()
|
||||
Reference in New Issue
Block a user