Build a Gumroad Lead Machine in Google Sheets (Ping Webhook + License API + AI Upsell) A developer built a system using Google Sheets, Apps Script, and Gumroad's API to turn every small sale into a verified lead with an AI-personalized follow-up email, bypassing traditional CRM and Zapier. The 70-line script catches Gumroad's form-encoded ping, deduplicates sales, verifies licenses via Gumroad's API, and sends upsell emails using GPT-4o-mini for under $0.01 each. A traditional B2B funnel asks a stranger to fill a form, wait for a sales rep, and sit through a call — and it typically converts around 0.1–0.5%. I stopped running that. Instead I sell a small $29 product on Gumroad and let the purchase itself qualify the lead. This post is the Apps Script that turns every Gumroad sale into a logged, license-verified lead with an AI-personalized follow-up email — no CRM, no Zapier, about 70 lines you can copy. A free download gets you emails from people who spent nothing, so they're not qualified. A demo form gets you almost nobody, because the friction is huge. A small paid "micro-commitment" sits in the middle: the buyer proved intent with their wallet, and you now have their email, the exact product they bought, and a valid license — everything you need to follow up with the right offer. Gumroad handles the checkout. Google Sheets is the lead database. Apps Script is the glue: catch the sale, verify it's real, and send the next step. Make a sheet named Leads and add this so the code and the columns stay in sync: // Leads columns: timestamp | saleId | email | product | amount | license | status const COL = { TS:1, SALE ID:2, EMAIL:3, PRODUCT:4, AMOUNT:5, LICENSE:6, STATUS:7 }; In Gumroad, enable the ping under Settings → Advanced → "Ping", and point it at your Web App URL with a secret token in the query string: https://script.google.com/.../exec?token=my-secret . The one thing to get right: Gumroad posts application/x-www-form-urlencoded, not JSON. So you read e.parameter , not JSON.parse e.postData.contents . This trips up everyone who copied a JSON webhook example. js function doPost e { const p = e.parameter; // Gumroad is form-encoded — use e.parameter, NOT JSON.parse const expected = PropertiesService.getScriptProperties .getProperty 'GUMROAD TOKEN' ; if expected && p.token == expected { return ContentService.createTextOutput 'unauthorized' ; } const sale = saleToRow p ; if sale.saleId return ContentService.createTextOutput 'ignored' ; const sheet = SpreadsheetApp.getActiveSpreadsheet .getSheetByName 'Leads' ; // Gumroad can resend a ping — dedupe by sale id. const seen = sheet.getRange 2, COL.SALE ID, Math.max sheet.getLastRow - 1, 1 , 1 .getValues .flat ; if seen.indexOf sale.saleId == -1 return ContentService.createTextOutput 'duplicate' ; sheet.appendRow new Date , sale.saleId, sale.email, sale.product, sale.amount, sale.license, sale.status ; return ContentService.createTextOutput 'ok' ; } function saleToRow p { return { saleId: p.sale id || '', email: p.email || '', product: p.product name || '', amount: Number p.price || 0 / 100, // Gumroad sends price in cents license: p.license key || '', status: 'new' }; } A ping is just an HTTP POST — anyone who finds your URL can fake one. Before you provision anything valuable, verify the license against Gumroad's API. It returns the real purchase record, or nothing. js function verifyLicense productPermalink, licenseKey { const res = UrlFetchApp.fetch 'https://api.gumroad.com/v2/licenses/verify', { method: 'post', muteHttpExceptions: true, payload: { product permalink: productPermalink, license key: licenseKey, increment uses count: 'false' // just checking — don't burn a use } } ; const data = JSON.parse res.getContentText ; return data.success ? data.purchase : null; // buyer + product details, or null } A time trigger Triggers → Add trigger → sendUpsells , every 5 minutes emails each new lead once, then marks it so it never double-sends. The message is written per-product by gpt-4o-mini — under $0.01 per email — and falls back to a plain template if no API key is set. js function sendUpsells { const sheet = SpreadsheetApp.getActiveSpreadsheet .getSheetByName 'Leads' ; const last = sheet.getLastRow ; if last < 2 return; const rows = sheet.getRange 2, 1, last - 1, COL.STATUS .getValues ; rows.forEach row, i = { if row COL.STATUS - 1 == 'new' return; // only fresh leads const email = row COL.EMAIL - 1 ; const product = row COL.PRODUCT - 1 ; const message = personalize product ; // one API call GmailApp.sendEmail email, Your next step after ${product} , message, { name: 'Hayrullah at MageSheet' } ; sheet.getRange i + 2, COL.STATUS .setValue 'emailed' ; } ; } function personalize product { const apiKey = PropertiesService.getScriptProperties .getProperty 'OPENAI API KEY' ; if apiKey { return Thanks for grabbing ${product}. Reply to this email and I'll send the advanced setup. ; } const res = UrlFetchApp.fetch 'https://api.openai.com/v1/chat/completions', { method: 'post', contentType: 'application/json', headers: { Authorization: 'Bearer ' + apiKey }, muteHttpExceptions: true, payload: JSON.stringify { model: 'gpt-4o-mini', temperature: 0.7, messages: { role: 'user', content: Write a friendly 3-sentence follow-up email to someone who just bought "${product}". + Offer a paid done-for-you upgrade. No subject line, no placeholders. } } } ; if res.getResponseCode == 200 { return Thanks for grabbing ${product}. Happy to help you take it further — just reply. ; } return JSON.parse res.getContentText .choices 0 .message.content; } I unit-tested saleToRow before shipping — the price field is the trap, because Gumroad reports it in cents 2900 means $29.00 , and a missing field must default cleanly instead of writing NaN into the sheet. e.parameter.email , not JSON.parse e.postData.contents . This is the single most common mistake. doPost e can't read HTTP headers. ?token= in the ping URL and compare e.parameter.token . sale id check stops duplicate leads.That's an autonomous lead machine in about 70 lines: a Gumroad checkout becomes a logged, verified lead with a personalized upsell — all inside a Google Sheet, with the micro-commitment doing your lead qualification for you. The production version — ad attribution back to the sale, license provisioning, and a multi-step follow-up sequence — is written up on the MageSheet blog https://magesheet.com/blog/gumroad-lead-generation-machine . Built by the MageSheet team.