diff --git a/examples/receiving_email.py b/examples/receiving_email.py
index e50656f..2edd1de 100644
--- a/examples/receiving_email.py
+++ b/examples/receiving_email.py
@@ -38,12 +38,25 @@
print(f"BCC: {received_email.get('bcc', [])}")
print(f"Reply-To: {received_email.get('reply_to', [])}")
-print("\n--- Headers ---")
+print("\n--- Email MIME Headers ---")
+# received_email["headers"] contains the MIME headers of the inbound email
+# (e.g. X-Mailer, DKIM-Signature). These come from the API response body
+# and are part of the email itself, not the HTTP response.
if received_email.get("headers"):
for header_name, header_value in received_email["headers"].items():
print(f"{header_name}: {header_value}")
else:
- print("No custom headers")
+ print("No email headers")
+
+print("\n--- HTTP Response Headers ---")
+# received_email["http_headers"] contains HTTP-level metadata from the Resend API
+# (e.g. x-request-id, x-ratelimit-remaining). Injected by the SDK, never part
+# of the email content.
+if received_email.get("http_headers"):
+ print(f"Rate limit: {received_email['http_headers'].get('ratelimit-limit')}")
+ print(
+ f"Rate limit remaining: {received_email['http_headers'].get('ratelimit-remaining')}"
+ )
print("\n--- Attachments ---")
if received_email["attachments"]:
diff --git a/examples/with_headers.py b/examples/with_headers.py
index ad36ed7..7d53b81 100644
--- a/examples/with_headers.py
+++ b/examples/with_headers.py
@@ -1,44 +1,80 @@
"""
-Example demonstrating how to access response headers.
+Example demonstrating the three different types of headers in the Resend Python SDK:
-Response headers include useful information like rate limits, request IDs, etc.
+1. Email headers (SendParams["headers"]): Custom MIME headers added to the outgoing
+ email itself, visible to the recipient's mail client (e.g. X-Entity-Ref-ID).
+
+2. HTTP response headers (response["http_headers"]): HTTP-level metadata returned
+ by the Resend API, such as rate limit info and request IDs. These are injected
+ by the SDK and are never part of the email content.
+
+3. Inbound email MIME headers (email["headers"]): MIME headers present on a received
+ email, returned as part of the API response body (e.g. X-Mailer, DKIM-Signature).
"""
import os
import resend
-if not os.environ["RESEND_API_KEY"]:
- raise EnvironmentError("RESEND_API_KEY is missing")
+resend.api_key = os.environ["RESEND_API_KEY"]
+
+# --- Example 1: Custom email headers (part of the outgoing email itself) ---
params: resend.Emails.SendParams = {
"from": "onboarding@resend.dev",
"to": ["delivered@resend.dev"],
"subject": "Hello from Resend",
"html": "Hello, world!",
+ "headers": {
+ "X-Entity-Ref-ID": "123456789",
+ },
}
resp: resend.Emails.SendResponse = resend.Emails.send(params)
print(f"Email sent! ID: {resp['id']}")
-if "headers" in resp:
- print(f"Request ID: {resp['headers'].get('x-request-id')}")
- print(f"Rate limit: {resp['headers'].get('x-ratelimit-limit')}")
- print(f"Rate limit remaining: {resp['headers'].get('x-ratelimit-remaining')}")
- print(f"Rate limit reset: {resp['headers'].get('x-ratelimit-reset')}")
+# --- Example 2: HTTP response headers (SDK metadata, not part of the email) ---
+
+if "http_headers" in resp:
+ print(f"Rate limit: {resp['http_headers'].get('ratelimit-limit')}")
+ print(f"Rate limit remaining: {resp['http_headers'].get('ratelimit-remaining')}")
+ print(f"Rate limit reset: {resp['http_headers'].get('ratelimit-reset')}")
+
+# --- Example 3: Inbound email MIME headers (from a received email response body) ---
+
+# Replace with a real received email ID
+received_email_id = os.environ.get("RECEIVED_EMAIL_ID", "")
+
+if received_email_id:
+ received: resend.ReceivedEmail = resend.Emails.Receiving.get(
+ email_id=received_email_id
+ )
+
+ # email["headers"] — MIME headers of the inbound email, part of the API response body.
+ # Completely separate from http_headers injected by the SDK.
+ if received.get("headers"):
+ print("Inbound email MIME headers:")
+ for name, value in received["headers"].items():
+ print(f" {name}: {value}")
+
+ # http_headers are also available on received email responses
+ if received.get("http_headers"):
+ print(
+ f"Rate limit remaining: {received['http_headers'].get('ratelimit-remaining')}"
+ )
+else:
+ print("Set RECEIVED_EMAIL_ID env var to run the inbound email headers example.")
-print("\n")
-print("Example 3: Rate limit tracking")
+# --- Example 4: Rate limit tracking via HTTP response headers ---
def send_with_rate_limit_check(params: resend.Emails.SendParams) -> str:
"""Example function showing how to track rate limits."""
response = resend.Emails.send(params)
- # Access headers via dict key
- headers = response.get("headers", {})
- remaining = headers.get("x-ratelimit-remaining")
- limit = headers.get("x-ratelimit-limit")
+ http_headers = response.get("http_headers", {})
+ remaining = http_headers.get("ratelimit-remaining")
+ limit = http_headers.get("ratelimit-limit")
if remaining and limit:
print(f"Rate limit usage: {int(limit) - int(remaining)}/{limit}")
diff --git a/resend/_base_response.py b/resend/_base_response.py
index 47ac531..1f0d23a 100644
--- a/resend/_base_response.py
+++ b/resend/_base_response.py
@@ -9,8 +9,8 @@ class BaseResponse(TypedDict):
"""Base response type that all API responses inherit from.
Attributes:
- headers: HTTP response headers including rate limit info, request IDs, etc.
- Optional field that may not be present in all responses.
+ http_headers: HTTP response headers including rate limit info, request IDs, etc.
+ Optional field that may not be present in all responses.
"""
- headers: NotRequired[Dict[str, str]]
+ http_headers: NotRequired[Dict[str, str]]
diff --git a/resend/emails/_emails.py b/resend/emails/_emails.py
index 9f7416a..4921e09 100644
--- a/resend/emails/_emails.py
+++ b/resend/emails/_emails.py
@@ -201,7 +201,7 @@ class SendResponse(BaseResponse):
Attributes:
id (str): The ID of the sent email
- headers (NotRequired[Dict[str, str]]): HTTP response headers (inherited from BaseResponse)
+ http_headers (NotRequired[Dict[str, str]]): HTTP response headers (inherited from BaseResponse)
"""
id: str
@@ -240,7 +240,7 @@ class ListResponse(BaseResponse):
object (str): The object type: "list"
data (List[Email]): The list of email objects.
has_more (bool): Whether there are more emails available for pagination.
- headers (NotRequired[Dict[str, str]]): HTTP response headers (inherited from BaseResponse)
+ http_headers (NotRequired[Dict[str, str]]): HTTP response headers (inherited from BaseResponse)
"""
object: str
diff --git a/resend/emails/_received_email.py b/resend/emails/_received_email.py
index 5176b18..4234257 100644
--- a/resend/emails/_received_email.py
+++ b/resend/emails/_received_email.py
@@ -2,6 +2,8 @@
from typing_extensions import NotRequired, TypedDict
+from resend._base_response import BaseResponse
+
class EmailAttachment(TypedDict):
"""
@@ -109,7 +111,7 @@ class EmailAttachmentDetails(TypedDict):
)
-class _ReceivedEmailDefaultAttrs(_ReceivedEmailFromParam):
+class _ReceivedEmailDefaultAttrs(_ReceivedEmailFromParam, BaseResponse):
object: str
"""
The object type.
diff --git a/resend/request.py b/resend/request.py
index 5d8ddf3..26e2697 100644
--- a/resend/request.py
+++ b/resend/request.py
@@ -114,7 +114,7 @@ def make_request(self, url: str) -> Union[Dict[str, Any], List[Any]]:
parsed_data = cast(Union[Dict[str, Any], List[Any]], json.loads(content))
# Inject headers into dict responses
if isinstance(parsed_data, dict):
- parsed_data["headers"] = dict(self._response_headers)
+ parsed_data["http_headers"] = dict(self._response_headers)
# For list responses, return as-is (lists can't have headers key)
return parsed_data
except json.JSONDecodeError:
diff --git a/resend/version.py b/resend/version.py
index 68580de..327a273 100644
--- a/resend/version.py
+++ b/resend/version.py
@@ -1,4 +1,4 @@
-__version__ = "2.24.0"
+__version__ = "2.25.0"
def get_version() -> str:
diff --git a/tests/emails_test.py b/tests/emails_test.py
index 62faf01..6da9b90 100644
--- a/tests/emails_test.py
+++ b/tests/emails_test.py
@@ -1,4 +1,4 @@
-from unittest.mock import MagicMock
+from unittest.mock import MagicMock, Mock
import resend
from resend import EmailsReceiving
@@ -485,3 +485,65 @@ def test_email_send_with_template_and_variables(self) -> None:
}
email: resend.Emails.SendResponse = resend.Emails.send(params)
assert email["id"] == "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794"
+
+ def test_email_send_with_custom_headers(self) -> None:
+ self.set_mock_json(
+ {
+ "id": "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794",
+ }
+ )
+ params: resend.Emails.SendParams = {
+ "to": "to@email.com",
+ "from": "from@email.com",
+ "subject": "subject",
+ "html": "html",
+ "headers": {
+ "X-Entity-Ref-ID": "123456",
+ },
+ }
+ email: resend.Emails.SendResponse = resend.Emails.send(params)
+ assert email["id"] == "49a3999c-0ce1-4ea6-ab68-afcd6dc2e794"
+
+
+import unittest as _unittest
+
+
+class TestEmailHeadersRegression(_unittest.TestCase):
+ """
+ Tests that mock at the HTTP client level to exercise request.py's injection
+ code. ResendBaseTest mocks make_request directly, which bypasses that code
+ and would not have caught the v2.23.0 regression.
+ """
+
+ def setUp(self) -> None:
+ resend.api_key = "re_123"
+
+ def test_receiving_get_email_headers_not_overwritten_by_http_headers(self) -> None:
+ mock_client = Mock()
+ mock_client.request.return_value = (
+ b'{"object":"inbound","id":"67d9bcdb-5a02-42d7-8da9-0d6feea18cff",'
+ b'"to":["received@example.com"],"from":"sender@example.com",'
+ b'"created_at":"2023-04-07T23:13:52.669661+00:00","subject":"Test",'
+ b'"html":null,"text":"hello","bcc":null,"cc":null,"reply_to":null,'
+ b'"message_id":"","headers":{"X-Custom":"email-value"},'
+ b'"attachments":[]}',
+ 200,
+ {
+ "content-type": "application/json",
+ "x-request-id": "req_abc123",
+ },
+ )
+
+ original_client = resend.default_http_client
+ resend.default_http_client = mock_client
+
+ try:
+ email: resend.ReceivedEmail = resend.Emails.Receiving.get(
+ email_id="67d9bcdb-5a02-42d7-8da9-0d6feea18cff",
+ )
+ # Email MIME headers must survive the HTTP headers injection
+ assert email["headers"] == {"X-Custom": "email-value"}
+ # HTTP response headers are available separately
+ assert email["http_headers"]["x-request-id"] == "req_abc123"
+ finally:
+ resend.default_http_client = original_client
diff --git a/tests/response_headers_integration_test.py b/tests/response_headers_integration_test.py
index 2816025..668a3cb 100644
--- a/tests/response_headers_integration_test.py
+++ b/tests/response_headers_integration_test.py
@@ -48,11 +48,11 @@ def test_email_send_response_includes_headers(self) -> None:
assert response.get("from") == "test@example.com"
# Verify new feature - headers are accessible via dict key
- assert "headers" in response
- assert response["headers"]["x-request-id"] == "req_abc123"
- assert response["headers"]["x-ratelimit-limit"] == "100"
- assert response["headers"]["x-ratelimit-remaining"] == "95"
- assert response["headers"]["x-ratelimit-reset"] == "1699564800"
+ assert "http_headers" in response
+ assert response["http_headers"]["x-request-id"] == "req_abc123"
+ assert response["http_headers"]["x-ratelimit-limit"] == "100"
+ assert response["http_headers"]["x-ratelimit-remaining"] == "95"
+ assert response["http_headers"]["x-ratelimit-reset"] == "1699564800"
finally:
# Restore original HTTP client
@@ -82,8 +82,8 @@ def test_list_response_headers(self) -> None:
assert isinstance(response, dict)
assert "data" in response
# Headers are injected into the dict
- assert "headers" in response
- assert response["headers"]["x-request-id"] == "req_xyz"
+ assert "http_headers" in response
+ assert response["http_headers"]["x-request-id"] == "req_xyz"
finally:
resend.default_http_client = original_client