Making API Calls
Each resource in the client provides calls for get
, list
, create
, update
and delete
calls. Please note that
some API resources are scoped to a FreshBooks account_id
while others are scoped to a business_id
or a
business_uuid
. In general these fall along the lines of account_id
for older accounting resources,
business_id
for projects/time tracking, and business_uuid
for all newer resources, but that is not precise.
client = freshBooksClient.clients.get(account_id, client_user_id)
project = freshBooksClient.projects.get(business_id, project_id)
account = freshBooksClient.ledger_accounts.get(business_uuid, ledger_account_uuid)
Get and List
API calls which return a single resource return a Result
object with the returned data accessible via attributes.
The raw json-parsed dictionary can also be accessed via the data
attribute.
client = freshBooksClient.clients.get(account_id, client_user_id)
assert client.organization == "FreshBooks"
assert client.userid == client_user_id
assert client.data["organization"] == "FreshBooks"
assert client.data["userid"] == client_user_id
vis_state
returns an Enum. See FreshBooks API - Active and Deleted Objects
for details.
from freshbooks import VisState
assert client.vis_state == VisState.ACTIVE
assert client.vis_state == 0
assert client.data['vis_state'] == VisState.ACTIVE
assert client.data['vis_state'] == 0
API calls which return a list of resources return a ListResult
object. The resources in the list can be accessed by
index and iterated over. Similarly, the raw dictionary can be accessed via the data
attribute.
clients = freshBooksClient.clients.list(account_id)
assert clients[0].organization == "FreshBooks"
assert clients.data["clients"][0]["organization"] == "FreshBooks"
for client in clients:
assert client.organization == "FreshBooks"
assert client.data["organization"] == "FreshBooks"
Create, Update, and Delete
API calls to create and update take a dictionary of the resource data. A successful call will return a Result
object
as if a get
call.
Create:
payload = {"email": "john.doe@abcorp.com"}
new_client = FreshBooksClient.clients.create(account_id, payload)
client_id = new_client.userid
Update:
payload = {"email": "john.doe@abcorp.ca"}
client = freshBooksClient.clients.update(account_id, client_id, payload)
assert client.email == "john.doe@abcorp.ca"
Delete:
client = freshBooksClient.clients.delete(account_id, client_id)
assert client.vis_state == VisState.DELETED
Error Handling
Calls made to the FreshBooks API with a non-2xx response are wrapped in a FreshBooksError
exception.
This exception class contains the error message, HTTP response code, FreshBooks-specific error number if one exists,
and the HTTP response body.
Example:
from freshbooks import FreshBooksError
try:
client = freshBooksClient.clients.get(account_id, client_id)
except FreshBooksError as e:
assert str(e) == "Client not found."
assert e.status_code == 404
assert e.error_code == 1012
assert e.error_details == [{
"errno": 1012,
"field": "userid",
"message": "Client not found.",
"object": "client",
"value": "12345"
}]
assert e.raw_response == ("{'response': {'errors': [{'errno': 1012, "
"'field': 'userid', 'message': 'Client not found.', "
"'object': 'client', 'value': '134'}]}}")
Not all resources have full CRUD methods available. For example expense categories have list
and get
calls, but are not deletable. If you attempt to call a method that does not exist, the SDK will raise a
FreshBooksNotImplementedError
exception, but this is not something you will likely have to account
for outside of development.
Pagination, Filters, and Includes, Sorting
list
calls take a list of builder objects that can be used to paginate, filter, and include
optional data in the response. See FreshBooks API - Parameters documentation.
Pagination
Pagination results are included in list
responses in the pages
attribute:
>>> clients = freshBooksClient.clients.list(account_id)
>>> clients.pages
PageResult(page=1, pages=1, per_page=30, total=6)
>>> clients.pages.total
6
To make a paginated call, first create a PaginateBuilder
object that can be passed into the list
method.
>>> from freshbooks import PaginateBuilder
>>> paginator = PaginateBuilder(2, 4)
>>> paginator
PaginateBuilder(page=2, per_page=4)
>>> clients = freshBooksClient.clients.list(account_id, builders=[paginator])
>>> clients.pages
PageResult(page=2, pages=3, per_page=4, total=9)
PaginateBuilder
has methods page
and per_page
to return or set the values. When setting the values the calls
can be chained.
>>> paginator = PaginateBuilder(1, 3)
>>> paginator
PaginateBuilder(page=1, per_page=3)
>>> paginator.page()
1
>>> paginator.page(2).per_page(4)
>>> paginator
PaginateBuilder(page=2, per_page=4)
ListResults can be combined, allowing your to use pagination to get all the results of a resource.
paginator = PaginateBuilder(1, 100)
clients = freshBooksClient.clients.list(self.account_id, builders=[paginator])
while clients.pages.page < clients.pages.pages:
paginator.page(clients.pages.page + 1)
new_clients = freshBooksClient.clients.list(self.account_id, builders=[paginator])
clients = clients + new_clients
Filters
To filter which results are return by list
method calls, construct a FilterBuilder
and pass that
in the list of builders to the list
method.
>>> from freshbooks import FilterBuilder
>>> filter = FilterBuilder()
>>> filter.equals("userid", 123)
>>> clients = freshBooksClient.clients.list(account_id, builders=[filter])
Filters can be built with the methods: equals
, in_list
, like
, between
, and boolean
,
which can be chained together.
Please see FreshBooks API - Active and Deleted Objects for details on filtering active, archived, and deleted resources.
>>> f = FilterBuilder()
>>> f.in_list("clientids", [123, 456])
FilterBuilder(&search[clientids][]=123&search[clientids][]=456)
>>> f = FilterBuilder()
>>> f.like("email_like", "@freshbooks.com")
FilterBuilder(&search[email_like]=@freshbooks.com)
>>> f = FilterBuilder()
>>> f.between("amount", 1, 10)
FilterBuilder(&search[amount_min]=1&search[amount_max]=10)
>>> f = FilterBuilder()
>>> f.between("amount", min=15) # For just minimum
FilterBuilder(&search[amount_min]=15)
>>> f = FilterBuilder()
>>> f.between("amount_min", 15) # Alternatively
FilterBuilder(&search[amount_min]=15)
>>> f = FilterBuilder()
>>> f.between("start_date", date.today())
FilterBuilder(&search[start_date]=2020-11-21)
>>> f = FilterBuilder()
>>> f.boolean("complete", False) # Boolean filters are mostly used on Project-like resources
FilterBuilder(&complete=False)
>>> last_week = date.today() - timedelta(days=7)
>>> f = FilterBuilder()
>>> f.equals("vis_state", VisState.ACTIVE).between("updated", last_week, date.today()) # Chaining filters
FilterBuilder(&search[vis_state]=0&search[updated_min]=2020-11-14&search[updated_max]=2020-11-21)
Includes
To include additional relationships, sub-resources, or data in a response an IncludesBuilder
can be constructed.
>>> from freshbooks import IncludesBuilder
>>> includes = IncludesBuilder()
>>> includes.include("outstanding_balance")
IncludesBuilder(&include[]=outstanding_balance)
Which can then be passed into list
or get
calls:
>>> clients = freshBooksClient.clients.list(account_id, builders=[includes])
>>> clients[0].outstanding_balance
[{'amount': {'amount': '100.00', 'code': 'USD'}}]
>>> client = freshBooksClient.clients.get(account_id, client_id, includes=includes)
>>> client.outstanding_balance
[{'amount': {'amount': '100.00', 'code': 'USD'}}]
Includes can also be passed into create
and update
calls to include the data in the response of the updated resource:
>>> payload = {"email": "john.doe@abcorp.com"}
>>> new_client = FreshBooksClient.clients.create(account_id, payload, includes=includes)
>>> new_client.outstanding_balance
[] # New client has no balance
Sorting
To sort the results of a list call by supported fields (see the documentation for that resource) a
SortBuilder
can be used.
>>> from freshbooks import SortBuilder
>>> sort = SortBuilder()
>>> sort.ascending("invoice_date")
SortBuilder(&sort=invoice_date_asc)
to sort by the invoice date in ascending order, or:
>>> from freshbooks import SortBuilder
>>> sort = SortBuilder()
>>> sort.descending("invoice_date")
SortBuilder(&sort=invoice_date_desc)
for descending order.
invoices = freshBooksClient.invoices.list(account_id, builders=[sort])
Dates and Times
For historical reasons, some resources in the FreshBooks API (mostly accounting-releated) return date/times in
“US/Eastern” timezone. Some effort is taken to return datetime
objects as zone-aware and normalized to UTC. In these
cases, the raw response string will differ from the attribute. For example:
from datetime import datetime, timezone
assert client.data["updated"] == "2021-04-16 10:31:59" # Zone-naive string in "US/Eastern"
assert client.updated.isoformat() == '2021-04-16T14:31:59+00:00' # Zone-aware datetime in UTC
assert client.updated == datetime(year=2021, month=4, day=16, hour=14, minute=31, second=59, tzinfo=timezone.utc)