Merge Payment Ledger
Merge Payment Ledger is a lightweight Python tool designed to consolidate transaction data from Gumroad, Stripe, and PayPal into a single, standardized CSV ledger. This is especially useful for creators and small businesses that use multiple platforms to sell digital products or services and want a unified view of their revenue.
Features
- Automatically detects and parses CSV files from Gumroad, Stripe, and PayPal
- Normalizes diverse column names and date formats into a consistent schema
- Outputs a clean, unified ledger with essential fields: date, platform, transaction_id, description, amount (USD), fee, net, and buyer info
- Handles currency conversion warnings (all inputs assumed USD)
- Simple CLI interface for easy automation
Usage
Run the script from the command line:
python merge_payment_ledger.py -i gumroad.csv stripe.csv paypal.csv -o ledger.csv
You can pass multiple input files from any supported provider. The tool identifies the source platform by analyzing header patterns.
Output
The resulting ledger.csv contains a unified timeline of all transactions sorted by date, making it ideal for accounting, tax preparation, or dashboarding in tools like Google Sheets or Excel.
Requirements
- Python 3.7+
- Standard library only — no external dependencies
Limitations
- Assumes all transactions are in USD
- Limited to CSV exports using standard formats from each platform
- Does not support refunds or disputes as separate line items (treated as negative net)
License
MIT
import csv
import argparse
import sys
from datetime import datetime
from typing import List, Dict, Optional
def detect_platform(headers: List[str]) -> Optional[str]:
headers = [h.lower() for h in headers]
if 'sale id' in headers and 'product name' in headers:
return 'Gumroad'
if 'transaction id' in headers and 'net' in headers and 'stripe fee' in headers:
return 'Stripe'
if 'transaction id' in headers and 'gross' in headers and 'fee' in headers and 'paypal' in headers:
return 'PayPal'
return None
def parse_row(platform: str, row: Dict[str, str]) -> Dict[str, str]:
common = {
'date': '',
'platform': platform,
'transaction_id': '',
'description': '',
'amount_usd': '',
'fee': '',
'net': '',
'buyer': ''
}
try:
if platform == 'Gumroad':
common['date'] = row['Sale creation date']
common['transaction_id'] = row['Sale ID']
common['description'] = row['Product name']
common['amount_usd'] = row['Price (including tax)']
common['fee'] = str(float(row['Gumroad fee']) + float(row.get('Additional fee', '0')))
common['net'] = row['Net']
common['buyer'] = row['Customer email']
elif platform == 'Stripe':
common['date'] = row['Created (UTC)']
common['transaction_id'] = row['Transaction ID']
common['description'] = row['Description']
common['amount_usd'] = row['Gross']
common['fee'] = row['Stripe fee']
common['net'] = row['Net']
common['buyer'] = row.get('Customer email', '')
elif platform == 'PayPal':
common['date'] = row['Date']
common['transaction_id'] = row['Transaction ID']
common['description'] = row['Subject']
common['amount_usd'] = row['Gross']
common['fee'] = row['Fee']
common['net'] = row['Net']
common['buyer'] = row['Name']
except KeyError as e:
print(f"Missing column: {e}", file=sys.stderr)
return common
def process_file(filepath: str, all_rows: List[Dict[str, str]]):
with open(filepath, mode='r', encoding='utf-8') as f:
reader = csv.DictReader(f)
platform = detect_platform(reader.fieldnames)
if not platform:
print(f"Unknown format: {filepath}", file=sys.stderr)
return
for row in reader:
if row.get(list(row.keys())[0]): # skip empty rows
all_rows.append(parse_row(platform, row))
def main():
parser = argparse.ArgumentParser(description='Merge Gumroad, Stripe, PayPal CSVs into one ledger.')
parser.add_argument('-i', '--inputs', nargs='+', required=True)
parser.add_argument('-o', '--output', required=True)
args = parser.parse_args()
rows = []
for file in args.inputs:
process_file(file, rows)
# Sort by date
def parse_date(row):
try:
return datetime.strptime(row['date'].split()[0], '%Y-%m-%d')
except:
return datetime.min
rows.sort(key=parse_date)
# Write unified CSV
if rows:
with open(args.output, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
writer.writeheader()
writer.writerows(rows)
print(f"Wrote {len(rows)} transactions to {args.output}")
else:
print("No valid data found.")
if __name__ == '__main__':
main()
Top comments (0)