Skip to content

feat: Async Support with httpx 🔥 #149

Merged
drish merged 29 commits intomainfrom
async-support
Mar 20, 2026
Merged

feat: Async Support with httpx 🔥 #149
drish merged 29 commits intomainfrom
async-support

Conversation

@drish
Copy link
Member

@drish drish commented Jun 16, 2025

Introduces async support using the httpx library.

  • Base extra_requires setup
  • Async requests module setup
  • Api Keys module
  • Audiences module
  • Broadcasts module
  • Contacts module
  • Domains module
  • Emails module
  • Batch Emails module

SDK async version

  1. Users will need to install the async version of the sdk (extra-requries) with: pip install resend[async]

  2. Set the default_http_client:

import resend
# Set up async HTTP client
resend.default_http_client = resend.HTTPXClient()
  1. call the async version method (suffixed with _async):
email: resend.Email = await resend.Emails.send_async(params)

@pedroimpulcetto
Copy link
Contributor

wow, I loved this Async support!!! great job @drish

Base automatically changed from feat/custom-http-client to main July 7, 2025 17:46
@drish drish marked this pull request as ready for review July 18, 2025 15:19
@ojh
Copy link

ojh commented Nov 19, 2025

@bukinoshita @felipevolpone Is there any update on this? Are there any outstanding tasks that any of us could potentially help with to get this PR unblocked and merged?

@drish
Copy link
Member Author

drish commented Nov 21, 2025

@bukinoshita @felipevolpone Is there any update on this? Are there any outstanding tasks that any of us could potentially help with to get this PR unblocked and merged?

@ojh i had to re-prioritize some work and this ended up having to wait a bit. I'll be working on this back again this coming week, should be very close to wrap up.

@tchaguitos
Copy link

nice! i was thinking about this and now i am happy because this pull request. thanks

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 issues found across 23 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="resend/contacts/_contacts.py">

<violation number="1" location="resend/contacts/_contacts.py:379">
P2: Custom agent: **API Key Permission Check SDK Methods**

New async contact paths now call the global /contacts endpoints. Please confirm that production API keys have the required permissions for global contact operations to avoid permission-related failures after deployment (API Key Permission Check SDK Methods rule).</violation>
</file>

<file name="resend/emails/_attachments.py">

<violation number="1" location="resend/emails/_attachments.py:133">
P2: AsyncRequest can be undefined when async extras aren’t installed, causing a NameError on get_async. Import it inside the method (or raise a clear ImportError) to fail fast with a clear message.</violation>
</file>

<file name="tests/contacts_segments_async_test.py">

<violation number="1" location="tests/contacts_segments_async_test.py:8">
P2: Async test methods on a `unittest.TestCase` base are not awaited, so these tests won’t run and will report false positives. Use `IsolatedAsyncioTestCase` (or a pytest async marker) for async tests.</violation>
</file>

<file name="tests/contact_properties_async_test.py">

<violation number="1" location="tests/contact_properties_async_test.py:8">
P2: These async tests inherit from `unittest.TestCase` via `ResendBaseTest`, so the `async def` test methods are never awaited by the unittest runner. The assertions inside the new tests won’t execute. Use an async-capable base like `unittest.IsolatedAsyncioTestCase` or convert these to pytest-style async tests.</violation>
</file>

<file name="resend/webhooks/_webhooks.py">

<violation number="1" location="resend/webhooks/_webhooks.py:370">
P2: Async methods call `AsyncRequest` even when the optional async dependency isn’t installed, which will raise `NameError` at runtime. Add an explicit guard (and/or define `AsyncRequest = None` on import failure) to raise a clear ImportError before trying to use it.</violation>
</file>

<file name="tests/templates_async_test.py">

<violation number="1" location="tests/templates_async_test.py:8">
P2: Async test methods are defined on a `unittest.TestCase` subclass, which doesn’t await async tests. These tests will be skipped or treated as coroutines rather than executed. Use an async-aware base class (e.g., `unittest.IsolatedAsyncioTestCase`) or mark the tests for an async test runner (e.g., pytest with `@pytest.mark.asyncio`) and adjust the base class accordingly.</violation>
</file>

<file name="resend/contact_properties/_contact_properties.py">

<violation number="1" location="resend/contact_properties/_contact_properties.py:284">
P2: Guard async methods against missing `resend[async]` so callers get a clear ImportError instead of a NameError when AsyncRequest isn’t available.</violation>
</file>

<file name="resend/contacts/_topics.py">

