This is a wrapper of the Freshbooks web API. It is far from comprehensive: It was created for the specific client- and invoice-related needs of Averbach Transcription. However, there are "band-aids" here to work around some of the API's shortcomings. For example, you don't have to deal with pagination at all.
Install it with
pip install avt-fresh.
Here's how you use it:
from avt_fresh import ApiClient client = ApiClient(client_secret="...", client_id="...", redirect_uri="https://...", account_id="...") monster_invoices = client.get_all_invoices_for_org_name("Monsters Inc") client.get_one_invoice(12345)
You can get and set the required arguments to
ApiClient here. Well, all of them except
FRESHBOOKS_ACCOUNT_ID, which you can see (there's got to be another way??) by clicking on one of your invoices and grabbing the substring here:
Don't tell anyone but
redirect_uri can be pretty much anything! See
client.delete_invoice are the bread and butter methods here.
get... functions return some handy
NamedTuple instances with helpful attributes, notably
FreshbooksInvoice.lines which have
Decimal values where you would hope to find them. Also some lookups for addressing the
FreshbooksLines you may be interested in.
class FreshbooksInvoice(NamedTuple): lines: list[FreshbooksLine] notes: str client_id: int date: dt.date invoice_id: int number: str organization: str amount: Decimal status: str amount_outstanding: Decimal po_number: str line_id_line_dict: dict line_description_line_dict: dict line_description_line_id_dict: dict contacts: dict[str, dict] allowed_gateways: list class FreshbooksLine(NamedTuple): invoice_id: int client_id: int description: str name: str rate: Decimal line_id: int quantity: Decimal amount: Decimal
Then you have helpers
Create an Invoice
The signature of
client.create_invoice is like so:
def create( self, client_id: int, notes: str, lines: list[dict], status: str | int, contacts: list[dict] | None = None, po_number=None, create_date=None, ) -> dict:
The dictionaries must contain entries for
The values must be JSON-serializable, so no
Decimals for example (all strings is fine).
Each of these dictionaries should simply be
Status can be any of the
v3_status values as a
client.delete_client are available here.
Once more the
get... functions return
NamedTuple instances with some helpful attributes, notably
FreshbooksClient.contacts and a couple of related lookups (
class FreshbooksClient(NamedTuple): client_id: int email: str organization: str first_name: str last_name: str contacts: dict[str, FreshbooksContact] contact_id_email_lookup: dict[int, str] email_contact_id_lookup: dict[str, int] class FreshbooksContact(NamedTuple): contact_id: int first_name: str last_name: str email: str
When you first call one of the functions which touches the Freshbooks API, you'll be prompted in the terminal like so:
Please go here and get an auth code: https://my.freshbooks.com/#/developer, then enter it here:
If you don't have an app there, create a really basic one. Name and description can be whatever, and you can skip the URL fields.
Application Type: "Private App"
Add a redirect URI, it can actually be pretty much anything. Well, preferably a URL you control since it will receive OAuth tokens.
Finally, once you have an app on that developer page, click into it and click "go to authentication" page. Freshbooks will pop open a tab and go to your redirect URI, appending
?code=blahblahblah to it. Grab the "blah blah blah" value and paste it into the prompt.
You should only have to do this once in each environment you use this library in.
OAuth Token Stores
By default this library stores OAuth tokens on disk in whatever working directory its methods are called from. As an alternative you can use Redis via the
avt_fresh.token.TokenStoreOnRedis at instantiation of an
ApiClient like so:
client = Client( client_secret="...", client_id="...", redirect_uri="https://...", account_id="...", token_store=avt_fresh.token.TokenStoreOnRedis, connection_string="redis://..." , )
As a further alternative, feel free to implement and inject your own! See
avt_fresh.token.TokenStore for the API, but tl;dr simply inherit from
TokenStore and implement
set() methods, the former of which should return an instance of
Hardcoded Stuff / TODOs
Here are some quirks and TODOs. PRs are welcome!:
Only Python 3.10 is supported at the moment.
When it comes to invoice statuses, we're only using
v3_status strings, not the numbers. What's more, when you create an invoice we're only supporting two possible statuses: "draft" and "paid".
delete functions return dictionaries rather than an instance of the appropriate
NamedTuple. This would be a great improvement!
The docs need improvement for sure: For now, have a peek at the source code, which includes pretty comprehensive type hints at the very least.
There are no tests! However, this code has been used in production in at least one company with some success.