diff --git a/.gitignore b/.gitignore
index e9e52a7..2935f83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -161,6 +161,8 @@ cython_debug/
 #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
 .idea/
 
+# VSC
+.vscode/
 ########################################################################################
 
 # Project specific
diff --git a/README.md b/README.md
index 43342d3..3b5308a 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,12 @@ Get all your messages.
 Send a message with content `text`.
 Either specify an existing `chat_id`, or both `user` and `group_uid` for a new chat.
 
+### get_event_attendance_xlsx()
+Get Excel attendance report for a single event, available via the web client.
+
+### change_response()
+Change a member's response for an event (e.g. accept/decline)
+
 ## Example scripts
 
 The following scripts are included as examples.  Some of the scripts might require additional packages to be installed (csv, ical etc).
@@ -72,6 +78,9 @@ Generates a json-file for each group you are a member of.
 ### attendance.py <-f from_date> <-t to_date> [-a]
 Generates a csv-file for each event between `from_date` and `to_date` with attendance status of all organizers.  The optional parameter `-a` also includes all members that has been invited.
 
+### transactions.py
+Generates a csv-file for transactions / payments appeared in [Spond Club](https://www.spond.com/spond-club-overview/) > Finance > Payments.
+
 ## AsyncIO
 [Asyncio](https://docs.python.org/3/library/asyncio.html) might seem intimidating in the beginning, but for basic stuff, it is quite easy to follow the examples above, and just remeber to prefix functions that use the API with `async def ...` and to `await` all API-calls and all calls to said functions.
 
diff --git a/config.py.sample b/config.py.sample
index 2ed0501..733f9e5 100644
--- a/config.py.sample
+++ b/config.py.sample
@@ -1,2 +1,3 @@
 username = 'user@name.invalid'
 password = 'Pa55w0rd'
+club_id = '1234567890'
diff --git a/manual_test_functions.py b/manual_test_functions.py
index 4285d72..c76dfc6 100644
--- a/manual_test_functions.py
+++ b/manual_test_functions.py
@@ -6,9 +6,10 @@
 Doesn't yet use `get_person(user)` or any `send_`, `update_` methods."""
 
 import asyncio
+import tempfile
 
-from config import password, username
-from spond import spond
+from config import club_id, password, username
+from spond import club, spond
 
 DUMMY_ID = "DUMMY_ID"
 
@@ -40,8 +41,28 @@ async def main() -> None:
     for i, message in enumerate(messages):
         print(f"[{i}] {_message_summary(message)}")
 
+    # ATTENDANCE EXPORT
+
+    print("\nGetting attendance report for the first event...")
+    e = events[0]
+    data = await s.get_event_attendance_xlsx(e["id"])
+    with tempfile.NamedTemporaryFile(
+        mode="wb", suffix=".xlsx", delete=False
+    ) as temp_file:
+        temp_file.write(data)
+        print(f"Check out {temp_file.name}")
+
     await s.clientsession.close()
 
+    # SPOND CLUB
+    sc = club.SpondClub(username=username, password=password)
+    print("\nGetting up to 10 transactions...")
+    transactions = await sc.get_transactions(club_id=club_id, max_items=10)
+    print(f"{len(transactions)} transactions:")
+    for i, t in enumerate(transactions):
+        print(f"[{i}] {_transaction_summary(t)}")
+    await sc.clientsession.close()
+
 
 def _group_summary(group) -> str:
     return f"id='{group['id']}', " f"name='{group['name']}'"
@@ -63,6 +84,15 @@ def _message_summary(message) -> str:
     )
 
 
+def _transaction_summary(transaction) -> str:
+    return (
+        f"id='{transaction['id']}', "
+        f"timestamp='{transaction['paidAt']}', "
+        f"payment_name='{transaction['paymentName']}', "
+        f"name={transaction['paidByName']}"
+    )
+
+
 def _abbreviate(text, length) -> str:
     """Abbreviate long text, normalising line endings to escape characters."""
     escaped_text = repr(text)
diff --git a/spond/base.py b/spond/base.py
new file mode 100644
index 0000000..020b1f8
--- /dev/null
+++ b/spond/base.py
@@ -0,0 +1,45 @@
+from abc import ABC
+
+import aiohttp
+
+
+class AuthenticationError(Exception):
+    pass
+
+
+class _SpondBase(ABC):
+    def __init__(self, username, password, api_url):
+        self.username = username
+        self.password = password
+        self.api_url = api_url
+        self.clientsession = aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar())
+        self.token = None
+
+    @property
+    def auth_headers(self):
+        return {
+            "content-type": "application/json",
+            "Authorization": f"Bearer {self.token}",
+        }
+
+    def require_authentication(func: callable):
+        async def wrapper(self, *args, **kwargs):
+            if not self.token:
+                try:
+                    await self.login()
+                except AuthenticationError as e:
+                    await self.clientsession.close()
+                    raise e
+            return await func(self, *args, **kwargs)
+
+        return wrapper
+
+    async def login(self):
+        login_url = f"{self.api_url}login"
+        data = {"email": self.username, "password": self.password}
+        async with self.clientsession.post(login_url, json=data) as r:
+            login_result = await r.json()
+            self.token = login_result.get("loginToken")
+            if self.token is None:
+                err_msg = f"Login failed. Response received: {login_result}"
+                raise AuthenticationError(err_msg)
diff --git a/spond/club.py b/spond/club.py
new file mode 100644
index 0000000..6e516e6
--- /dev/null
+++ b/spond/club.py
@@ -0,0 +1,58 @@
+from typing import Optional
+
+from .base import _SpondBase
+
+
+class SpondClub(_SpondBase):
+    def __init__(self, username, password):
+        super().__init__(username, password, "https://api.spond.com/club/v1/")
+        self.transactions = None
+
+    @_SpondBase.require_authentication
+    async def get_transactions(
+        self, club_id: str, skip: Optional[int] = None, max_items: int = 100
+    ):
+        """
+        Retrieves a list of transactions/payments for a specified club.
+
+        Parameters
+        ----------
+        club_id : str
+            Identifier for the club. Note that this is different from the Group ID used
+            in the core API.
+        max_items : int, optional
+            The maximum number of transactions to retrieve. Defaults to 100.
+        skip : int, optional
+            This endpoint only returns 25 transactions at a time (page scrolling).
+            Therefore, we need to increment this `skip` param to grab the next
+            25 etc. Defaults to None. It's better to keep `skip` at None
+            and specify `max_items` instead. This param is only here for the
+            recursion implementation
+
+        Returns
+        -------
+        list of dict
+            A list of transactions, each represented as a dictionary.
+        """
+        if self.transactions is None:
+            self.transactions = []
+
+        url = f"{self.api_url}transactions"
+        params = None if skip is None else {"skip": skip}
+        headers = {**self.auth_headers, "X-Spond-Clubid": club_id}
+
+        async with self.clientsession.get(url, headers=headers, params=params) as r:
+            if r.status == 200:
+                t = await r.json()
+                if len(t) == 0:
+                    return self.transactions
+
+                self.transactions.extend(t)
+                if len(self.transactions) < max_items:
+                    return await self.get_transactions(
+                        club_id=club_id,
+                        skip=len(t) if skip is None else skip + len(t),
+                        max_items=max_items,
+                    )
+
+        return self.transactions
diff --git a/spond/spond.py b/spond/spond.py
index 7c493c4..dba9202 100644
--- a/spond/spond.py
+++ b/spond/spond.py
@@ -1,74 +1,32 @@
 #!/usr/bin/env python3
