diff --git a/mollie/api/client.py b/mollie/api/client.py
index 79fcc59..281c3b2 100644
--- a/mollie/api/client.py
+++ b/mollie/api/client.py
@@ -3,6 +3,7 @@ import platform
 import re
 import ssl
 import warnings
+from collections import OrderedDict
 
 import requests
 
@@ -33,11 +34,6 @@ class Client(object):
     API_ENDPOINT = 'https://api.mollie.com'
     API_VERSION = 'v2'
     UNAME = ' '.join(platform.uname())
-    USER_AGENT = ' '.join(vs.replace(r'\s+', '-') for vs in [
-        'Mollie/{client_version}'.format(client_version=CLIENT_VERSION),
-        'Python/{python_version}'.format(python_version=platform.python_version()),
-        'OpenSSL/{ssl_version}'.format(ssl_version=ssl.OPENSSL_VERSION.split(' ')[1]),
-    ])
 
     @staticmethod
     def validate_api_endpoint(api_endpoint):
@@ -63,8 +59,10 @@ class Client(object):
     def __init__(self, api_key=None, api_endpoint=None, timeout=10):
         self.api_endpoint = self.validate_api_endpoint(api_endpoint or self.API_ENDPOINT)
         self.api_version = self.API_VERSION
-        self.api_key = None
         self.timeout = timeout
+        self.api_key = None
+
+        # add endpoint resources
         self.payments = Payments(self)
         self.payment_refunds = PaymentRefunds(self)
         self.payment_chargebacks = PaymentChargebacks(self)
@@ -78,6 +76,12 @@ class Client(object):
         self.orders = Orders(self)
         self.subscription_payments = SubscriptionPayments(self)
 
+        # compose base user agent string
+        self.user_agent_components = OrderedDict()
+        self.set_user_agent_component('Mollie', self.CLIENT_VERSION)
+        self.set_user_agent_component('Python', platform.python_version())
+        self.set_user_agent_component('OpenSSL', ssl.OPENSSL_VERSION.split(' ')[1])
+
         if api_key:
             # There is no clean way for supporting both API key and access token acceptance and validation
             #  in __init__(). Furthermore the naming of the parameter would be inconsistent.
@@ -101,6 +105,26 @@ class Client(object):
     def set_timeout(self, timeout):
         self.timeout = timeout
 
+    def set_user_agent_component(self, key, value):
+        """Add or replace new user-agent component strings.
+
+        Given strings are formatted along the format agreed upon by Mollie and implementers:
+        - key and values are separated by a forward slash ("/").
+        - multiple key/values are separated by a space.
+        - keys are camel-cased, and cannot contain spaces.
+        - values cannot contain spaces.
+        """
+        key = ''.join(_x.capitalize() for _x in re.findall(r'\S+', key))
+        if re.search(r'\s+', value):
+            value = '_'.join(re.findall(r'\S+', value))
+        self.user_agent_components[key] = value
+
+    @property
+    def user_agent(self):
+        """Return the formatted user agent string."""
+        components = ["/".join(x) for x in self.user_agent_components.items()]
+        return " ".join(components)
+
     def perform_http_call(self, http_method, path, data=None, params=None):
         if not self.api_key:
             raise RequestSetupError('You have not set an API key. Please use set_api_key() to set the API key.')
@@ -128,7 +152,7 @@ class Client(object):
                     'Accept': 'application/json',
                     'Authorization': 'Bearer {api_key}'.format(api_key=self.api_key),
                     'Content-Type': 'application/json',
-                    'User-Agent': self.USER_AGENT,
+                    'User-Agent': self.user_agent,
                     'X-Mollie-Client-Info': self.UNAME,
                 },
                 params=params,

Allow setting custom useragent data
See the use of `addVersionString` in https://github.com/mollie/mollie-api-php/blob/master/src/MollieApiClient.php. This will enable integrators to add their implementation details to the User-Agent.

FooBar plugin for Oscar then could do:
```
client = Client()
client.add_useragent_string('Oscar', OSCAR_VERSION)
client.add_useragent_string('FooBar', FOOBAR_VERSION)
```
diff --git a/tests/test_client.py b/tests/test_client.py
index 028094e..516a873 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
+import re
 import sys
 from datetime import datetime
 
@@ -313,3 +314,82 @@ def test_client_data_consistency_error(client, response):
     # Update an nonexistent order line. This raises an data consistency error.
     with pytest.raises(DataConsistencyError, match=r'Line id .* not found in response.'):
         order.update_line(line_id, data)
