Freelance Automation: Streamlining Client Billing with API Integration
Learn how to automate invoice generation using Python and Wunderlist API. This tutorial shows how to track billable hours in Wunderlist tasks and generate CSV reports for invoicing clients based on time entries.
2 min read
#python#freelance

For the past 8 years, I've been working remotely on contract jobs. For clients who pay by the hour, I need to send invoices every two weeks. I tried various invoicing applications before eventually settling on Google Docs.

Then I discovered Wunderlist, a nice to-do list application. I created Shopping and TODO lists and began using it daily. It quickly became a habit for me to record tasks there. I realized it would be perfect for tracking my billable hours as well.

I use the following format for tracking time in tasks:

<hours> hours TASK-1
<minutes> minutes TASK-2
<days> days TASK-3

After a month of tracking my time this way, I wanted to download this data and calculate the hours I'd spent during the month. So I created a script to generate a CSV report of my hours.

import wunderpy2
from dateutil import parser
from datetime import date
from csv import writer
ACCESS_TOKEN = '<WUNDERLIST_ACCESS_TOKEN>'
CLIENT_ID = '<WUNDERLIST_CLIENT_ID>'
PROJECT_BOARD = 'LIST_TITLE'
HOUR_RATE = 1
def __build_hour(task):
'''
Parses task titles in formats like:
dict(title='1 hour CORE-1234')
dict(title='5 hours CORE-4321')
dict(title='1 minute CORE-1234')
'''
columns = task['title'].strip().split(' ')
time, measure, task_id = columns
if measure == 'hour' or measure == 'hours':
hours = float(time)
elif measure == 'minute' or measure == 'minutes':
hours = float(time) / 60
elif measure == 'day' or measure == 'days':
hours = 24 * float(time)
else:
raise ValueError('Unknown time measure in task title: ' + task['title'])
return dict(
task_id=task_id,
created_at=parser.parse(task['created_at']),
hours=hours)
def __main__():
api = wunderpy2.WunderApi()
client = api.get_client(ACCESS_TOKEN, CLIENT_ID)
board = [l for l in client.get_lists() if l['title'] == PROJECT_BOARD][0]
active_tasks = [t for t in client.get_tasks(board['id']) if not t['completed']]
report = [__build_hour(task) for task in active_tasks]
estimated_earnings = sum([task['hours'] for task in report]) * HOUR_RATE
print(estimated_earnings)
with open(f"report-{date.today()}.csv", 'w+') as f:
w = writer(f)
rows = [[task['task_id'], task['hours'], task['created_at'].date()]
for task in report]
w.writerows(rows)
for task in active_tasks:
client.update_task(task['id'], task['revision'], completed=True)
# TODO: make monthly output report to pdf file based on created_at field.
if __name__ == '__main__':
__main__()

© Copyright 2025 Bitscorp