DEV Community

孫昊
孫昊

Posted on

Gumroad SKU Creation via CDP: From Markdown to LIVE in 90 Seconds

TL;DR: Gumroad has a partial REST API but it can't create products end-to-end (no cover image, no description rich text, no pricing tier UI). CDP automation closes the gap. 6 SKUs created via CDP in 60 days, average 90 seconds per SKU from "I have a product idea" to "buyable URL".


What Gumroad's API does and doesn't do

What works via API

import requests
r = requests.put(
    "https://api.gumroad.com/v2/products/{id}",
    data={"access_token": TOKEN, "price": 1900, "name": "Updated"},
)
Enter fullscreen mode Exit fullscreen mode
  • ✓ Update existing product attributes (price, name, custom fields)
  • ✓ Get product list, sales list
  • ✓ Update tag list
  • ✓ Update affiliate program

What doesn't work via API

  • ✗ Create a NEW product (no /v2/products POST endpoint that exposes everything)
  • ✗ Upload cover image (must use UI)
  • ✗ Set rich-text description (markdown editor in browser only)
  • ✗ Add downloadable file
  • ✗ Configure thank-you-page redirects to specific URL
  • ✗ Set discount codes per product

So for full SKU creation, CDP is required.

The CDP flow (90 sec end-to-end)

from playwright.sync_api import sync_playwright

SELECTORS = {
    'create_product': 'a[href="/products/new"]',
    'product_type': 'div:has-text("Digital product"):visible',
    'product_name': 'input[name="name"]',
    'price': 'input[name="price"]',
    'description_editor': 'div.ProseMirror',
    'cover_upload': 'input[type="file"][accept*="image"]',
    'save_button': 'button:has-text("Save and continue")',
    'publish_button': 'button:has-text("Publish")',
}

def create_sku(name, price_usd, description_md, cover_path, file_path):
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp("http://127.0.0.1:9222")
        page = browser.contexts[0].new_page()

        page.goto("https://gumroad.com/products")
        page.click(SELECTORS['create_product'])
        page.click(SELECTORS['product_type'])
        page.fill(SELECTORS['product_name'], name)
        page.fill(SELECTORS['price'], str(price_usd))
        page.click(SELECTORS['description_editor'])

        # Set description via clipboard
        import pyperclip
        pyperclip.copy(description_md)
        page.keyboard.press("Control+V")

        # Upload cover
        page.set_input_files(SELECTORS['cover_upload'], cover_path)

        # Upload file
        upload_input = page.locator('input[type="file"]:not([accept*="image"])')
        upload_input.set_input_files(file_path)

        page.wait_for_selector('text=Uploaded', timeout=60_000)
        page.click(SELECTORS['save_button'])
        page.wait_for_url(re.compile(r'.*/products/[a-z0-9]+/edit'))
        page.click(SELECTORS['publish_button'])

        return page.url
Enter fullscreen mode Exit fullscreen mode

Real numbers from 6 SKUs

SKU Price Build time LIVE URL slug
ASC API Toolkit $499 90 sec vszsui
AutoApp Dashboard $39 80 sec hmmzt
Indie Hacker Bundle $25 95 sec emvfkj
iOS Indie Launch Playbook $19 100 sec gncbck
30-Day B2B Cold Email $15 85 sec jdmmy
14 iOS Rejection Reasons Free 70 sec sphytu

Total catalog value: $597 in ~9 minutes of CDP runtime.

What I learned from 6 SKUs

1. Cover image is the highest-leverage field

Spend 2 min designing a great cover. It dominates conversion. CDP sets it programmatically, so I generate covers in batch via Canva API + import.

2. Tags need to be set AFTER product creation

The Gumroad new-product flow doesn't expose tags. After creation, navigate to /products/{id}/edit and set tags via API:

requests.put(f"https://api.gumroad.com/v2/products/{id}",
             data={"access_token": TOKEN, "tags": "ios,indie,toolkit"})
Enter fullscreen mode Exit fullscreen mode

3. File upload is async — wait for "Uploaded" text

The wait condition page.wait_for_selector('text=Uploaded', timeout=60_000) is non-negotiable. Without it, the save button submits before the file binds.

4. ProseMirror description doesn't take markdown directly

Same as Substack TipTap — paste markdown gets shown as plain text. Either:

  • Convert to HTML first
  • Or paste markdown then manually format (defeats automation)

5. After publish, set affiliate program via API

requests.put(f"https://api.gumroad.com/v2/products/{id}",
             data={"access_token": TOKEN,
                   "affiliate_basis_points": 3000,  # 30% commission
                   "shown_on_profile": True})
Enter fullscreen mode Exit fullscreen mode

Source

Full Gumroad CDP automation + 30% affiliate setup:

AutoApp Dashboard ($39) includes:

  • gumroad_create_sku.py (CDP creator, this article)
  • gumroad_setup_affiliates.py (batch 30% setup)
  • gumroad_uploader.py (with retry + cover upload)
  • 4 cover image templates (Canva PSD)

If you're an indie hacker and you've been hand-creating Gumroad products, automate it. 90 sec per SKU adds up across a catalog.

Top comments (0)