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| Check | What it looks for |
|---|---|
| Spend without a receipt | Expenses of the month whose receipt or invoice is missing or declaredAsInvalid |
| Spend not exported to accounting yet | Expenses of the month still toPrepare or toExport |
| Invoices still waiting for approval | Supplier invoices issued in the month, approval pending |
| Invoices whose export failed | Supplier invoices issued in the month, export failed |
| Declined card payments | Card 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, andapprovalStatus=pending— orexportStatus=failed. -
Declined card payments — Requests and transactions → Get Failed Transactions, with
startDate=2026-08-01andendDate=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
toPreparemeans the expense is still being prepared in Spendesk (accounting fields, receipt);toExportmeans 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 examplefor 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.