---
name: commonshub-opendata
description: Fetch and analyse the open data of Commons Hub Brussels (finances, expenses line by line, vendors, customers, room bookings, events, VAT returns, integrity hashes) through the public read-only JSON API at https://commonshub.brussels/opendata. Use when asked about the Hub's money, suppliers, room use or events, or when building something on that data. No key needed. Licensed under ODbL (attribution, share-alike). GDPR-safe by construction - private individuals are never named.
---

# Commons Hub Brussels — open data

Commons Hub Brussels (Rue de la Madeleine 51, 1000 Brussels) runs its books in
the open. Every hour the `chb` CLI (https://github.com/CommonsHub/chb) pulls
the Hub's bank accounts, Stripe, Odoo accounting, on-chain wallets, room
calendars and event calendars, and publishes three versions of the result:
for stewards, for members, and for **everyone**. This API serves the one for
everyone.

- Base URL: `https://commonshub.brussels/opendata`
- Read-only, `GET` only, no key, no login. CORS is open (`*`), so a browser app can call it directly.
- JSON unless the file says otherwise (`.md`, `.csv`, `.ics`, images).
- Responses are cached for 5 minutes. The data itself refreshes hourly.
- Be gentle: cache what you download, do not poll more than once every few minutes.
- Licence: [Open Database License (ODbL) v1.0](https://opendatacommons.org/licenses/odbl/1-0/) — see [Licence](#licence) below.

## Endpoints

| URL | returns |
|---|---|
| `https://commonshub.brussels/opendata` | this skill (markdown) |
| `https://commonshub.brussels/opendata/index.json` | every year and month that has data, with links |
| `https://commonshub.brussels/opendata/monthly.json` | one row per month since the start: money in/out, expenses, invoiced income, bookings, events, door, members, tokens |
| `https://commonshub.brussels/opendata/{YYYY}/monthly.json` | the same, for one year |
| `https://commonshub.brussels/opendata/annotations.json` | known one-off events in the data (test mints, incidents, changes of method) and known bugs at the source |
| `https://commonshub.brussels/opendata/latest` | the files of the current state: newest month and lifetime rollups |
| `https://commonshub.brussels/opendata/{YYYY}` | the files of a whole year (rollups) |
| `https://commonshub.brussels/opendata/{YYYY}/{MM}` | the files of one month (`MM` is two digits: `2026/09`) |
| `https://commonshub.brussels/opendata/{period}/{file}` | one file, e.g. `https://commonshub.brussels/opendata/2026/09/expenses.json` |
| `https://commonshub.brussels/opendata/{YYYY}/{MM}/events/images/{file}` | an event cover image |

A listing answers `{ "period": "2026/09", "files": [{ "file", "href", "bytes", "description" }] }`
and only shows files that exist for that period. A **404 means "nothing for that
period"** (a month without vendor bills has no `expenses.json`), or a path that is
not part of the open dataset.

## Files

Scope: **M** = month (`/{YYYY}/{MM}/`), **Y** = year (`/{YYYY}/`), **L** = `/latest/`.

| file | scope | what |
|---|---|---|
| `transactions.json` | M, L | Every transaction: amount, direction, account, category, collective. No counterparty, no bank narration. |
| `counterparties.json` | M, L | The Hub's own accounts. |
| `summary.json` | M, L | Totals per account, collective and category. |
| `commissions.json` | M | The monthly fiscal-host commission each collective pays the Hub. |
| `inbound_spreads.json` | M | Transactions spread over several months: the share landing in this month. No counterparty. |
| `activitygrid.json` | Y, L | Contributors and photos per month (counts only). |
| `members.json` | M, L | Membership summary: counts and totals only, no member list. |
| `door.json` | M, L | Door openings: counts only. |
| `events.json` | M, Y, L | Events as published on the public calendar: name, times, place, host, cover, url. |
| `events.csv` | Y | One year of events, one row each (no attendance or income columns). |
| `events.md` | L | Upcoming events, as markdown. |
| `rooms.md` | L | The rooms and their prices, as markdown. |
| `calendars/public.ics` | M | Room bookings feed (no person names). |
| `expenses.json` | M, Y | Every vendor bill, credit note and expense claim, line by line. Organisations and sole traders named; individuals anonymous. |
| `vendors.json` | M, Y | One row per vendor with totals; individuals merged per category. |
| `customers.json` | M, Y | One row per customer with totals; only organisations named. |
| `bookings.json` | M, Y | Room occupancy (room, start, end), room-rental revenue per room. |
| `pending-bills.json` | L | Bills the Hub still has to pay (the "help us pay" list). |
| `hashes.json` | M, L | Integrity manifest: per-provider counts and content hashes. |
| `vat.json` | Y, L | Quarterly VAT declarations filed with the Belgian State. |

## Quick start

```bash
# What exists?
curl -s https://commonshub.brussels/opendata/index.json | jq '.periods[] | {year, months: [.months[].month]}'

# The Hub's own euro cash flow, month by month (one request)
curl -s https://commonshub.brussels/opendata/monthly.json | jq -r '.months[] | [.month, .money.byCollective.commonshub.in, .money.byCollective.commonshub.out] | @tsv'

# Who did the Hub pay in 2026, and how much? (year rollup, largest first)
curl -s https://commonshub.brussels/opendata/2026/vendors.json | jq -r '.vendors[] | [(.vendor.name // "(\(.individuals) individuals)"), .category, .totalAmount] | @tsv'

# What was bought, line by line, in September 2026?
curl -s https://commonshub.brussels/opendata/2026/09/expenses.json | jq -r '.expenses[] | .vendor.name as $v | .lines[] | [.product // .description, .quantity, .totalAmount, ($v // "individual")] | @tsv'

# Bills still to pay (the "help us pay" list)
curl -s https://commonshub.brussels/opendata/latest/pending-bills.json | jq '.totals, [.bills[] | {number, vendor: .vendor.name, amountDue, dueDate}]'

# Upcoming events
curl -s https://commonshub.brussels/opendata/latest/events.md
```

```python
import requests
API = "https://commonshub.brussels/opendata"
idx = requests.get(f"{API}/index.json", timeout=30).json()
for y in idx["periods"]:
    for m in y["months"]:
        r = requests.get(f"{API}/{y['year']}/{m['month']}/summary.json", timeout=30)
        if r.ok:
            print(y["year"], m["month"], r.json().keys())
```

```js
const API = "https://commonshub.brussels/opendata";
const { bookings, rooms } = await fetch(`${API}/2026/bookings.json`).then(r => r.json());
```

## Conventions

- **Money** is in euros unless `currency` says otherwise. Accounting files (`expenses`, `vendors`,
  `customers`, `bookings`) carry `currency: "EUR"` and convert foreign documents
  (`totalAmountEUR`). Credit notes and refunds subtract, so a month, a category or an income type can
  have a negative total: a correction, not negative revenue.
- **Time**: dates are `YYYY-MM-DD`; timestamps are RFC 3339 with an explicit offset; the Hub's
  timezone is `Europe/Brussels`. `transactions[].timestamp` is Unix seconds. All-day events have no clock time.
- Every file has `generatedAt`. Accounting files also have `scope` (`month` | `year`) and `period` (`2026-09` | `2026`).
- **Ids are stable**: a bill keeps the same `id` (`b-…`) in `expenses.json` and `pending-bills.json`;
  an organisation keeps the same `vendor.id` / `customer.id` (`p-…`) across months.
  Transaction ids are NIP-73 URIs (`stripe:txn_…`, `ethereum:42220:tx:0x…`, `iban:…:tx:…`).
- Accounting files are per month and per year only, never in `/latest/`. `pending-bills.json` is only in `/latest/`.

## The main files

### `expenses.json` (M, Y) — what the Hub spends, line by line

`totals` (count, untaxedAmount, totalAmount, paidAmount, amountDue), `byCategory[]`, and `expenses[]`:
`id`, `number` (our accounting number), `kind` (`bill` | `credit_note` | `expense` = someone reimbursed),
`status` (`pending` | `partially_paid` | `paid` | `reversed` | `submitted`), `date`, `dueDate`,
`vendor` (`{ id, type, name, vat }`, see below), `description`, `category`, `collective`, `event`,
amounts, `hasDocument`, and `lines[]` (`description`, `product`, `quantity`, `unitPrice`,
`untaxedAmount`, `totalAmount`, `vatRate`, `account: { code, class }`).
`reversed` documents are excluded from totals.

### `vendors.json` / `customers.json` (M, Y) — who the Hub pays, who pays the Hub

One row per vendor (`category`, `documents`, `totalAmount`, `paidAmount`, `amountDue`) or per customer
(`incomeType`: `membership` | `room_rental` | `tickets_events` | `sponsorship` | `donation` |
`reinvoiced_costs` | `sales_services` | `other_income` | `other`; `products`, `invoices`, `receivedAmount`).
Private individuals are merged into one anonymous row per category / income type with a count
(`individuals`). Only invoiced income is in `customers.json`; card payments without an invoice (most
event tickets) are in `transactions.json`.

### `bookings.json` (M, Y) — how the rooms are used

`rooms[]` per room: `bookings`, `hours`, `publicBookings`, `rentalLines`, `rentalRevenue` (untaxed).
`bookings[]`: `room`, `start`, `end`, `hours`, `public`; a `title` and `eventUrl` only when the
booking hosts a public event. `rentals[]`: room-rental invoice lines (`date` = invoice date, `room`,
`product`, amounts, `customer` named only when it is an organisation). Invoices and bookings are
deliberately **not** linked one to one. The year file adds `months[]` for charts.
Occupancy has been recorded this way only recently (see coverage below). For earlier months,
`summary.json` `summary.bookings` counts entries in the room calendars: a different measure, do not
join the two into one series.

### `transactions.json` (M, L) — every movement of money

`transactions[]`: `id`, `provider` (`stripe`, `etherscan`, `kbcbrussels`, …), `accountSlug`, `accountName`,
`currency`, `amount`, `normalizedAmount`, `grossAmount`, `fee`, `type` (`CREDIT` | `DEBIT`, and
`MINT` | `BURN` for community tokens), `timestamp`, `event` (when a ticket sale belongs to an event),
`metadata.category`, `metadata.collective`, `metadata.description` (only labels we wrote, never the
bank's narration). `counterpartyId` is present only when it names nobody (a blockchain address, one of
the Hub's own accounts).

### `monthly.json` — the time series

Computed on request from the month files. `months[]`, oldest first, one row per month: `month`
(`YYYY-MM`), `status` (`closed` | `current` | `future`), `money` (`eur`, `byCurrency`, `byCollective`, each `{ in, out, net, transactions }`),
`activity`, `expenses` and `invoicedIncome` (the `totals` of `expenses.json` / `customers.json`),
`bookings`, `door`, `members`, `tokens`. A section is `null` when the month has no such file.
`fields` explains each section. `money` is summed from `transactions.json`: euro-denominated rows
only (EUR, EURe, EURb), `CREDIT`/`DEBIT`/`MINT`/`BURN` only (moves between the Hub's own accounts are
left out), Stripe net of fees. **Prefer it over `summary.json` for money** (see known bugs below).
Each row has `notes[]` (the annotations of that month), and the file has `coverage`: from when each
section holds data. `future` months exist because room calendars are booked ahead; they hold calendar
entries only.

### `summary.json` (M, L) — aggregates

Per account, collective and category for a month; `/latest/summary.json` is the lifetime rollup per
collective (`firstMonth`, `lastMonth`, `collectives[]` with per-currency totals and balances).
Read the known bugs below before using its amounts or balances.

### `events.json` (M, Y, L), `events.csv` (Y), `events.md` (L)

`events[]`: `id`, `name`, `description`, `startAt`, `endAt`, `allDay`, `location`, `url`, `coverImage`,
`source`, `tags`, `metadata.host` — events exactly as their organisers published them on the public
calendar. No guest lists, no attendance, no ticket revenue. Many gatherings at the Hub are not on the
public calendar, so a low count does not mean little happened.

### `pending-bills.json` (L) — the "help us pay" list

Every vendor bill still open: `totals` (count, totalAmount, amountDue), `bills[]` with `id`, `number`,
`status`, `date`, `dueDate`, `vendor: { type: "business" | "individual", name, vat }`, `lines[]`,
`amountDue`. To help pay one, people donate to the Hub with the bill `number` as reference
(https://commonshub.brussels/donate) — never to the vendor directly.

### `hashes.json` (M, L) and `vat.json` (Y, L)

`hashes.json` is the integrity manifest of a month: per data source, counts and a sha256 over the raw
archives, plus hashes of the public and members trees. `/latest/hashes.json` indexes every month and
carries one top-level `hash` for the whole dataset. Use it to prove two copies are the same.
`vat.json` lists every quarterly VAT return filed with the Belgian State (Intervat): every grid of the
official form, control totals (`outputVat`, `inputVat`, `net`), and corrections (`filings[]`).
`gridLabels` names the grids.

## Privacy: what you will and will not find

This dataset is designed to be as transparent as possible **and** to respect the GDPR. The
filtering happens before a file is written, so the data simply is not there.

- **Organisations are named** (companies, associations, public bodies) as vendors and customers,
  with their VAT number and what they sold or bought. That is how you can follow the money.
- **Sole traders** (a person registered for VAT) are named as vendors with their VAT number,
  which is public in the Belgian company register, and the **products** they sold — but not the
  free text of their invoices, their invoice reference, or the event a bill is tagged with.
- **Private individuals are never named.** They appear only by type
  (`{"type": "individual"}`, sometimes `"member": true`), merged per category, with no id, so nobody
  can be followed from month to month.
- **No invoice links a person to an event.** Bills from individuals and sole traders lose their
  event tag and free text; room-rental invoice lines show the room and the invoice date, never who
  rented it unless it is an organisation; bookings show a title only for public events.
- **Never published**: emails, phone numbers, addresses, IBANs and BICs of third parties,
  Stripe/Odoo ids, payroll details (payroll appears as "Payroll" with an amount), bank narrations,
  attendee lists, door-opening dates, member lists, Discord profiles and photos.
- Event hosts (`metadata.host`) are shown as the organisers published them on the public calendar.

**Ground rules for reuse**

1. Do not try to re-identify anonymous rows, and do not join this data with other sources to
   attach a name, a date or a place to a private individual.
2. If you find personal data that should not be here, stop using it and tell
   hello@commonshub.brussels: it will be removed at the source.
3. Respect the licence (below).

## Licence

The dataset is published under the **Open Database License (ODbL) v1.0**: https://opendatacommons.org/licenses/odbl/1-0/
(`license` in `index.json`, and a `Link: <…>; rel="license"` header on every response).

- **Attribute.** Wherever you use or show the data, credit it. For example:
  > Contains data from Commons Hub Brussels, available under the Open Database License (ODbL): https://commonshub.brussels/opendata

  Keep the `generatedAt` of the files you used, so others can reproduce your result.
- **Share alike.** If you publicly use a database derived from this one (data you combined,
  cleaned, enriched or restructured), you must offer that derived database under the ODbL too,
  or the changes needed to rebuild it. Public use includes a public website, app or API built on it.
- **Produced works are yours.** Charts, articles, slides and apps made from the data may use any
  licence, as long as they carry the attribution notice above.
- **Keep it open.** Do not add technical restrictions (DRM) to copies you distribute without also
  offering an unrestricted copy.
- Private analysis owes nothing back. Sharing your derived data with the Hub is always welcome:
  hello@commonshub.brussels.

The ODbL covers the database. It does not lift the ground rules above: personal-data protection
(GDPR) applies to anyone who processes the data, whatever the licence.

## Data quality

The data is published as chb produces it. Work around what follows rather than reporting it as a finding.

### Coverage

From when each section of `monthly.json` holds data, over closed months (also `coverage` in that
file). Before "every month since", a 0 may mean "not recorded" rather than "none": do not chart it as
a drop. Events are sparse by nature (see `events.json`), so their gaps are real.

| section | first month with data | every month since | empty months since first |
|---|---|---|---|
| money | 2024-01 | 2024-01 | 0 |
| expenses | 2024-01 | 2024-11 | 1 |
| invoicedIncome | 2024-09 | 2024-12 | 2 |
| bookings (bookings.json) | 2024-09 | 2025-12 | 8 |
| room calendar entries (activity.bookings) | 2024-06 | 2024-06 | 0 |
| events (activity.events) | 2024-06 | 2026-05 | 2 |
| contributors (activity.contributors) | 2024-09 | 2024-09 | 0 |
| door | 2026-08 | 2026-08 | 0 |
| members | never | – | 0 |
| tokens (CHT) | 2024-06 | 2024-06 | 0 |

### Annotations

One-off events in the data (`incident` = a real event that distorts totals: mention it rather than averaging over it; `test` = not real activity: exclude it; `method` = how the data is recorded changed: do not compare across it naively). Machine-readable at `https://commonshub.brussels/opendata/annotations.json`,
and on the matching rows of `monthly.json` as `notes[]`.

- **2025-01**, `tokens.CHT` (test): Test mint of about 10⁶ CHT, burnt again the same month: supply is right, minted and burnt are inflated.
- **2025-09**, `tokens.CHT` (test): Test mint of about 2×10¹² CHT, burnt again the same month: supply is right, minted and burnt are inflated.
- **2026-01**, `expenses` (method): Expense categories switch from Odoo account classes ("Services and other goods", "Balance sheet") to custom tags ("furniture", "cold-drinks"); both can appear in one month. Compare years with lines[].account.code, not category.
- **2026-05**, `money` (incident): One outflow of about €110k EURe from the account 202605-savings-hacked: a real loss, which dominates any 2026 total.

### Known bugs at the source

- **summary.json counts transfers between the Hub's own accounts as income and spending**. collectives[] includes INTERNAL rows (savings → checking, Stripe payouts), so /latest/summary.json gives commonshub a lifetime EUR "end balance" of hundreds of thousands of euros. It is not a bank balance. Workaround: Use monthly.json money, or sum transactions.json without INTERNAL and TRANSFER rows. Real balances: /finance.
- **summary.json accounts[] and currencies[] miss on-chain euros**. EURe movements on Gnosis (Monerium mints and burns, which carry rent, catering and many incoming payments) show in: 0, out: 0. Workaround: Use monthly.json money, which counts them.

## Good to know

- Odoo is pulled hourly, but a bill stays `pending` until it is reconciled with its payment, so
  utilities paid by direct debit can look unpaid for a while.
- Treat a missing file as "nothing that period", not as an error.
- A vendor that shows as anonymous but is a company is fixed in Odoo (mark it as a company, add
  its VAT number); it reappears by name at the next hourly run.
- The schemas and the reasoning behind each field are documented in the chb repository:
  https://github.com/CommonsHub/chb/blob/main/docs/website.md and
  https://github.com/CommonsHub/chb/blob/main/docs/accounting-data.md
- Other entry points: https://commonshub.brussels/llms.txt (the site for agents), https://commonshub.brussels/events.md,
  https://commonshub.brussels/rooms.md, https://commonshub.brussels/finance.md, https://commonshub.brussels/economy.md, https://commonshub.brussels/community.md
  (month-by-month summaries), https://commonshub.brussels/finance (the human view of the same data).
- `/data/` and `/api/` on the website are internal and may change or require a login: use `/opendata`.
