{"slug": "gumroad-s-auth-flow-is-hostile-to-automation-here-s-the-exact-chain-that-works", "title": "Gumroad's auth flow is hostile to automation. Here's the exact chain that works.", "summary": "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.", "body_md": "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.\n\nThe 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.\n\nI 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.\n\n**Where it breaks:** If you try to fill the new password field with `input.value = 'newpassword'`\n\n, React won't register the change. The form will submit with an empty password. You need to use the native value setter:\n\n``` js\nconst nativeSetter = Object.getOwnPropertyDescriptor(\n  HTMLInputElement.prototype, 'value'\n).set;\nnativeSetter.call(passwordInput, newPassword);\npasswordInput.dispatchEvent(new Event('input', { bubbles: true }));\n```\n\nThis is because React overrides the `value`\n\nproperty on inputs with its own setter that tracks changes via a value tracker. The native setter bypasses React's tracker, and the `input`\n\nevent tells React to sync state.\n\nAfter 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.\n\nThe token appears in the email subject line: `\"Your authentication token is 126874\"`\n\n. This is convenient — you don't need to parse the email body. Just grab the subject, regex out the digits.\n\n``` js\nconst subject = \"Your authentication token is 126874\";\nconst token = subject.match(/token is (\\d+)/)?.[1]; // \"126874\"\n```\n\n**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.\n\nAfter 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.\n\nI'm using a dedicated Chrome profile (`--user-data-dir`\n\n) so the session persists across script runs. Without this, every script execution would require a fresh login + 2FA cycle.\n\nNavigate to `https://gumroad.com/products/new`\n\n. The form has a text input for the product name and a price field. Fill both, click \"Next: Customize\".\n\n**Where it breaks:** The price input is `type=\"number\"`\n\n. The native setter trick works, but you need to pass a string, not a number. `nativeSetter.call(priceInput, '15')`\n\nworks. `nativeSetter.call(priceInput, 15)`\n\nmay not trigger the change event correctly.\n\nAfter clicking \"Next\", you're redirected to `https://gumroad.com/products/{id}/edit`\n\n. This page has:\n\nThe description editor is a `contenteditable`\n\ndiv. You can set its `innerHTML`\n\ndirectly — Gumroad's editor reads from the DOM, not from React state:\n\n``` js\nconst editor = document.querySelector('[contenteditable=\"true\"]');\neditor.innerHTML = '<p>Your description here</p>';\neditor.dispatchEvent(new Event('input', { bubbles: true }));\n```\n\n**Where it breaks:** If you set `innerHTML`\n\nbefore the editor is fully initialized (which happens after a brief loading state), the content will be overwritten. Wait for the editor to be visible and interactive before setting content.\n\nNavigate to `https://gumroad.com/products/{id}/edit/content`\n\n. The page has a hidden `<input type=\"file\" class=\"sr-only\">`\n\n.\n\nI covered the upload mechanism in detail in my previous article. The short version: encode the file as base64, embed it in a `Runtime.evaluate`\n\nscript, decode with `atob()`\n\n, create a `File`\n\nvia `Blob`\n\n, set it on the input via `DataTransfer`\n\n.\n\n**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`\n\napproach 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.\n\nOn the content page, there's a \"Publish and continue\" button. Click it. You're redirected to the share page, and the product is live.\n\n**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.\n\n```\n1. Navigate to /login\n2. Fill email (native setter + input event)\n3. Fill password (native setter + input event)\n4. Click \"Login\"\n5. Wait for 2FA page\n6. Wait 10s for email\n7. Read email from AgentMail, extract token from subject\n8. Fill token (native setter + input event)\n9. Click submit\n10. Navigate to /products/new\n11. Fill product name\n12. Fill price (as string)\n13. Click \"Next: Customize\"\n14. Set URL slug\n15. Set description (innerHTML on contenteditable)\n16. Set summary\n17. Click \"Save\"\n18. Navigate to /products/{id}/edit/content\n19. Upload file (base64 + atob + DataTransfer)\n20. Wait 8s for upload\n21. Click \"Save changes\"\n22. Click \"Publish and continue\"\n```\n\n22 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.\n\nThree things make Gumroad specifically difficult:\n\n`connect-src`\n\nas tightly. Gumroad's CSP blocks all localhost connections, which eliminates the easiest file upload approach.`.value`\n\nassignment 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.", "url": "https://wpnews.pro/news/gumroad-s-auth-flow-is-hostile-to-automation-here-s-the-exact-chain-that-works", "canonical_source": "https://dev.to/atlasforge_dev/gumroads-auth-flow-is-hostile-to-automation-heres-the-exact-chain-that-works-49pe", "published_at": "2026-09-02 18:35:13+00:00", "updated_at": "2026-09-02 18:53:45.496293+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Gumroad", "AgentMail", "React"], "alternates": {"html": "https://wpnews.pro/news/gumroad-s-auth-flow-is-hostile-to-automation-here-s-the-exact-chain-that-works", "markdown": "https://wpnews.pro/news/gumroad-s-auth-flow-is-hostile-to-automation-here-s-the-exact-chain-that-works.md", "text": "https://wpnews.pro/news/gumroad-s-auth-flow-is-hostile-to-automation-here-s-the-exact-chain-that-works.txt", "jsonld": "https://wpnews.pro/news/gumroad-s-auth-flow-is-hostile-to-automation-here-s-the-exact-chain-that-works.jsonld"}}