<violation number="1" location="resend/contacts/_topics.py:217">
P2: AsyncRequest is optional but the ImportError is swallowed, leaving AsyncRequest undefined. Calling list_async/update_async without the async extra will raise a NameError instead of a clear ImportError. Add an explicit guard or error when the async dependency is missing.</violation>
</file>

<file name="resend/topics/_topics.py">

<violation number="1" location="resend/topics/_topics.py:251">
P2: `AsyncRequest` is silently ignored on ImportError, so calling any `*_async` method without the async extra raises `NameError` instead of a clear dependency error. Add a guard or explicit ImportError for missing async extras.</violation>
</file>

<file name="tests/webhooks_async_test.py">

<violation number="1" location="tests/webhooks_async_test.py:8">
P2: Async tests in a unittest.TestCase subclass won’t be awaited, so these new async tests won’t actually run (they’ll return coroutine objects and be treated as passing). Use an async-capable base (e.g., IsolatedAsyncioTestCase) or pytest-asyncio markers instead.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@drish drish requested review from gabrielmfern and removed request for bukinoshita and felipevolpone February 23, 2026 14:52
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

26 issues found across 50 files

Confidence score: 3/5

  • There is a concrete runtime risk in multiple async code paths (for example resend/emails/_emails.py, resend/emails/_batch.py, and resend/domains/_domains.py): when the async extra is not installed, AsyncRequest can be undefined and _async calls may fail with NameError instead of a clear guidance error.
  • Several new async examples (examples/async/batch_email_send_async.py, examples/async/audiences_async.py, examples/async/contacts_async.py, examples/async/domains_async.py) depend on specific API-key scopes, so production usage can fail with permission errors if keys are not provisioned correctly.
  • Given the medium-high severity and strong confidence on user-facing failure modes, this carries some regression risk, though fixes appear straightforward (explicit async dependency guards and clearer permission prerequisites).
  • Pay close attention to resend/emails/_emails.py, resend/emails/_batch.py, resend/domains/_domains.py, resend/emails/_attachments.py, resend/topics/_topics.py, resend/contact_properties/_contact_properties.py - async methods should fail gracefully with a clear ImportError when async support is missing.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="examples/async/batch_email_send_async.py">

<violation number="1" location="examples/async/batch_email_send_async.py:8">
P3: Use os.environ.get(...) (and optionally assign it) so missing RESEND_API_KEY triggers the intended error instead of a KeyError.</violation>

<violation number="2" location="examples/async/batch_email_send_async.py:33">
P1: Custom agent: **API Key Permission Check SDK Methods**

The new async batch send example uses Resend’s Batch.send_async. Confirm that production API keys have the required permissions for batch email sending before rollout to avoid permission failures. (Rule: API Key Permission Check SDK Methods)</violation>
</file>

<file name="examples/async/audiences_async.py">

<violation number="1" location="examples/async/audiences_async.py:6">
P2: Use os.environ.get(...) for the check so missing variables raise the intended EnvironmentError instead of a KeyError.</violation>

<violation number="2" location="examples/async/audiences_async.py:16">
P1: Custom agent: **API Key Permission Check SDK Methods**

Add a reminder that API keys must have segment management permissions for these new async operations to avoid permission failures.</violation>
</file>

<file name="examples/async/contacts_async.py">

<violation number="1" location="examples/async/contacts_async.py:24">
P1: Custom agent: **API Key Permission Check SDK Methods**

Confirm that the production API keys have the required Contacts/Audiences permissions for these new async operations to avoid permission-related failures after deployment.</violation>
</file>

<file name="examples/async/domains_async.py">

<violation number="1" location="examples/async/domains_async.py:18">
P1: Custom agent: **API Key Permission Check SDK Methods**

These newly added async domain-management calls introduce new provider SDK operations; confirm production API keys have the required domain-management permissions to avoid permission failures after deployment (Rule: API Key Permission Check SDK Methods).</violation>
</file>

<file name="examples/async/receiving_email_async.py">

<violation number="1" location="examples/async/receiving_email_async.py:7">
P3: Use `os.environ.get` (or `os.getenv`) so the missing API key triggers the intended EnvironmentError instead of a KeyError.</violation>
</file>

<file name="resend/contacts/_contacts.py">

<violation number="1" location="resend/contacts/_contacts.py:381">
P2: Guard async methods when `resend[async]` is missing so they raise a clear ImportError instead of `NameError`.</violation>
</file>

