Merge Payment CSVs Tool
This simple Python utility merges transaction data from Gumroad, Stripe, and PayPal CSV export files into a single, consistent ledger format. It's designed for creators and small businesses that use multiple payment platforms and want a unified view of their income for bookkeeping, tax reporting, or financial analysis.
Features
- Automatically detects and parses CSVs from Gumroad, Stripe, and PayPal
- Normalizes columns into a uniform schema: Date, Description, Amount (USD), Fee, Net, and Source
- Handles different date formats and currency representations
- Filters out non-transaction rows (refunds, transfers, fees-only entries) where needed
- Outputs a clean CSV file sorted by date
Usage
Run the script from the command line:
python merge_payment_csvs.py --gumroad gumroad.csv --stripe stripe.csv --paypal paypal.csv --output ledger.csv
If a platform wasn’t used in the period, omit its argument. The script will only process provided files.
Output
The resulting ledger.csv contains all sales transactions with consistent formatting, making it easy to import into Excel, Google Sheets, or accounting software.
Requirements
- Python 3.6+
- Standard library only — no external dependencies
Why This Exists
Managing multiple platforms means scattered records. Manual merging is error-prone and time-consuming. This tool automates the consolidation, saving hours each month and reducing mistakes in financial reporting.
Notes
- Always back up original files before processing
- Review output for edge cases (e.g. currency conversions, promotional discounts)
- Designed for read-only transaction exports — no API keys or live data access
Maintained for simplicity and privacy: no data leaves your machine.
import csv
import argparse
from datetime import datetime
from pathlib import Path
def parse_gumroad_row(row):
return {
'Date': row['Created at'].split(' ')[0],
'Description': f"Gumroad: {row['Product']}",
'Amount': row['Sale price'],
'Fee': row['Gumroad fee'],
'Net': row['Amount sent to you'],
'Source': 'Gumroad'
}
def parse_stripe_row(row):
if row['Type'] != 'charge':
return None
return {
'Date': row['Created'].split(' ')[0],
'Description': row['Description'],
'Amount': row['Gross'],
'Fee': row['Fee'],
'Net': row['Net'],
'Source': 'Stripe'
}
def parse_paypal_row(row):
if 'Sale' not in row.get('Type', ''):
return None
return {
'Date': row['Date'],
'Description': row['Name'],
'Amount': row['Gross'],
'Fee': row['Fee'],
'Net': row['Net'],
'Source': 'PayPal'
}
def merge_ledger(gumroad_path, stripe_path, paypal_path, output_path):
records = []
sources = [
(gumroad_path, parse_gumroad_row, 'Gumroad'),
(stripe_path, parse_stripe_row, 'Stripe'),
(paypal_path, parse_paypal_row, 'PayPal')
]
for file_path, parser, name in sources:
if not file_path:
continue
if not Path(file_path).exists():
print(f"Warning: {name} file not found: {file_path}")
continue
with open(file_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
parsed = parser(row)
if parsed:
records.append(parsed)
# Sort by date
records.sort(key=lambda x: datetime.strptime(x['Date'], '%Y-%m-%d'))
# Write merged output
with open(output_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['Date', 'Description', 'Amount', 'Fee', 'Net', 'Source'])
writer.writeheader()
writer.writerows(records)
print(f"Merged {len(records)} transactions into {output_path}")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Merge Gumroad, Stripe, and PayPal CSVs into one ledger.')
parser.add_argument('--gumroad', help='Path to Gumroad CSV export')
parser.add_argument('--stripe', help='Path to Stripe CSV export')
parser.add_argument('--paypal', help='Path to PayPal CSV export')
parser.add_argument('--output', required=True, help='Output CSV file path')
args = parser.parse_args()
merge_ledger(args.gumroad, args.stripe, args.paypal, args.output)
Top comments (0)