+
+
+def test_client_default_user_agent(client, response):
+    """Default user-agent should contain some known values."""
+
+    regex = re.compile(r'^Mollie/[\d\.]+ Python/[\d\.]+ Openssl/\w+')
+    assert re.match(regex, client.user_agent)
+
+    # perform a request and inpect the actual used headers
+    response.get('https://api.mollie.com/v2/methods', 'methods_list')
+    client.methods.list()
+    request = response.calls[0].request
+    assert re.match(regex, request.headers['User-Agent'])
+
+
+def test_client_set_user_agent_component(response):
+    """We should be able to add useragent components.
+
+    Note: we don't use the fixture client because it is shared between tests, and we don't want it
+    to be clobbered with random User-Agent strings.
+    """
+    client = Client()
+    assert 'Hoeba' not in client.user_agent
+    client.set_user_agent_component('Hoeba', '1.0.0')
+    assert 'Hoeba/1.0.0' in client.user_agent
+
+    response.get('https://api.mollie.com/v2/methods', 'methods_list')
+    client.set_api_key('test_123')
+    client.methods.list()
+    request = response.calls[0].request
+    assert 'Hoeba/1.0.0' in request.headers['User-Agent']
+
+
+@pytest.mark.parametrize("key, expected", [
+    ('lowercase', 'Lowercase'),
+    ('UPPERCASE', 'Uppercase'),
+    ('multiple words', 'MultipleWords'),
+    ('multiple   spaces', 'MultipleSpaces'),
+    ('trailing space ', 'TrailingSpace')
+])
+def test_client_set_user_agent_component_correct_key_syntax(key, expected):
+    """When we receive UA component keys that don't adhere to the proposed syntax, they are corrected."""
+    client = Client()
+    client.set_user_agent_component(key, '1.0.0')
+    assert "{expected}/1.0.0".format(expected=expected) in client.user_agent
+
+
+@pytest.mark.parametrize("value, expected", [
+    ('1.2.3', '1.2.3'),
+    ('singleword', 'singleword'),
+    ('MiXedCaSe', 'MiXedCaSe'),  # should be preserved
+    ('UPPERCASE', 'UPPERCASE'),  # should be preserved
+    ('with space', 'with_space'),
+    ('multiple   spaces', 'multiple_spaces'),
+    ('trailing space ', 'trailing_space')
+])
+def test_client_set_user_agent_component_correct_value_syntax(value, expected):
+    """When we receive UA component values that don't adhere to the proposed syntax, they are corrected."""
+    client = Client()
+    client.set_user_agent_component('Something', value)
+    assert "Something/{expected}".format(expected=expected) in client.user_agent
+
+
+def test_client_update_user_agent_component():
+    """We should be able to update the User-Agent component when using the same key."""
+    client = Client()
+    client.set_user_agent_component('Test', '1.0.0')
+    assert 'Test/1.0.0' in client.user_agent
+
+    # now update the component using the same key
+    client.set_user_agent_component('Test', '2.0.0')
+    assert 'Test/2.0.0' in client.user_agent
+    assert 'Test/1.0.0' not in client.user_agent
+
+    # and update with a key that will be converted to the same value
+    client.set_user_agent_component('TEST', '3.0.0')
+    assert 'Test/3.0.0' in client.user_agent
+    assert 'Test/2.0.0' not in client.user_agent
+    assert 'Test/1.0.0' not in client.user_agent

pipenv sync --dev
parse_log_pytest
Pipfile
pytest pytest-cov responses mock pycodestyle isort
apt-get update apt-get install -y gcc pip install pipenv
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi==2025.1.31
charset-normalizer==3.4.1
coverage==7.8.0
distlib==0.3.9
exceptiongroup==1.2.2
filelock==3.18.0
idna==3.10
iniconfig==2.1.0
isort==6.0.1
mock==5.2.0
packaging==24.2
pipenv==2024.4.1
pipfile==0.0.2
platformdirs==4.3.7
pluggy==1.5.0
pycodestyle==2.13.0
pytest==8.3.5
pytest-cov==6.0.0
PyYAML==6.0.2
requests==2.32.3
responses==0.25.7
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tomli==2.2.1
urllib3==2.3.0
virtualenv==20.29.3

