Export a month of spend to a spreadsheet

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.

ColumnContent
dateWhen the expense was created in Spendesk
typeinvoice, creditNote, expense, singlePurchase, subscription, physical, mileageAllowance, perDiemAllowance…
descriptionThe description entered in Spendesk
counterpartyThe supplier, or the employee for an expense claim
amount, currencyIn the currency of the expense
amount_company_currency, company_currencyConverted to your company's currency
cost_centersThe cost centers of the expense lines
bookkeeping_statustoPrepare, toExport or exported
receiptWhether 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 payableDate filter). The date column 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.