<file name="resend/emails/_emails.py">

<violation number="1" location="resend/emails/_emails.py:387">
P2: If the async extra isn’t installed, `AsyncRequest` is undefined and calling this method raises a `NameError`. Add an explicit guard to raise a helpful ImportError when async support is missing.</violation>
</file>

<file name="resend/emails/_batch.py">

<violation number="1" location="resend/emails/_batch.py:122">
P2: Guard the async path when the optional AsyncRequest import is unavailable; otherwise send_async will crash with NameError when the async extra isn’t installed.</violation>
</file>

<file name="resend/webhooks/_webhooks.py">

<violation number="1" location="resend/webhooks/_webhooks.py:370">
P2: Guard async methods when the async extra isn’t installed; otherwise calling create_async/get_async raises NameError because AsyncRequest is undefined.</violation>
</file>

<file name="examples/async/api_keys_async.py">

<violation number="1" location="examples/async/api_keys_async.py:7">
P3: Use os.getenv (or os.environ.get) to avoid a KeyError when RESEND_API_KEY is missing so the intended EnvironmentError is raised.</violation>
</file>

<file name="resend/segments/_segments.py">

<violation number="1" location="resend/segments/_segments.py:14">
P2: Swallowing the ImportError leaves `AsyncRequest` undefined, so calling any async method without the async extra will crash with a `NameError`. Raise a clear error (or guard in the async methods) so missing extras fail predictably.</violation>
</file>

<file name="tests/contacts_async_test.py">

<violation number="1" location="tests/contacts_async_test.py:68">
P2: This test will pass even if `update_async` doesn’t raise because there’s no failure path when no exception occurs. Use `pytest.raises` to make the expectation explicit.</violation>
</file>

<file name="resend/contacts/segments/_contact_segments.py">

<violation number="1" location="resend/contacts/segments/_contact_segments.py:268">
P2: Guard the async methods when AsyncRequest isn’t available. Right now a missing async extra will surface as a NameError instead of a clear install error.</violation>
</file>

<file name="resend/domains/_domains.py">

<violation number="1" location="resend/domains/_domains.py:270">
P2: Guard async methods with a clear ImportError when `AsyncRequest` isn't available; otherwise calling these methods without `resend[async]` raises a `NameError`.</violation>
</file>

<file name="resend/emails/_attachments.py">

<violation number="1" location="resend/emails/_attachments.py:133">
P2: Calling the async methods will raise `NameError` when the optional async dependency is not installed because `AsyncRequest` is never defined after the import `pass`. Add a guard or explicit error so users get a clear ImportError instead of a runtime NameError.</violation>
</file>

<file name="examples/async/broadcasts_async.py">

<violation number="1" location="examples/async/broadcasts_async.py:8">
P2: Use `os.environ.get` (or `os.getenv`) so missing keys trigger the intended `EnvironmentError` instead of a `KeyError`.</violation>
</file>

<file name="resend/topics/_topics.py">

<violation number="1" location="resend/topics/_topics.py:251">
P2: `AsyncRequest` can be undefined when the async extra isn’t installed, so calling this async method will raise `NameError`. Add an explicit guard/ImportError so users get a clear message when async support isn’t available.</violation>
</file>

<file name="resend/api_keys/_api_keys.py">

<violation number="1" location="resend/api_keys/_api_keys.py:167">
P2: Guard async methods when AsyncRequest isn’t available so callers get a clear ImportError instead of a NameError.</violation>
</file>

<file name="resend/contact_properties/_contact_properties.py">

<violation number="1" location="resend/contact_properties/_contact_properties.py:13">
P2: Swallowing the ImportError leaves `AsyncRequest` undefined, so any `_async` call crashes with a NameError/TypeError. Define a sentinel and raise a clear ImportError when async methods are used without the extra installed.</violation>
</file>

<file name="resend/emails/_receiving.py">

<violation number="1" location="resend/emails/_receiving.py:173">
P2: Guard async methods when AsyncRequest is unavailable to avoid a NameError and raise a clear ImportError for missing async extras.</violation>
</file>

<file name="examples/async/simple_email_async.py">

<violation number="1" location="examples/async/simple_email_async.py:6">
P2: Avoid direct os.environ indexing here; it throws KeyError when RESEND_API_KEY is unset, so your custom error is never raised.</violation>
</file>

<file name="resend/contacts/_topics.py">

<violation number="1" location="resend/contacts/_topics.py:217">
P2: Guard async calls with a clear ImportError when the async extra isn’t installed; otherwise calling list_async will raise NameError.</violation>
</file>

