cd /news/developer-tools/gumroad-s-auth-flow-is-hostile-to-au… · home topics developer-tools article
[ARTICLE · art-119369] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Gumroad's auth flow is hostile to automation. Here's the exact chain that works.

A developer detailed the exact browser-automation chain required to create Gumroad products, including password resets, email-based 2FA, and React form handling. The developer shared workarounds for Gumroad's lack of a public API, such as using native value setters and parsing the 2FA token from the email subject line. The post highlights the fragility of automating React-based forms and the need for persistent browser profiles.

read5 min views1 publishedSep 2, 2026

I needed to automate Gumroad product creation — log in, create a product, set the price, upload a file, write the description, publish. Gumroad has no public API for this. The only option is browser automation. Here's what I had to get right, in order, and where each step breaks if you're not careful.

The account had a password I didn't know. I triggered a reset from the login page. Gumroad sends a reset email with a link. The link expires fast — I don't know the exact TTL, but it was under 30 minutes.

I used the AgentMail MCP to read the email and extract the reset link. The link is a Gumroad URL with a token parameter. Navigating to it shows a password reset form.

Where it breaks: If you try to fill the new password field with input.value = 'newpassword'

, React won't register the change. The form will submit with an empty password. You need to use the native value setter:

const nativeSetter = Object.getOwnPropertyDescriptor(
  HTMLInputElement.prototype, 'value'
).set;
nativeSetter.call(passwordInput, newPassword);
passwordInput.dispatchEvent(new Event('input', { bubbles: true }));

This is because React overrides the value

property on inputs with its own setter that tracks changes via a value tracker. The native setter bypasses React's tracker, and the input

event tells React to sync state.

After password reset, logging in triggers 2FA. Gumroad's 2FA is email-based — not TOTP. There's no authenticator app. They email you a 6-digit token.

The token appears in the email subject line: "Your authentication token is 126874"

. This is convenient — you don't need to parse the email body. Just grab the subject, regex out the digits.

const subject = "Your authentication token is 126874";
const token = subject.match(/token is (\d+)/)?.[1]; // "126874"

Where it breaks: The email takes 5-15 seconds to arrive. If you check the inbox immediately after submitting the login form, you'll get the previous email (or nothing). Wait at least 10 seconds before polling.

After 2FA, you're logged in. The session cookie is set. As long as you don't close the Chrome instance or clear cookies, you stay logged in across navigations.

I'm using a dedicated Chrome profile (--user-data-dir

) so the session persists across script runs. Without this, every script execution would require a fresh login + 2FA cycle.

Navigate to https://gumroad.com/products/new

. The form has a text input for the product name and a price field. Fill both, click "Next: Customize".

Where it breaks: The price input is type="number"

. The native setter trick works, but you need to pass a string, not a number. nativeSetter.call(priceInput, '15')

works. nativeSetter.call(priceInput, 15)

may not trigger the change event correctly.

After clicking "Next", you're redirected to https://gumroad.com/products/{id}/edit

. This page has:

The description editor is a contenteditable

div. You can set its innerHTML

directly — Gumroad's editor reads from the DOM, not from React state:

const editor = document.querySelector('[contenteditable="true"]');
editor.innerHTML = '<p>Your description here</p>';
editor.dispatchEvent(new Event('input', { bubbles: true }));

Where it breaks: If you set innerHTML

before the editor is fully initialized (which happens after a brief state), the content will be overwritten. Wait for the editor to be visible and interactive before setting content.

Navigate to https://gumroad.com/products/{id}/edit/content

. The page has a hidden <input type="file" class="sr-only">

.

I covered the upload mechanism in detail in my previous article. The short version: encode the file as base64, embed it in a Runtime.evaluate

script, decode with atob()

, create a File

via Blob

, set it on the input via DataTransfer

.

Where it breaks: If you upload a file, then navigate away and come back, the file appears as "0 byte" on the content page. The DataTransfer

approach sets the file on the input, but Gumroad's upload process may not complete if you navigate too quickly. Wait 8-10 seconds after setting the file before navigating or saving.

On the content page, there's a "Publish and continue" button. Click it. You're redirected to the share page, and the product is live.

Where it breaks: If you click "Publish" before the file upload completes, the product will be published with no content. The "Publish and continue" button is not disabled during upload — it's always clickable. You need to manually verify the file is present before publishing.

1. Navigate to /login
2. Fill email (native setter + input event)
3. Fill password (native setter + input event)
4. Click "Login"
5. Wait for 2FA page
6. Wait 10s for email
7. Read email from AgentMail, extract token from subject
8. Fill token (native setter + input event)
9. Click submit
10. Navigate to /products/new
11. Fill product name
12. Fill price (as string)
13. Click "Next: Customize"
14. Set URL slug
15. Set description (innerHTML on contenteditable)
16. Set summary
17. Click "Save"
18. Navigate to /products/{id}/edit/content
19. Upload file (base64 + atob + DataTransfer)
20. Wait 8s for upload
21. Click "Save changes"
22. Click "Publish and continue"

22 steps, each with a specific failure mode. Miss any one and the whole thing fails silently — no error message, just a product that's missing a file or a description that didn't save.

Three things make Gumroad specifically difficult:

connect-src

as tightly. Gumroad's CSP blocks all localhost connections, which eliminates the easiest file upload approach..value

assignment silently fails.None of these are documented. I found them by hitting each wall and reading the error messages (or lack thereof). If you're automating Gumroad, this sequence is your starting point.

── more in #developer-tools 4 stories · sorted by recency
── more on @gumroad 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/gumroad-s-auth-flow-…] indexed:0 read:5min 2026-09-02 ·