Every expense of a month in a CSV: supplier or employee, amount, cost center, bookkeeping status and receipt.
Get every expense of a month — supplier invoices, card payments, expense claims, credit notes — in one CSV file you can open in Excel or Google Sheets.
| Column | Content |
|---|---|
company | The company (entity) the expense belongs to |
date | When the expense was created in Spendesk |
type | invoice, creditNote, expense, singlePurchase, subscription, physical, mileageAllowance, perDiemAllowance… |
description | The description entered in Spendesk |
counterparty | The supplier, or the employee for an expense claim |
amount, currency | In the currency of the expense |
amount_company_currency, company_currency | Converted to your company's currency |
cost_centers | The cost centers of the expense lines |
bookkeeping_status | toPrepare, toExport or exported |
receipt | Whether a receipt or invoice is attached |
Scopes: experimental:payable-search:read, supplier:read, user:read, cost-center:read. With an organisation-level key, add experimental:company:read so the script can list the companies. Set-up: Running the recipes.
With Postman
In the Payables folder, open Search Payables and replace the body with the month you want:
{
"filters": {
"field": "payableDate",
"operator": "between",
"value": { "from": "2026-08-01T00:00:00.000Z", "to": "2026-08-31T23:59:59.999Z" }
},
"limit": 100
}Send it. If the response has a nextCursor, add it to the body as "cursor" and send again for the next 100 expenses. Suppliers, employees and cost centers come as IDs: Get Suppliers, Get Users and Get Cost Centers give their names. For more than a few dozen expenses, the script below does all of this for you.
With an AI assistant
List all our spend for August 2026 with the supplier or employee, amount, cost center and whether a receipt is attached, as a table I can paste into a spreadsheet.
The assistant reads the results page by page. For large months, ask it for the total first, then the detail by cost center or by week.
With a script
Save it as monthly_spend.py, then run python3 monthly_spend.py 2026-08 — or with no month for last month. It writes monthly_spend_2026-08.csv in the current folder.
"""Export one month of Spendesk spend to monthly_spend_<month>.csv.
Usage: python3 monthly_spend.py [YYYY-MM] (default: last month)
Needs SPENDESK_CLIENT_ID and SPENDESK_CLIENT_SECRET. With an organisation-level key it covers every
company, or only SPENDESK_COMPANY_ID if set.
Scopes: experimental:payable-search:read, supplier:read, user:read, cost-center:read
(and experimental:company:read with an organisation-level key).
"""
import base64, csv, datetime as dt, json, os, sys, urllib.error, urllib.parse, urllib.request
BASE = os.environ.get("SPENDESK_BASE_URL", "https://public-api.spendesk.com")
def token():
creds = base64.b64encode(f"{os.environ['SPENDESK_CLIENT_ID']}:{os.environ['SPENDESK_CLIENT_SECRET']}".encode()).decode()
req = urllib.request.Request(BASE + "/v1/auth/token", data=b"grant_type=client_credentials",
headers={"Authorization": "Basic " + creds, "Content-Type": "application/x-www-form-urlencoded"})
return json.load(urllib.request.urlopen(req))["access_token"]
TOKEN = token()
def api(method, path, query=None, body=None, company=None):
url = BASE + path + ("?" + urllib.parse.urlencode(query, doseq=True) if query else "")
headers = {"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"}
if company:
headers["X-Company-Id"] = company
req = urllib.request.Request(url, method=method, headers=headers, data=json.dumps(body).encode() if body else None)
return json.load(urllib.request.urlopen(req))
def companies(need_id=False):
"""The companies to cover, as (id, name, X-Company-Id header).
Organisation-level key: every company of the organisation, or only SPENDESK_COMPANY_ID if set.
Company-level key: the key's own company (no header needed)."""
only = os.environ.get("SPENDESK_COMPANY_ID")
try:
found, page = [], 1
while True:
result = api("GET", "/v2/companies", {"page": page, "pageSize": 30})
found += [(c["id"], c["name"], c["id"]) for c in result["data"]]
if page * 30 >= result["meta"]["pagination"]["total"]:
return [c for c in found if not only or c[0] == only]
page += 1
except urllib.error.HTTPError:
if only: # organisation-level key without experimental:company:read
return [(only, only, only)]
own = api("GET", "/v1/users", {"pageSize": 1})["data"][0]["companyId"] if need_id else None
return [(own, "", None)]
# 1. The month to export: first day 00:00 to last day 23:59:59 (UTC).
today = dt.date.today()
month = sys.argv[1] if len(sys.argv) > 1 else (today.replace(day=1) - dt.timedelta(days=1)).strftime("%Y-%m")
first = dt.date.fromisoformat(month + "-01")
last = (first + dt.timedelta(days=32)).replace(day=1) - dt.timedelta(days=1)
def names(path, ids, label, company):
"""Names for the IDs the payables carry, 30 per call."""
found, ids = {}, sorted(ids)
for i in range(0, len(ids), 30):
for item in api("GET", path, {"ids": ids[i:i + 30], "pageSize": 30}, company=company)["data"]:
found[item["id"]] = label(item)
return found
with open(f"monthly_spend_{month}.csv", "w", newline="") as f:
out = csv.writer(f)
out.writerow(["company", "date", "type", "description", "counterparty", "amount", "currency",
"amount_company_currency", "company_currency", "cost_centers", "bookkeeping_status", "receipt"])
total = 0
for company_id, company_name, header in companies():
# 2. Every payable of the month for this company, page by page.
payables, cursor = [], None
while True:
body = {"filters": {"field": "payableDate", "operator": "between",
"value": {"from": f"{first}T00:00:00.000Z", "to": f"{last}T23:59:59.999Z"}},
"limit": 100}
if cursor:
body["cursor"] = cursor
page = api("POST", "/v1/payables/search", body=body, company=header)
payables += page["payables"]
cursor = page.get("nextCursor")
if not cursor:
break
# 3. Names for suppliers, employees and cost centers.
counterparties = [p.get("counterparty") or {} for p in payables]
suppliers = names("/v1/suppliers", {c["supplierId"] for c in counterparties if c.get("type") == "supplier"},
lambda s: s["name"], header)
employees = names("/v1/users", {c["memberId"] for c in counterparties if c.get("type") == "employee"},
lambda u: f"{u['firstName']} {u['lastName']}", header)
cost_centers = {c["id"]: c["name"] for c in api("GET", "/v1/cost-centers", {"includeArchived": "true"}, company=header)["data"]}
# 4. One row per payable. Amounts are in cents: divide by 100.
for p, c in zip(payables, counterparties):
centers = {cost_centers.get(a["fieldEntityValueId"], a["fieldEntityValueId"])
for line in p.get("itemLines", []) for a in line.get("analyticalFieldAssociations", [])
if a["fieldKind"] == "costCenter"}
out.writerow([company_name or company_id or "", p["creationDate"][:10], p["subType"], p.get("description", ""),
suppliers.get(c.get("supplierId")) or employees.get(c.get("memberId")) or "",
p["amount"] / 100, p["currency"], p["functionalAmount"] / 100, p["functionalCurrency"],
"; ".join(sorted(centers)), p["state"], "yes" if p.get("documentaryEvidence") else "no"])
total += len(payables)
print(f"{company_name or 'Company'}: {len(payables)} payables")
print(f"{total} payables written to monthly_spend_{month}.csv")Good to know
- Which month an expense belongs to: the script selects expenses by their date in Spendesk (the
payableDatefilter). Thedatecolumn shows when the expense was created in Spendesk, which can differ by a few days. - Credit notes and refunds appear as their own lines.
- Employees who have left are not returned by Get Users, which lists active users by default: their column stays empty.
- Several companies: with an organisation-level key, the script covers every company of the organisation in one file — the
companycolumn tells them apart. SetSPENDESK_COMPANY_IDto export only one. - This export is for analysis. For accounting entries, use Exporting accounting data.
The request is Search Payables; Pagination explains nextCursor.
Last checked against the Spendesk demo environment on 29 September 2026.