<file name="resend/broadcasts/_broadcasts.py">

<violation number="1" location="resend/broadcasts/_broadcasts.py:394">
P2: Guard async methods when AsyncRequest isn’t available. With the current silent ImportError pass, calling a *_async method without the async extra raises a NameError instead of a clear install hint.</violation>
</file>

<file name="resend/templates/_templates.py">

<violation number="1" location="resend/templates/_templates.py:362">
P2: Guard async methods when the async extra isn’t installed. As written, calling this method without `resend[async]` raises NameError because AsyncRequest isn’t defined.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

else:
path = "/contacts"

resp = await AsyncRequest[Contacts.CreateContactResponse](
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Guard async methods when resend[async] is missing so they raise a clear ImportError instead of NameError.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At resend/contacts/_contacts.py, line 381:

<comment>Guard async methods when `resend[async]` is missing so they raise a clear ImportError instead of `NameError`.</comment>

<file context>
@@ -344,3 +355,161 @@ def remove(
+        else:
+            path = "/contacts"
+
+        resp = await AsyncRequest[Contacts.CreateContactResponse](
+            path=path, params=cast(Dict[Any, Any], params), verb="post"
+        ).perform_with_content()
</file context>

CreateResponse: The new broadcast object response
"""
path = "/broadcasts"
resp = await AsyncRequest[Broadcasts.CreateResponse](
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Guard async methods when AsyncRequest isn’t available. With the current silent ImportError pass, calling a *_async method without the async extra raises a NameError instead of a clear install hint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At resend/broadcasts/_broadcasts.py, line 394:

<comment>Guard async methods when AsyncRequest isn’t available. With the current silent ImportError pass, calling a *_async method without the async extra raises a NameError instead of a clear install hint.</comment>

<file context>
@@ -371,3 +377,113 @@ def remove(cls, id: str) -> RemoveResponse:
+            CreateResponse: The new broadcast object response
+        """
+        path = "/broadcasts"
+        resp = await AsyncRequest[Broadcasts.CreateResponse](
+            path=path, params=cast(Dict[Any, Any], params), verb="post"
+        ).perform_with_content()
</file context>

CreateResponse: The created template response with ID and object type.
"""
path = "/templates"
resp = await AsyncRequest[Templates.CreateResponse](
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Guard async methods when the async extra isn’t installed. As written, calling this method without resend[async] raises NameError because AsyncRequest isn’t defined.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At resend/templates/_templates.py, line 362:

<comment>Guard async methods when the async extra isn’t installed. As written, calling this method without `resend[async]` raises NameError because AsyncRequest isn’t defined.</comment>

<file context>
@@ -341,3 +347,119 @@ def remove(cls, template_id: str) -> RemoveResponse:
+            CreateResponse: The created template response with ID and object type.
+        """
+        path = "/templates"
+        resp = await AsyncRequest[Templates.CreateResponse](
+            path=path, params=cast(Dict[Any, Any], params), verb="post"
+        ).perform_with_content()
</file context>

import resend
from resend import EmailsReceiving

if not os.environ["RESEND_API_KEY"]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Use os.environ.get (or os.getenv) so the missing API key triggers the intended EnvironmentError instead of a KeyError.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/async/receiving_email_async.py, line 7:

<comment>Use `os.environ.get` (or `os.getenv`) so the missing API key triggers the intended EnvironmentError instead of a KeyError.</comment>

<file context>
@@ -0,0 +1,163 @@
+import resend
+from resend import EmailsReceiving
+
+if not os.environ["RESEND_API_KEY"]:
+    raise EnvironmentError("RESEND_API_KEY is missing")
+
</file context>


import resend

if not os.environ["RESEND_API_KEY"]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Use os.getenv (or os.environ.get) to avoid a KeyError when RESEND_API_KEY is missing so the intended EnvironmentError is raised.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/async/api_keys_async.py, line 7:

<comment>Use os.getenv (or os.environ.get) to avoid a KeyError when RESEND_API_KEY is missing so the intended EnvironmentError is raised.</comment>

<file context>
@@ -0,0 +1,34 @@
+
+import resend
+
+if not os.environ["RESEND_API_KEY"]:
+    raise EnvironmentError("RESEND_API_KEY is missing")
+
</file context>

@drish drish merged commit f498033 into main Mar 20, 2026
20 checks passed
@drish drish deleted the async-support branch March 20, 2026 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants