diff --git a/CHANGELOG.md b/CHANGELOG.md
index 546e996..047287e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
# Changelog
+## Unreleased
+* Convert timezone-aware datetimes to UTC before serializing them with a `Z` suffix
+
## 4.48.0
* Add `Transaction.reverse()` to automatically void or fully refund a transaction
* Add `previous_mastercard_transaction_link_id` to `external_vault` on `Transaction` and `CreditCardVerification.create`
diff --git a/braintree/util/generator.py b/braintree/util/generator.py
index b7b68ec..5ed75fc 100644
--- a/braintree/util/generator.py
+++ b/braintree/util/generator.py
@@ -20,6 +20,8 @@ def __generate_boolean(self, value):
return str(value).lower()
def __generate_datetime(self, value):
+ if isinstance(value, datetime.datetime) and value.utcoffset() is not None:
+ value = value.astimezone(datetime.timezone.utc)
return value.strftime("%Y-%m-%dT%H:%M:%SZ")
def __generate_dict(self, dictionary):
diff --git a/tests/unit/test_generator_timezones.py b/tests/unit/test_generator_timezones.py
new file mode 100644
index 0000000..2d860d4
--- /dev/null
+++ b/tests/unit/test_generator_timezones.py
@@ -0,0 +1,29 @@
+import unittest
+from datetime import date, datetime, timedelta, timezone
+
+from braintree.util.xml_util import XmlUtil
+
+
+class TestGeneratorTimezones(unittest.TestCase):
+ def test_positive_offset_preserves_instant_across_date_boundary(self):
+ value = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone(timedelta(hours=5, minutes=30)))
+ xml = XmlUtil.xml_from_dict({"created_at": value})
+ self.assertEqual('2026-01-01T21:34:05Z', xml)
+ self.assertEqual({"created_at": datetime(2026, 1, 1, 21, 34, 5)}, XmlUtil.dict_from_xml(xml))
+
+ def test_negative_offset_preserves_instant(self):
+ value = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone(timedelta(hours=-7)))
+ xml = XmlUtil.xml_from_dict({"created_at": value})
+ self.assertEqual('2026-01-02T10:04:05Z', xml)
+ self.assertEqual({"created_at": datetime(2026, 1, 2, 10, 4, 5)}, XmlUtil.dict_from_xml(xml))
+
+ def test_utc_keeps_existing_output(self):
+ value = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone.utc)
+ self.assertEqual('2026-01-02T03:04:05Z', XmlUtil.xml_from_dict({"value": value}))
+
+ def test_naive_datetime_keeps_existing_output(self):
+ value = datetime(2026, 1, 2, 3, 4, 5)
+ self.assertEqual('2026-01-02T03:04:05Z', XmlUtil.xml_from_dict({"value": value}))
+
+ def test_date_keeps_existing_output(self):
+ self.assertEqual('2026-01-02T00:00:00Z', XmlUtil.xml_from_dict({"value": date(2026, 1, 2)}))