diff --git a/changelog.d/358.feature b/changelog.d/358.feature
new file mode 100644
index 00000000..c12cfd33
--- /dev/null
+++ b/changelog.d/358.feature
@@ -0,0 +1 @@
+Add a new APNs configuration option `push_with_badge` to suppress sending `aps.badge` in APNs messages.
diff --git a/sygnal.yaml.sample b/sygnal.yaml.sample
index 1b59d4c5..bbe6c425 100644
--- a/sygnal.yaml.sample
+++ b/sygnal.yaml.sample
@@ -202,6 +202,10 @@ apps:
   #  # Defaults to True, set this to False if your client library provides a
   #  # push token in hex format.
   #  #convert_device_token_to_hex: false
+  #  #
+  #  # Specifies whether to send the badge key in the APNs message.
+  #  # Defaults to True, set this to False to suppress sending the badge count.
+  #  #push_with_badge: false
 
   # This is an example GCM/FCM push configuration.
   #
diff --git a/sygnal/apnspushkin.py b/sygnal/apnspushkin.py
index a9c2d5f1..073c6bb8 100644
--- a/sygnal/apnspushkin.py
+++ b/sygnal/apnspushkin.py
@@ -111,6 +111,7 @@ class ApnsPushkin(ConcurrencyLimitedPushkin):
         "topic",
         "push_type",
         "convert_device_token_to_hex",
+        "push_with_badge",
     } | ConcurrencyLimitedPushkin.UNDERSTOOD_CONFIG_FIELDS
 
     APNS_PUSH_TYPES = {
@@ -555,7 +556,7 @@ def _get_payload_full(
         if loc_args:
             payload["aps"].setdefault("alert", {})["loc-args"] = loc_args
 
-        if badge is not None:
+        if self.get_config("push_with_badge", bool, True) and badge is not None:
             payload["aps"]["badge"] = badge
 
         if loc_key and n.room_id:
@@ -563,6 +564,12 @@ def _get_payload_full(
         if loc_key and n.event_id:
             payload["event_id"] = n.event_id
 
+        if not self.get_config("push_with_badge", bool, True):
+            if n.counts.unread is not None:
+                payload["unread_count"] = n.counts.unread
+            if n.counts.missed_calls is not None:
+                payload["missed_calls"] = n.counts.missed_calls
+
         return payload
 
     async def _send_notification(
diff --git a/tests/test_apns.py b/tests/test_apns.py
index 6a6653e3..5b1f9b72 100644
--- a/tests/test_apns.py
+++ b/tests/test_apns.py
@@ -24,6 +24,7 @@
 
 PUSHKIN_ID = "com.example.apns"
 PUSHKIN_ID_WITH_PUSH_TYPE = "com.example.apns.push_type"
+PUSHKIN_ID_WITH_NO_BADGE = "com.example.apns.no_badge"
 
 TEST_CERTFILE_PATH = "/path/to/my/certfile.pem"
 
@@ -55,6 +56,12 @@
     "pushkey_ts": 42,
 }
 
+DEVICE_EXAMPLE_FOR_NO_BADGE_PUSHKIN = {
+    "app_id": "com.example.apns.no_badge",
+    "pushkey": "spqr",
+    "pushkey_ts": 42,
+}
+
 
 class ApnsTestCase(testutils.TestCase):
     def setUp(self) -> None:
@@ -73,10 +80,12 @@ def setUp(self) -> None:
         self.apns_pushkin_snotif = MagicMock()
         test_pushkin = self.get_test_pushkin(PUSHKIN_ID)
         test_pushkin_push_type = self.get_test_pushkin(PUSHKIN_ID_WITH_PUSH_TYPE)
+        test_pushkin_with_no_badge = self.get_test_pushkin(PUSHKIN_ID_WITH_NO_BADGE)
         # type safety: using ignore here due to mypy not handling monkeypatching,
         # see https://github.com/python/mypy/issues/2427
         test_pushkin._send_notification = self.apns_pushkin_snotif  # type: ignore[assignment] # noqa: E501
         test_pushkin_push_type._send_notification = self.apns_pushkin_snotif  # type: ignore[assignment] # noqa: E501
+        test_pushkin_with_no_badge._send_notification = self.apns_pushkin_snotif  # type: ignore[assignment] # noqa: E501
 
     def get_test_pushkin(self, name: str) -> ApnsPushkin:
         test_pushkin = self.sygnal.pushkins[name]
@@ -91,6 +100,12 @@ def config_setup(self, config: Dict[str, Any]) -> None:
             "certfile": TEST_CERTFILE_PATH,
             "push_type": "alert",
         }
+        config["apps"][PUSHKIN_ID_WITH_NO_BADGE] = {
+            "type": "apns",
+            "certfile": TEST_CERTFILE_PATH,
+            "push_type": "alert",
+            "push_with_badge": False,
+        }
 
     def test_payload_truncation(self) -> None:
         """
@@ -391,3 +406,47 @@ def test_expected_with_push_type(self) -> None:
         self.assertEqual(PushType.ALERT, notification_req.push_type)
 
         self.assertEqual({"rejected": []}, resp)
+
+    def test_expected_with_no_badge(self) -> None:
+        """
+        Tests the expected case with no badge: a good response from APNS means
+        we pass on a good response to the homeserver.
+        """
+        # Arrange
+        method = self.apns_pushkin_snotif
+        method.side_effect = testutils.make_async_magic_mock(
+            NotificationResult("notID", "200")
+        )
+
+        # Act
+        resp = self._request(
+            self._make_dummy_notification([DEVICE_EXAMPLE_FOR_NO_BADGE_PUSHKIN])
+        )
+
+        # Assert
+        self.assertEqual(1, method.call_count)
+        ((notification_req,), _kwargs) = method.call_args
+
+        self.assertEqual(
+            {
+                "room_id": "!slw48wfj34rtnrf:example.com",
+                "event_id": "$qTOWWTEL48yPm3uT-gdNhFcoHxfKbZuqRVnnWWSkGBs",
+                "missed_calls": 1,
+                "unread_count": 2,
+                "aps": {
+                    "alert": {
+                        "loc-key": "MSG_FROM_USER_IN_ROOM_WITH_CONTENT",
+                        "loc-args": [
+                            "Major Tom",
+                            "Mission Control",
+                            "I'm floating in a most peculiar way.",
+                        ],
+                    },
+                },
+            },
+            notification_req.message,
+        )
+
+        self.assertEqual(PushType.ALERT, notification_req.push_type)
+
+        self.assertEqual({"rejected": []}, resp)