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 |
|---|---|
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. 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; SPENDESK_COMPANY_ID with an organisation-level key.
Scopes: experimental:payable-search:read, supplier:read, user:read, cost-center:read.
"""
import base64, csv, datetime as dt, json, os, sys, urllib.parse, urllib.request
BASE = os.environ.get("SPENDESK_BASE_URL", "https://public-api.spendesk.com")
COMPANY = os.environ.get("SPENDESK_COMPANY_ID")
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):
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))
# 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)
# 2. Every payable of the month, 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)
payables += page["payables"]
cursor = page.get("nextCursor")
if not cursor:
break
# 3. Names for the IDs the payables carry: suppliers, employees, cost centers.
def names(path, ids, label):
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})["data"]:
found[item["id"]] = label(item)
return found
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"])
employees = names("/v1/users", {c["memberId"] for c in counterparties if c.get("type") == "employee"},
lambda u: f"{u['firstName']} {u['lastName']}")
cost_centers = {c["id"]: c["name"] for c in api("GET", "/v1/cost-centers", {"includeArchived": "true"})["data"]}
# 4. One row per payable. Amounts are in cents: divide by 100.
with open(f"monthly_spend_{month}.csv", "w", newline="") as f:
out = csv.writer(f)
out.writerow(["date", "type", "description", "counterparty", "amount", "currency",
"amount_company_currency", "company_currency", "cost_centers", "bookkeeping_status", "receipt"])
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([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"])
print(f"{len(payables)} 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, run the script once per company, changing
SPENDESK_COMPANY_ID. - This export is for analysis. For accounting entries, use Exporting accounting data.
The request is Search Payables; Pagination explains nextCursor.