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.