-
 from datetime import datetime
 from typing import TYPE_CHECKING, List, Optional
 
-import aiohttp
+from .base import _SpondBase
 
 if TYPE_CHECKING:
     from datetime import datetime
 
 
-class AuthenticationError(Exception):
-    pass
-
-
-class Spond:
+class Spond(_SpondBase):
 
-    API_BASE_URL = "https://api.spond.com/core/v1/"
     DT_FORMAT = "%Y-%m-%dT00:00:00.000Z"
 
     def __init__(self, username, password):
-        self.username = username
-        self.password = password
-        self.clientsession = aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar())
+        super().__init__(username, password, "https://api.spond.com/core/v1/")
         self.chat_url = None
         self.auth = None
-        self.token = None
         self.groups = None
         self.events = None
 
-    @property
-    def auth_headers(self):
-        return {
-            "content-type": "application/json",
-            "Authorization": f"Bearer {self.token}",
-            "auth": f"{self.auth}",
-        }
-
-    async def login(self):
-        login_url = f"{self.API_BASE_URL}login"
-        data = {"email": self.username, "password": self.password}
-        async with self.clientsession.post(login_url, json=data) as r:
-            login_result = await r.json()
-            self.token = login_result.get("loginToken", None)
-            if self.token is None:
-                err_msg = f"Login failed. Response received: {login_result}"
-                raise AuthenticationError(err_msg)
-
-        api_chat_url = f"{self.API_BASE_URL}chat"
-        headers = {
-            "content-type": "application/json",
-            "Authorization": f"Bearer {self.token}",
-        }
-        r = await self.clientsession.post(api_chat_url, headers=headers)
+    async def login_chat(self):
+        api_chat_url = f"{self.api_url}chat"
+        r = await self.clientsession.post(api_chat_url, headers=self.auth_headers)
         result = await r.json()
         self.chat_url = result["url"]
         self.auth = result["auth"]
 