name: mollie-api-python
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - pipfile=0.0.2=py_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - coverage==7.8.0
      - distlib==0.3.9
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - idna==3.10
      - iniconfig==2.1.0
      - isort==6.0.1
      - mock==5.2.0
      - packaging==24.2
      - pipenv==2024.4.1
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pyyaml==6.0.2
      - requests==2.32.3
      - responses==0.25.7
      - tomli==2.2.1
      - urllib3==2.3.0
      - virtualenv==20.29.3
prefix: /opt/conda/envs/mollie-api-python

tests/test_client.py::test_client_default_user_agent tests/test_client.py::test_client_set_user_agent_component tests/test_client.py::test_client_set_user_agent_component_correct_key_syntax[lowercase-Lowercase] tests/test_client.py::test_client_set_user_agent_component_correct_key_syntax[UPPERCASE-Uppercase] tests/test_client.py::test_client_set_user_agent_component_correct_key_syntax[multiple tests/test_client.py::test_client_set_user_agent_component_correct_key_syntax[trailing tests/test_client.py::test_client_set_user_agent_component_correct_value_syntax[1.2.3-1.2.3] tests/test_client.py::test_client_set_user_agent_component_correct_value_syntax[singleword-singleword] tests/test_client.py::test_client_set_user_agent_component_correct_value_syntax[MiXedCaSe-MiXedCaSe] tests/test_client.py::test_client_set_user_agent_component_correct_value_syntax[UPPERCASE-UPPERCASE] tests/test_client.py::test_client_set_user_agent_component_correct_value_syntax[with tests/test_client.py::test_client_set_user_agent_component_correct_value_syntax[multiple tests/test_client.py::test_client_set_user_agent_component_correct_value_syntax[trailing tests/test_client.py::test_client_update_user_agent_component
tests/test_client.py::test_client_broken_cert_bundle
tests/test_client.py::test_generate_querystring[params0-None] tests/test_client.py::test_generate_querystring[params1-locale=nl_NL] tests/test_client.py::test_generate_querystring[params2-hoeba=kek&locale=nl_NL] tests/test_client.py::test_generate_querystring[params3-amount%5Bcurrency%5D=USD&amount%5Bvalue%5D=100.00] tests/test_client.py::test_client_querystring tests/test_client.py::test_client_api_key tests/test_client.py::test_client_no_api_key tests/test_client.py::test_client_invalid_api_key tests/test_client.py::test_client_api_key_during_init_deprecated tests/test_client.py::test_client_generic_request_error tests/test_client.py::test_client_invalid_create_data tests/test_client.py::test_client_invalid_update_data tests/test_client.py::test_client_get_invalid_id[customers-Invalid tests/test_client.py::test_client_get_invalid_id[payments-Invalid tests/test_client.py::test_client_get_invalid_id[refunds-Invalid tests/test_client.py::test_client_get_invalid_id[orders-Invalid tests/test_client.py::test_client_get_customer_related_invalid_id[customer_mandates-Invalid tests/test_client.py::test_client_get_customer_related_invalid_id[customer_payments-Invalid tests/test_client.py::test_client_get_customer_related_invalid_id[customer_subscriptions-Invalid tests/test_client.py::test_client_get_payment_related_invalid_id[payment_chargebacks-Invalid tests/test_client.py::test_client_get_payment_related_invalid_id[payment_refunds-Invalid tests/test_client.py::test_client_invalid_json_response tests/test_client.py::test_client_get_received_error_response[error_unauthorized-401-UnauthorizedError-Missing tests/test_client.py::test_client_get_received_error_response[customer_doesnotexist-404-NotFoundError-No tests/test_client.py::test_client_get_received_error_response[payment_rejected-422-UnprocessableEntityError-The tests/test_client.py::test_client_get_received_error_response[error_teapot-418-ResponseError-Just tests/test_client.py::test_client_delete_received_error_response[error_unauthorized-401-UnauthorizedError-Missing tests/test_client.py::test_client_delete_received_error_response[customer_doesnotexist-404-NotFoundError-No tests/test_client.py::test_client_delete_received_error_response[error_teapot-418-ResponseError-Just tests/test_client.py::test_client_response_404_but_no_payload tests/test_client.py::test_client_error_including_field_response tests/test_client.py::test_client_unicode_error_py3 tests/test_client.py::test_client_request_timeout tests/test_client.py::test_client_request_timed_out tests/test_client.py::test_client_data_consistency_error
mollie/api/client.py
mollie/api/client.py