diff --git a/README.rst b/README.rst
index 6306c32..b1bd1f1 100644
--- a/README.rst
+++ b/README.rst
@@ -58,7 +58,7 @@ configs are used to tell **dooblr** which topics and pieces of data need to be p
 Let's say you have a device that publishes to the ``home/kitchen/fridge/temperature`` and
 ``home/kitchen/freezer/temperature`` topics with the data::
 
-    {"temperature": 40.1, "units":"F", "humidity": 0.12}
+    {"temperature": 40.1, "units":"F", "humidity": 0.12, "label": "blue"}
 
 You would probably want to create a measurement config called ``temperature.yml`` that looks like::
 
@@ -73,9 +73,11 @@ You would probably want to create a measurement config called ``temperature.yml`
         - humidity
         tags:
         - units
+        optional_tags:
+        - label  # Maybe not every message on these topics have a "label" property!
 
 Notice that there can be multiple topics, fields, and tags. Tags and fields refer to the tags and fields used in
-InfluxDB.
+InfluxDB. Optional tags will not raise an error if they weren't defined in the MQTT message, while regular tags will.
 
 .. |Build Status| image:: https://travis-ci.org/makerslocal/dooblr.svg?branch=master
    :target: https://travis-ci.org/makerslocal/dooblr
diff --git a/dooblr/config.py b/dooblr/config.py
index 1f0e389..e6476b9 100644
--- a/dooblr/config.py
+++ b/dooblr/config.py
@@ -99,23 +99,31 @@ class MeasurementConfig(object):
             self.measurements[measurement] = {}
 
             if "fields" not in self._config[measurement]:
-                raise DooblrConfigError("Measurement {m} does not contain required option 'fields'".format(
+                raise DooblrConfigError("Measurement {m} does not contain required property 'fields'".format(
                     m=measurement))
             else:
                 self.measurements[measurement]["fields"] = self._listify(self._config[measurement]["fields"])
 
             if "topics" not in self._config[measurement]:
-                raise DooblrConfigError("Measurement {m} does not contain required option 'topics'".format(
+                raise DooblrConfigError("Measurement {m} does not contain required property 'topics'".format(
                     m=measurement))
             else:
                 self.measurements[measurement]["topics"] = self._listify(self._config[measurement]["topics"])
 
             if "tags" not in self._config[measurement]:
-                self._logger.info("Measurement {m} does not contain optional option 'tags'".format(m=measurement))
+                self._logger.info("Measurement {m} does not contain optional property 'tags'".format(m=measurement))
                 self.measurements[measurement]["tags"] = []
             else:
                 self.measurements[measurement]["tags"] = self._listify(self._config[measurement]["tags"])
 
+            if "optional_tags" not in self._config[measurement]:
+                self._logger.info("Measurement {m} does not contain optional property 'optional_tags'".format(
+                    m=measurement))
+                self.measurements[measurement]["optional_tags"] = []
+            else:
+                self.measurements[measurement]["optional_tags"] = self._listify(
+                    self._config[measurement]["optional_tags"])
+
     @staticmethod
     def _listify(items):
         item_list = items
@@ -129,7 +137,8 @@ class MeasurementConfig(object):
             "my_measurement": {
                 "fields": ["important_value"],
                 "topics": ["dooblr/testing/device"],
-                "tags": ["tag1", "tag2"]
+                "tags": ["tag1", "tag2"],
+                "optional_tags": ["option"]
             }}
         with open(path, 'w') as f:
             yaml.dump(sample_config, f, default_flow_style=False)
diff --git a/dooblr/mqttclient.py b/dooblr/mqttclient.py
index 128966e..f1cc668 100644
--- a/dooblr/mqttclient.py
+++ b/dooblr/mqttclient.py
@@ -74,6 +74,12 @@ class MqttClient(object):
             try:
                 parsed_message["tags"][tag] = message[tag]
             except KeyError:
-                raise DooblrMqttError("Message does not contain tag '{field}'".format(field=tag))
+                raise DooblrMqttError("Message does not contain required tag '{tag}'".format(tag=tag))
+
+        for tag in measurement["optional_tags"]:
+            try:
+                parsed_message["tags"][tag] = message[tag]
+            except KeyError:
+                self._logger.info("Message does not contain optional tag '{tag}'".format(tag=tag))
 
         return parsed_message

Make tags optional, not required, for a message to be valid
diff --git a/test/test_config.py b/test/test_config.py
index b297bac..f1c6b10 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -15,8 +15,8 @@ class MainConfigParseTestCase(unittest.TestCase):
     def test_partial_section_does_not_raise_error(self):
         config = MainConfig()
         config_text = "\n".join((
-                      u"mqtt:",
-                      "  host: blah"))
+            u"mqtt:",
+            "  host: blah"))
         try:
             config._parse(config_text)
         except Exception as e:
@@ -28,39 +28,39 @@ class MeasurementConfigParseTestCase(unittest.TestCase):
     def test_missing_field_value_raises_error(self):
         config = MeasurementConfig()
         config_text = "\n".join((
-                       u"measurement:",
-                       "  topics:",
-                       "    - ml256/topic/device",
-                       "    - ml256/topic/otherdevice",
-                       "  tags:",
-                       "    - supertag",
-                       "    - awesometag"))
+            u"measurement:",
+            "  topics:",
+            "    - ml256/topic/device",
+            "    - ml256/topic/otherdevice",
+            "  tags:",
+            "    - supertag",
+            "    - awesometag"))
         with self.assertRaises(DooblrConfigError):
             config._parse(config_text)
 
     def test_missing_topic_value_raises_error(self):
         config = MeasurementConfig()
         config_text = "\n".join((
-                       u"measurement:",
-                       "  fields:",
-                       "    - coolfield",
-                       "    - neatfield",
-                       "  tags: ",
-                       "    - supertag",
-                       "    - awesometag"))
+            u"measurement:",
+            "  fields:",
+            "    - coolfield",
+            "    - neatfield",
+            "  tags: ",
+            "    - supertag",
+            "    - awesometag"))
         with self.assertRaises(DooblrConfigError):
             config._parse(config_text)
 
     def test_missing_tag_value_does_not_raise_error(self):
         config = MeasurementConfig()
         config_text = "\n".join((
-                       u"measurement:",
-                       "  topics:",
-                       "    - ml256/topic/device",
-                       "    - ml256/topic/otherdevice",
-                       "  fields:",
-                       "    - coolfield",
-                       "    - neatfield"))
+            u"measurement:",
+            "  topics:",
+            "    - ml256/topic/device",
+            "    - ml256/topic/otherdevice",
+            "  fields:",
+            "    - coolfield",
+            "    - neatfield"))
         try:
             config._parse(config_text)
         except DooblrConfigError as e:
@@ -69,32 +69,61 @@ class MeasurementConfigParseTestCase(unittest.TestCase):
     def test_valid_config_does_not_raise_error(self):
         config = MeasurementConfig()
         config_text = "\n".join((
-                       u"measurement:",
-                       "  topics:",
-                       "    - ml256/topic/device",
-                       "    - ml256/topic/otherdevice",
-                       "  fields: ",
-                       "    - coolfield",
-                       "    - neatfield",
-                       "  tags: ",
-                       "    - supertag",
-                       "    - awesometag"))
+            u"measurement:",
+            "  topics:",
+            "    - ml256/topic/device",
+            "    - ml256/topic/otherdevice",
+            "  fields: ",
+            "    - coolfield",
+            "    - neatfield",
+            "  tags: ",
+            "    - supertag",
+            "    - awesometag"))
         try:
             config._parse(config_text)
         except DooblrConfigError as e:
             self.fail("Valid config raised an exception! ({e})".format(e=e))
 
+    def test_single_optional_tag_is_parsed(self):
+        config = MeasurementConfig()
+        config_text = "\n".join((
+            u"measurement:",
+            "  topics:",
+            "    - ml256/topic/device",
+            "  fields: ",
+            "    - coolfield",
+            "  optional_tags: ",
+            "    - optiontag"))
+
+        config._parse(config_text)
+        self.assertEquals(config.measurements["measurement"]["optional_tags"], ["optiontag"])
+
+    def test_multiple_optional_tags_are_parsed(self):
+        config = MeasurementConfig()
+        config_text = "\n".join((
+            u"measurement:",
+            "  topics:",
+            "    - ml256/topic/device",
+            "  fields: ",
+            "    - coolfield",
+            "  optional_tags: ",
+            "    - optiontag",
+            "    - label"))
+
+        config._parse(config_text)
+        self.assertEquals(config.measurements["measurement"]["optional_tags"], ["optiontag", "label"])
+
     def test_single_topic_is_parsed(self):
         config = MeasurementConfig()
         config_text = "\n".join((
-                       u"measurement:",
-                       "  topics: ml256/topic/device",
-                       "  fields: ",
-                       "    - coolfield",
-                       "    - neatfield",
-                       "  tags: ",
-                       "    - supertag",
-                       "    - awesometag"))
+            u"measurement:",
+            "  topics: ml256/topic/device",
+            "  fields: ",
+            "    - coolfield",
+            "    - neatfield",
+            "  tags: ",
+            "    - supertag",
+            "    - awesometag"))
 
         config._parse(config_text)
         self.assertEquals(config.measurements["measurement"]["topics"], ["ml256/topic/device"])
@@ -102,16 +131,16 @@ class MeasurementConfigParseTestCase(unittest.TestCase):
     def test_multiple_topics_are_parsed(self):
         config = MeasurementConfig()
         config_text = "\n".join((
-                       u"measurement:",
-                       "  topics:",
-                       "    - ml256/topic/device",
-                       "    - ml256/topic/otherdevice",
-                       "  fields: ",
-                       "    - coolfield",
-                       "    - neatfield",
-                       "  tags: ",
-                       "    - supertag",
-                       "    - awesometag"))
+            u"measurement:",
+            "  topics:",
+            "    - ml256/topic/device",
+            "    - ml256/topic/otherdevice",
+            "  fields: ",
+            "    - coolfield",
+            "    - neatfield",
+            "  tags: ",
+            "    - supertag",
+            "    - awesometag"))
 
         config._parse(config_text)
         self.assertEquals(config.measurements["measurement"]["topics"], ["ml256/topic/device", "ml256/topic/otherdevice"])
@@ -151,14 +180,14 @@ class MeasurementConfigParseTestCase(unittest.TestCase):
     def test_single_tag_is_parsed(self):
         config = MeasurementConfig()
         config_text = "\n".join((
-                       u"measurement:",
-                       "  topics:",
-                       "    - ml256/topic/device",
-                       "    - ml256/topic/otherdevice",
-                       "  fields:",
-                       "    - coolfield",
-                       "    - neatfield",
-                       "  tags: supertag"))
+            u"measurement:",
+            "  topics:",
+            "    - ml256/topic/device",
+            "    - ml256/topic/otherdevice",
+            "  fields:",
+            "    - coolfield",
+            "    - neatfield",
+            "  tags: supertag"))
 
         config._parse(config_text)
         self.assertEquals(config.measurements["measurement"]["tags"], ["supertag"])
@@ -166,16 +195,16 @@ class MeasurementConfigParseTestCase(unittest.TestCase):
     def test_multiple_tags_are_parsed(self):
         config = MeasurementConfig()
         config_text = "\n".join((
-                       u"measurement:",
-                       "  topics:",
-                       "    - ml256/topic/device",
-                       "    - ml256/topic/otherdevice",
-                       "  fields:",
-                       "    - coolfield",
-                       "    - neatfield",
-                       "  tags:",
-                       "    - supertag",
-                       "    - awesometag"))
+            u"measurement:",
+            "  topics:",
+            "    - ml256/topic/device",
+            "    - ml256/topic/otherdevice",
+            "  fields:",
+            "    - coolfield",
+            "    - neatfield",
+            "  tags:",
+            "    - supertag",
+            "    - awesometag"))
 
         config._parse(config_text)
         self.assertEquals(config.measurements["measurement"]["tags"], ["supertag", "awesometag"])
diff --git a/test/test_mqttclient.py b/test/test_mqttclient.py
index df65079..d4742b3 100644
--- a/test/test_mqttclient.py
+++ b/test/test_mqttclient.py
@@ -4,30 +4,58 @@ from dooblr import mqttclient
 
 class ParseMessageTestCase(unittest.TestCase):
 
-    def test_fields_and_tags_are_parsed(self):
+    def test_good_message_is_parsed(self):
         client = mqttclient.MqttClient(None)
-        client._measurements = {"environment":
-                                    {"fields": ["temperature", "humidity"],
-                                     "tags": ["location", "address"]
-                                     }
-                                }
+        client._measurements = {
+            "environment": {
+                    "fields": [
+                        "temperature",
+                        "humidity"
+                    ],
+                    "tags": [
+                        "location",
+                        "address"
+                    ],
+                    "optional_tags": [
+                        "machine",
+                        "label"
+                    ]
+                }
+        }
 
-        parsed = client._parse_message("environment", '{"temperature": 25.6, "humidity": 19.44, "location": "tool", "address": "1dec40"}')
+        parsed = client._parse_message("environment", '{"temperature": 25.6, "humidity": 19.44, "location": "tool", "address": "1dec40", "machine": "ts9000", "label": "blue"}')
         expected = {"measurement": "environment",
                     "fields": {"temperature": 25.6, "humidity": 19.44},
-                    "tags": {"location": u"tool", "address": u"1dec40"}}
+                    "tags": {"location": u"tool", "address": u"1dec40", "machine": u"ts9000", "label": u"blue"}}
         self.assertEquals(parsed, expected)
 
-    def test_missing_tag_raises_error(self):
+    def test_missing_required_tag_raises_error(self):
         client = mqttclient.MqttClient(None)
-        client._measurements = {"environment":
-                                    {"fields": ["temperature"],
-                                     "tags": ["location"]
-                                     }
-                                }
+        client._measurements = {
+            "environment": {
+                "fields": ["temperature"],
+                "tags": ["location"]
+            }
+        }
+
         with self.assertRaises(mqttclient.DooblrMqttError):
             client._parse_message("environment", '{"temperature": 25.6}')
 
+    def test_missing_optional_tag_does_not_raise_error(self):
+        client = mqttclient.MqttClient(None)
+        client._measurements = {
+            "environment": {
+                "fields": ["temperature"],
+                "tags": ["location"],
+                "optional_tags": ["machine_type"]
+            }
+        }
+
+        try:
+            client._parse_message("environment", '{"temperature": 25.6, "location":"kitchen"}')
+        except Exception as e:
+            self.fail("Unexpected error was raised: {e}".format(e=e))
+
     def test_missing_field_raises_error(self):
         client = mqttclient.MqttClient(None)
         client._measurements = {"environment":
@@ -36,4 +64,4 @@ class ParseMessageTestCase(unittest.TestCase):
                                      }
                                 }
         with self.assertRaises(mqttclient.DooblrMqttError):
-            client._parse_message("environment", '{"location": "tool"}')
\ No newline at end of file
+            client._parse_message("environment", '{"location": "tool"}')

pip install -e .
parse_log_pytest
requirements.txt
nose flake8 pytest
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi==2025.1.31
charset-normalizer==3.4.1
configparser==7.2.0
-e git+https://github.com/makerslocal/dooblr.git@6c73cbc543a68b8112f9c6eb748d8a44ee100ee0#egg=dooblr
exceptiongroup==1.2.2
flake8==7.2.0
idna==3.10
influxdb==5.3.2
iniconfig==2.1.0
mccabe==0.7.0
msgpack==1.1.0
nose==1.3.7
packaging==24.2
paho-mqtt==2.1.0
pluggy==1.5.0
pycodestyle==2.13.0
pyflakes==3.3.1
pytest==8.3.5
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.2
requests==2.32.3
schema==0.7.7
six==1.17.0
tomli==2.2.1
urllib3==2.3.0

name: dooblr
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
  - 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
  - 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
      - configparser==7.2.0
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - idna==3.10
      - influxdb==5.3.2
      - iniconfig==2.1.0
      - mccabe==0.7.0
      - msgpack==1.1.0
      - nose==1.3.7
      - packaging==24.2
      - paho-mqtt==2.1.0
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.1
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.2
      - requests==2.32.3
      - schema==0.7.7
      - six==1.17.0
      - tomli==2.2.1
      - urllib3==2.3.0
prefix: /opt/conda/envs/dooblr

test/test_config.py::MeasurementConfigParseTestCase::test_multiple_optional_tags_are_parsed test/test_config.py::MeasurementConfigParseTestCase::test_single_optional_tag_is_parsed test/test_mqttclient.py::ParseMessageTestCase::test_good_message_is_parsed
test/test_config.py::MainConfigParseTestCase::test_empty_section_does_not_raise_error test/test_config.py::MainConfigParseTestCase::test_partial_section_does_not_raise_error test/test_config.py::MeasurementConfigParseTestCase::test_missing_field_value_raises_error test/test_config.py::MeasurementConfigParseTestCase::test_missing_tag_value_does_not_raise_error test/test_config.py::MeasurementConfigParseTestCase::test_missing_topic_value_raises_error test/test_config.py::MeasurementConfigParseTestCase::test_multiple_fields_are_parsed test/test_config.py::MeasurementConfigParseTestCase::test_multiple_tags_are_parsed test/test_config.py::MeasurementConfigParseTestCase::test_multiple_topics_are_parsed test/test_config.py::MeasurementConfigParseTestCase::test_single_field_is_parsed test/test_config.py::MeasurementConfigParseTestCase::test_single_tag_is_parsed test/test_config.py::MeasurementConfigParseTestCase::test_single_topic_is_parsed test/test_config.py::MeasurementConfigParseTestCase::test_valid_config_does_not_raise_error test/test_mqttclient.py::ParseMessageTestCase::test_missing_field_raises_error test/test_mqttclient.py::ParseMessageTestCase::test_missing_optional_tag_does_not_raise_error test/test_mqttclient.py::ParseMessageTestCase::test_missing_required_tag_raises_error
README.rst dooblr/mqttclient.py dooblr/config.py
README.rst dooblr/mqttclient.py dooblr/config.py