-    def require_authentication(func: callable):
-        async def wrapper(self, *args, **kwargs):
-            if not self.token:
-                try:
-                    await self.login()
-                except AuthenticationError as e:
-                    await self.clientsession.close()
-                    raise e
-            return await func(self, *args, **kwargs)
-
-        return wrapper
-
-    @require_authentication
+    @_SpondBase.require_authentication
     async def get_groups(self):
         """
         Get all groups.
@@ -79,12 +37,12 @@ async def get_groups(self):
         list of dict
             Groups; each group is a dict.
         """
-        url = f"{self.API_BASE_URL}groups/"
+        url = f"{self.api_url}groups/"
         async with self.clientsession.get(url, headers=self.auth_headers) as r:
             self.groups = await r.json()
             return self.groups
 
-    @require_authentication
+    @_SpondBase.require_authentication
     async def get_group(self, uid) -> dict:
         """
         Get a group by unique ID.
@@ -113,7 +71,7 @@ async def get_group(self, uid) -> dict:
         errmsg = f"No group with id='{uid}'"
         raise IndexError(errmsg)
 
-    @require_authentication
+    @_SpondBase.require_authentication
     async def get_person(self, user) -> dict:
         """
         Get a member or guardian by matching various identifiers.
@@ -155,14 +113,15 @@ async def get_person(self, user) -> dict:
                             return guardian
         raise IndexError
 
-    @require_authentication
+    @_SpondBase.require_authentication
     async def get_messages(self):
+        if not self.auth:
+            await self.login_chat()
         url = f"{self.chat_url}/chats/?max=10"
-        headers = {"auth": self.auth}
-        async with self.clientsession.get(url, headers=headers) as r:
+        async with self.clientsession.get(url, headers={"auth": self.auth}) as r:
             return await r.json()
 
-    @require_authentication
+    @_SpondBase.require_authentication
     async def _continue_chat(self, chat_id, text):
         """
         Send a given text in an existing given chat.
@@ -181,13 +140,14 @@ async def _continue_chat(self, chat_id, text):
         dict
              Result of the sending.
         """
+        if not self.auth:
+            await self.login_chat()
         url = f"{self.chat_url}/messages"
         data = {"chatId": chat_id, "text": text, "type": "TEXT"}
-        headers = {"auth": self.auth}
-        r = await self.clientsession.post(url, json=data, headers=headers)
+        r = await self.clientsession.post(url, json=data, headers={"auth": self.auth})
         return await r.json()
 
-    @require_authentication
+    @_SpondBase.require_authentication
     async def send_message(self, text, user=None, group_uid=None, chat_id=None):
         """
         Start a new chat or continue an existing one.
@@ -212,6 +172,8 @@ async def send_message(self, text, user=None, group_uid=None, chat_id=None):
         dict
              Result of the sending.
         """
+        if self.auth is None:
+            await self.login_chat()
 
         if chat_id is not None:
             return self._continue_chat(chat_id, text)
