Created
October 5, 2025 18:29
-
-
Save johnlindquist/ce46b3249a6f0b1a2d311be8a82a0af8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Name: Stripe Payment Links | |
| // Description: Fetch payment links from Stripe and retrieve customer emails from successful payments | |
| // Author: johnlindquist | |
| import "@johnlindquist/kit" | |
| import Stripe from "stripe" | |
| // Get Stripe API key from environment | |
| const STRIPE_API_KEY = await env("STRIPE_API_KEY", { | |
| secret: true, | |
| hint: "Enter your Stripe secret key (sk_live_...)" | |
| }) | |
| // Initialize Stripe SDK | |
| const stripe = new Stripe(STRIPE_API_KEY, { | |
| apiVersion: null, | |
| }) | |
| // Fetch all payment links from Stripe | |
| const fetchPaymentLinks = async (): Promise<Stripe.PaymentLink[]> => { | |
| const links: Stripe.PaymentLink[] = [] | |
| for await (const link of stripe.paymentLinks.list()) { | |
| links.push(link) | |
| } | |
| return links | |
| } | |
| // Get display name for a payment link | |
| const getPaymentLinkName = async (link: Stripe.PaymentLink): Promise<string> => { | |
| try { | |
| const lineItems = await stripe.paymentLinks.listLineItems(link.id, { limit: 1 }) | |
| if (lineItems.data.length > 0) { | |
| const item = lineItems.data[0] | |
| if (item.description) { | |
| return item.description | |
| } | |
| if (item.price?.product) { | |
| return `Product ${item.price.product}` | |
| } | |
| } | |
| } catch (error) { | |
| console.warn(`Could not fetch line items for ${link.id}:`, error.message) | |
| } | |
| return link.url || link.id | |
| } | |
| interface Sale { | |
| email: string | |
| price: number | |
| } | |
| // Fetch customer sales for a payment link | |
| const fetchSales = async (linkId: string): Promise<Sale[]> => { | |
| const sales: Sale[] = [] | |
| let hasMore = true | |
| let startingAfter: string | undefined | |
| while (hasMore) { | |
| try { | |
| const params: Stripe.Checkout.SessionListParams = { | |
| payment_link: linkId, | |
| limit: 100, | |
| ...(startingAfter ? { starting_after: startingAfter } : {}) | |
| } | |
| const response = await stripe.checkout.sessions.list(params) | |
| for (const session of response.data) { | |
| // Only include sessions with successful payments | |
| if (session.payment_status === 'paid') { | |
| const email = session.customer_details?.email || (typeof session.customer_email === 'string' ? session.customer_email : undefined) | |
| const price = session.amount_total || 0 | |
| if (email) { | |
| sales.push({ email, price }) | |
| } | |
| } | |
| } | |
| hasMore = response.has_more | |
| if (hasMore && response.data.length > 0) { | |
| startingAfter = response.data[response.data.length - 1].id | |
| } | |
| } catch (error) { | |
| throw new Error(`Failed to fetch checkout sessions: ${error.message}`) | |
| } | |
| } | |
| return Array.from(sales) | |
| } | |
| try { | |
| // Show loading dot | |
| setLoading(true) | |
| // Fetch all payment links | |
| const paymentLinks = await fetchPaymentLinks() | |
| if (paymentLinks.length === 0) { | |
| await div(md("# No Payment Links Found\n\nNo payment links were found in your Stripe account.")) | |
| exit() | |
| } | |
| // Prepare choices with friendly names | |
| const choices = await Promise.all( | |
| paymentLinks.map(async (link) => { | |
| const name = await getPaymentLinkName(link) | |
| return { | |
| name: `${name} (${link.id})`, | |
| description: link.url, | |
| value: link | |
| } | |
| }) | |
| ) | |
| // Let user select a payment link | |
| setLoading(true) | |
| const selectedLink = await arg("Select a Payment Link:", choices) | |
| // Fetch emails for the selected payment link | |
| const sales = await fetchSales(selectedLink.id) | |
| if (sales.length === 0) { | |
| await div(md(`# No Successful Payments Found\n\nNo successful payments were found for the selected payment link:\n\n**${selectedLink.url}**`)) | |
| } else { | |
| // Display results | |
| const formattedSales = sales.map(email => `- ${email.email} ($${(email.price / 100).toFixed(2)})`).join('\n') | |
| // Sum up the total cost in cents, then format as dollars and cents (e.g., $12.34) | |
| const totalCents = sales.reduce((acc, email) => acc + email.price, 0) | |
| const revenue = `$${(totalCents / 100).toFixed(2)}` | |
| const emails = sales.map(sale => sale.email) | |
| const result = `# Customer Emails | |
| **Payment Link:** ${selectedLink.url} | |
| **Total Customers:** ${sales.length} | |
| **Total Cost:** ${revenue} | |
| ## Email Addresses: | |
| ${formattedSales}` | |
| await editor(result, [ | |
| { | |
| name: "Copy to Clipboard", | |
| shortcut: "cmd+shift+c", | |
| onAction: async () => { | |
| await copy(emails.join(', ')) | |
| await toast("Emails copied to clipboard!") | |
| } | |
| }, | |
| { | |
| name: "Copy as List", | |
| shortcut: "cmd+l", | |
| onAction: async () => { | |
| await copy(emails.join('\n')) | |
| await toast("Emails copied as list!") | |
| } | |
| }, | |
| { | |
| name: "Save to File", | |
| shortcut: "cmd+s", | |
| onAction: async () => { | |
| const timestamp = formatDate(new Date(), 'yyyy-MM-dd-HHmm') | |
| const filename = `stripe-emails-${timestamp}.txt` | |
| const filePath = home("Downloads", filename) | |
| await writeFile(filePath, emails.join('\n')) | |
| await toast(`Saved to ${filename}`) | |
| await revealFile(filePath) | |
| } | |
| } | |
| ]) | |
| } | |
| } catch (error) { | |
| await div(md(`# Error\n\n${error.message}\n\nPlease check your Stripe API key and try again.`)) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment