Month-end close checklist

What is still missing before closing a month: receipts, exports to accounting, approvals, failed exports.

Before closing a month, check in one go what still needs a follow-up:

Month-end checklist for 2026-08

OK    Spend without a receipt: 0
CHECK Spend not exported to accounting yet: 3
OK    Invoices still waiting for approval: 0
OK    Invoices whose export failed: 0
OK    Declined card payments: 0
CheckWhat it looks for
Spend without a receiptExpenses of the month whose receipt or invoice is missing or declaredAsInvalid
Spend not exported to accounting yetExpenses of the month still toPrepare or toExport
Invoices still waiting for approvalSupplier invoices issued in the month, approval pending
Invoices whose export failedSupplier invoices issued in the month, export failed
Declined card paymentsCard payments declined during the month — nothing to book, but worth a look

Scopes: experimental:payable-search:read, experimental:invoice:read, experimental:transaction:read, and user:read to find your company ID with a company-level key. Set-up: Running the recipes.

With Postman

Each check is one request; send them one after the other.

  • Receipts and exports — Payables → Search Payables, with the month and one more filter:

    {
      "filters": {
        "operator": "and",
        "subfilters": [
          { "field": "payableDate", "operator": "between",
            "value": { "from": "2026-08-01T00:00:00.000Z", "to": "2026-08-31T23:59:59.999Z" } },
          { "field": "documentaryEvidenceStatus", "operator": "=", "value": ["missing", "declaredAsInvalid"] }
        ]
      },
      "limit": 100
    }

    For exports, replace the second filter with { "field": "bookkeepingStatus", "operator": "=", "value": ["toPrepare", "toExport"] }.

  • Invoices — Invoices and intakes → List invoices, with issuedAfter = 2026-08-01, issuedBefore = 2026-08-31, and approvalStatus = pending — or exportStatus = failed.

  • Declined card payments — Requests and transactions → Get Failed Transactions, with startDate = 2026-08-01 and endDate = 2026-08-31.

With an AI assistant

For August 2026: how many expenses are missing a receipt, how many are not exported to accounting yet, which supplier invoices are still waiting for approval, and did any invoice export fail?

With a script

Save it as month_end_checklist.py, then run python3 month_end_checklist.py 2026-08 — or with no month for last month.

"""Month-end close checklist: what is still missing before closing a month.

Usage: python3 month_end_checklist.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, experimental:invoice:read, experimental:transaction:read, user:read.
"""
import base64, 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))


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)
company = COMPANY or api("GET", "/v1/users", {"pageSize": 1})["data"][0]["companyId"]


def search(extra_filter):
    """Payables of the month matching one more filter, every page."""
    found, cursor = [], None
    while True:
        body = {"filters": {"operator": "and", "subfilters": [
            {"field": "payableDate", "operator": "between",
             "value": {"from": f"{first}T00:00:00.000Z", "to": f"{last}T23:59:59.999Z"}},
            extra_filter]}, "limit": 100}
        if cursor:
            body["cursor"] = cursor
        page = api("POST", "/v1/payables/search", body=body)
        found += page["payables"]
        cursor = page.get("nextCursor")
        if not cursor:
            return found


def invoices(query):
    """Invoices issued in the month matching the query, every page."""
    found, cursor = [], None
    while True:
        page = api("GET", f"/v2/companies/{company}/invoices",
                   dict(query, issuedAfter=str(first), issuedBefore=str(last), limit=100, **({"cursor": cursor} if cursor else {})))
        found += page["data"]
        cursor = page["meta"]["pagination"]["nextCursor"]
        if not cursor:
            return found


checks = {
    "Spend without a receipt": search({"field": "documentaryEvidenceStatus", "operator": "=", "value": ["missing", "declaredAsInvalid"]}),
    "Spend not exported to accounting yet": search({"field": "bookkeepingStatus", "operator": "=", "value": ["toPrepare", "toExport"]}),
    "Invoices still waiting for approval": invoices({"approvalStatus": ["pending"]}),
    "Invoices whose export failed": invoices({"exportStatus": ["failed"]}),
    "Declined card payments": api("GET", "/v1/transactions/failed", {"startDate": str(first), "endDate": str(last)})["data"],
}
print(f"Month-end checklist for {month}\n")
for label, items in checks.items():
    print(f"{'OK' if not items else 'CHECK':<6}{label}: {len(items)}")

Good to know

  • toPrepare means the expense is still being prepared in Spendesk (accounting fields, receipt); toExport means it is ready and waits for the export. See Exporting accounting data for the export itself.
  • Invoices are counted by issue date, expenses by their date in Spendesk: an invoice issued at the end of a month can be booked in the next one.
  • To see the detail behind a CHECK, print the list instead of its length — for example for p in checks["Spend without a receipt"]: print(p["id"], p.get("description")).
  • Several companies: with an organisation-level key, run it once per company, changing SPENDESK_COMPANY_ID.

The requests are Search Payables, List invoices and Get Failed Transactions.