@@ -232,11 +194,10 @@ async def send_message(self, text, user=None, group_uid=None, chat_id=None):
             "recipient": user_uid,
             "groupId": group_uid,
         }
-        headers = {"auth": self.auth}
-        r = await self.clientsession.post(url, json=data, headers=headers)
+        r = await self.clientsession.post(url, json=data, headers={"auth": self.auth})
         return await r.json()
 
-    @require_authentication
+    @_SpondBase.require_authentication
     async def get_events(
         self,
         group_id: Optional[str] = None,
@@ -289,7 +250,7 @@ async def get_events(
         list of dict
             Events; each event is a dict.
         """
-        url = f"{self.API_BASE_URL}sponds/"
+        url = f"{self.api_url}sponds/"
         params = {
             "max": str(max_events),
             "scheduled": str(include_scheduled),
@@ -313,7 +274,7 @@ async def get_events(
             self.events = await r.json()
             return self.events
 
-    @require_authentication
+    @_SpondBase.require_authentication
     async def get_event(self, uid) -> dict:
         """
         Get an event by unique ID.
@@ -341,7 +302,7 @@ async def get_event(self, uid) -> dict:
         errmsg = f"No event with id='{uid}'"
         raise IndexError(errmsg)
 
-    @require_authentication
+    @_SpondBase.require_authentication
     async def update_event(self, uid, updates: dict):
         """
         Updates an existing event.
@@ -364,7 +325,7 @@ async def update_event(self, uid, updates: dict):
             if event["id"] == uid:
                 break
 
-        url = f"{self.API_BASE_URL}sponds/{uid}"
+        url = f"{self.api_url}sponds/{uid}"
 
         base_event = {
             "heading": None,
@@ -417,3 +378,46 @@ async def update_event(self, uid, updates: dict):
         ) as r:
             self.events_update = await r.json()
             return self.events
+
+    @_SpondBase.require_authentication
+    async def get_event_attendance_xlsx(self, uid: str) -> bytes:
+        """get Excel attendance report for a single event.
+           Available via the web client.
+
+        Parameters
+        ----------
+        uid : str
+            UID of the event.
+
+        Returns:
+            bytes: XLSX binary data
+        """
+        url = f"{self.api_url}sponds/{uid}/export"
+        async with self.clientsession.get(url, headers=self.auth_headers) as r:
+            output_data = await r.read()
+            return output_data
+
+    @_SpondBase.require_authentication
+    async def change_response(self, uid: str, user: str, payload: dict) -> dict:
+        """change a user's response for an event
+
+        Parameters
+        ----------
+        uid : str
+            UID of the event.
+
+        user : str
+            UID of the user
+
+        payload : dict
+            user response to event, e.g. {"accepted": "true"}
+
+        Returns
+        ----------
+            json: event["responses"] with updated info
+        """
+        url = f"{self.api_url}sponds/{uid}/responses/{user}"
+        async with self.clientsession.put(
+            url, headers=self.auth_headers, json=payload
+        ) as r:
+            return await r.json()
diff --git a/tests/test_spond.py b/tests/test_spond.py
index 1b606a4..0ffdaee 100644
--- a/tests/test_spond.py
+++ b/tests/test_spond.py
@@ -1,11 +1,15 @@
 """Test suite for Spond class."""
 
+from unittest.mock import AsyncMock, patch
+
 import pytest
 
+from spond.base import _SpondBase
 from spond.spond import Spond
 
 MOCK_USERNAME, MOCK_PASSWORD = "MOCK_USERNAME", "MOCK_PASSWORD"
 MOCK_TOKEN = "MOCK_TOKEN"
+MOCK_PAYLOAD = {"accepted": "false", "declineMessage": "sick cannot make it"}
 
 
 # Mock the `require_authentication` decorator to bypass authentication
@@ -16,7 +20,7 @@ async def wrapper(*args, **kwargs):
     return wrapper
 
 
-Spond.require_authentication = mock_require_authentication(Spond.get_event)
+_SpondBase.require_authentication = mock_require_authentication(Spond.get_event)
 
 
 @pytest.fixture
@@ -54,6 +58,11 @@ def mock_token():
     return MOCK_TOKEN
 
 
+@pytest.fixture
+def mock_payload():
+    return MOCK_PAYLOAD
+
+
 @pytest.mark.asyncio
 async def test_get_event__happy_path(mock_events, mock_token):
     """Test that a valid `id` returns the matching event."""
@@ -130,3 +139,61 @@ async def test_get_group__blank_id_raises_exception(mock_groups, mock_token):
 
     with pytest.raises(IndexError):
         await s.get_group("")
+
+
+@pytest.mark.asyncio
+@patch("aiohttp.ClientSession.get")
+async def test_get_export(mock_get, mock_token):
+    s = Spond(MOCK_USERNAME, MOCK_PASSWORD)
+    s.token = mock_token
+
+    mock_binary = b"\x68\x65\x6c\x6c\x6f\x77\x6f\x72\x6c\x64"  # helloworld
+    mock_get.return_value.__aenter__.return_value.status = 200
+    mock_get.return_value.__aenter__.return_value.read = AsyncMock(
+        return_value=mock_binary
+    )
+
+    data = await s.get_event_attendance_xlsx(uid="ID1")
+
+    mock_url = "https://api.spond.com/core/v1/sponds/ID1/export"
+    mock_get.assert_called_once_with(
+        mock_url,
+        headers={
+            "content-type": "application/json",
+            "Authorization": f"Bearer {mock_token}",
+        },
+    )
+    assert data == mock_binary
+
+
+@pytest.mark.asyncio
+@patch("aiohttp.ClientSession.put")
+async def test_change_response(mock_put, mock_payload, mock_token):
+    s = Spond(MOCK_USERNAME, MOCK_PASSWORD)
+    s.token = mock_token
+
+    mock_response_data = {
+        "acceptedIds": ["PID1", "PID2"],
+        "declinedIds": ["PID3"],
+        "unansweredIds": [],
+        "waitinglistIds": [],
+        "unconfirmedIds": [],
+        "declineMessages": {"PID3": "sick cannot make it"},
+    }
+    mock_put.return_value.__aenter__.return_value.status = 200
+    mock_put.return_value.__aenter__.return_value.json = AsyncMock(
+        return_value=mock_response_data
+    )
+
+    response = await s.change_response(uid="ID1", user="PID3", payload=mock_payload)
+
+    mock_url = "https://api.spond.com/core/v1/sponds/ID1/responses/PID3"
+    mock_put.assert_called_once_with(
+        mock_url,
+        headers={
+            "content-type": "application/json",
+            "Authorization": f"Bearer {mock_token}",
+        },
+        json=mock_payload,
+    )
+    assert response == mock_response_data
diff --git a/transactions.py b/transactions.py
new file mode 100644
index 0000000..0c63781
--- /dev/null
+++ b/transactions.py
@@ -0,0 +1,48 @@
+import argparse
+import asyncio
+import csv
+from pathlib import Path
+
+from config import club_id, password, username
+from spond.club import SpondClub
+
+parser = argparse.ArgumentParser(
+    description="Creates an transactions.csv to keep track of payments accessible on Spond Club"
+)
+parser.add_argument(
+    "-m",
+    "--max",
+    help="The max number of transactions to query for",
+    type=int,
+    dest="max",
+    default=1000,
+)
+
+args = parser.parse_args()
+
+
+async def main():
+    output_path = Path("./exports/transactions.csv")
+
+    s = SpondClub(username=username, password=password)
+    transactions = await s.get_transactions(club_id=club_id, max_items=args.max)
+    if not transactions:
+        print("No transactions found.")
+        await s.clientsession.close()
+        return
+
+    header = transactions[0].keys()
+
+    with open(output_path, "w", newline="") as file:
+        writer = csv.DictWriter(file, fieldnames=header)
+        writer.writeheader()
+        for t in transactions:
+            writer.writerow(t)
+
+    print(f"Collected {len(transactions)} transactions. Written to {output_path}")
+    await s.clientsession.close()
+
+
+loop = asyncio.new_event_loop()
+asyncio.set_event_loop(loop)
+asyncio.run(main())