Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
870b449
feat: init custom http client support
drish May 26, 2025
708d608
feat: custom http client foundation
drish May 26, 2025
dbbf76e
fix: mypy types
drish May 26, 2025
82326e4
fix: mypy types
drish May 26, 2025
f178816
fix: some mypy errors
drish May 26, 2025
b168cd5
example: add custom http client example
drish May 26, 2025
13255f6
feat: raise ResendError on custom client error
drish May 26, 2025
4665e82
feat: init async support with httpx
drish Jun 16, 2025
2118c10
fix: misc fixes
drish Jun 16, 2025
026a4f5
tweaks
drish Jul 11, 2025
5a1d2af
merge main
drish Jul 11, 2025
e781250
feat: api keys async module
drish Jul 11, 2025
6487f11
fix: couple of fixes
drish Jul 11, 2025
e6be21a
chore: update readme
drish Jul 11, 2025
b46e064
feat: batch send async module
drish Jul 11, 2025
e741f0a
feat: domains module async
drish Jul 11, 2025
b626f00
feat: audiences module async
drish Jul 16, 2025
dfbd13f
feat: broadcasts async module
drish Jul 16, 2025
b8021b7
feat: contacts async module
drish Jul 16, 2025
0b85867
fix conflicts
drish Feb 23, 2026
afc9051
feat: add async methods for missing packages
drish Feb 23, 2026
7a2c339
chore: update readme
drish Feb 23, 2026
e2e6a91
chore: different testing setup for async pipeline
drish Feb 23, 2026
0d45bd4
feat: automatically select client
drish Mar 10, 2026
b404731
chore: bump version to 2.26.0
drish Mar 20, 2026
a835d5e
chore: merge main into async-support
drish Mar 20, 2026
1db32d7
fix: sync async_request with http_headers and response headers support
drish Mar 20, 2026
d5d2950
test: add async response headers integration tests
drish Mar 20, 2026
754a1b9
chore: move async examples to examples/async and remove stale client …
drish Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,45 @@ params: resend.Emails.SendParams = {
email: resend.Emails.SendResponse = resend.Emails.send(params)
print(email)
```

## Async Support

The SDK supports async operations via `httpx`. Install the async extra:

```bash
pip install resend[async]
```

Once installed, async methods (suffixed with `_async`) work automatically — no extra setup needed:

```py
import asyncio
import resend

resend.api_key = "re_yourkey"

async def main():
params: resend.Emails.SendParams = {
"from": "onboarding@resend.dev",
"to": ["delivered@resend.dev"],
"subject": "hi",
"html": "<strong>hello, world!</strong>",
}

email: resend.Emails.SendResponse = await resend.Emails.send_async(params)
print(email)

if __name__ == "__main__":
asyncio.run(main())
```

### Custom async client

To use a custom async HTTP client or configure options like timeouts, set `resend.default_async_http_client`:

```py
import resend

resend.api_key = "re_yourkey"
resend.default_async_http_client = resend.HTTPXClient(timeout=60)
```
34 changes: 34 additions & 0 deletions examples/async/api_keys_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import asyncio
import os
from typing import List

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>

raise EnvironmentError("RESEND_API_KEY is missing")

# Set up async HTTP client


async def main() -> None:
create_params: resend.ApiKeys.CreateParams = {
"name": "example.com",
}

key = await resend.ApiKeys.create_async(params=create_params)
print("Created new api key")
print(f"Key id: {key['id']} and token: {key['token']}")

keys: resend.ApiKeys.ListResponse = await resend.ApiKeys.list_async()
for k in keys["data"]:
print(k["id"])
print(k["name"])
print(k["created_at"])

if len(keys["data"]) > 0:
await resend.ApiKeys.remove_async(api_key_id=keys["data"][0]["id"])
print(f"Removed api key: {keys['data'][0]['id']}")


if __name__ == "__main__":
asyncio.run(main())
36 changes: 36 additions & 0 deletions examples/async/audiences_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import asyncio
import os

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.

P2: Use os.environ.get(...) for the check so missing variables raise 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/audiences_async.py, line 6:

<comment>Use os.environ.get(...) for the check so missing variables raise the intended EnvironmentError instead of a KeyError.</comment>

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

raise EnvironmentError("RESEND_API_KEY is missing")

# Set up async HTTP client


async def main() -> None:
create_params: resend.Segments.CreateParams = {
"name": "New Segment from Python SDK (Async)",
}
segment: resend.Segments.CreateSegmentResponse = await resend.Segments.create_async(
create_params
)
print(f"Created segment: {segment['id']}")
print(segment)

seg: resend.Segment = await resend.Segments.get_async(segment["id"])
print("Retrieved segment: ", seg)

segments: resend.Segments.ListResponse = await resend.Segments.list_async()
print("List of segments:", [s["id"] for s in segments["data"]])

rmed: resend.Segments.RemoveSegmentResponse = await resend.Segments.remove_async(
id=segment["id"]
)
print("Deleted segment")
print(rmed)


if __name__ == "__main__":
asyncio.run(main())
61 changes: 61 additions & 0 deletions examples/async/batch_email_send_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import asyncio
import os
from typing import List

import resend
import resend.exceptions

if not os.environ["RESEND_API_KEY"]:
raise EnvironmentError("RESEND_API_KEY is missing")

# Set up async HTTP client


async def main() -> None:
params: List[resend.Emails.SendParams] = [
{
"from": "onboarding@resend.dev",
"to": ["delivered@resend.dev"],
"subject": "hey",
"html": "<strong>hello, world!</strong>",
},
{
"from": "onboarding@resend.dev",
"to": ["delivered@resend.dev"],
"subject": "hello",
"html": "<strong>hello, world!</strong>",
},
]

try:
# Send batch emails
print("sending without idempotency_key")
emails: resend.Batch.SendResponse = await resend.Batch.send_async(params)
for email in emails["data"]:
print(f"Email id: {email['id']}")
except resend.exceptions.ResendError as err:
print("Failed to send batch emails")
print(f"Error: {err}")
exit(1)

try:
# Send batch emails with idempotency_key
print("sending with idempotency_key")

options: resend.Batch.SendOptions = {
"idempotency_key": "af477dc78aa9fa91fff3b8c0d4a2e1a5",
}

e: resend.Batch.SendResponse = await resend.Batch.send_async(
params, options=options
)
for email in e["data"]:
print(f"Email id: {email['id']}")
except resend.exceptions.ResendError as err:
print("Failed to send batch emails")
print(f"Error: {err}")
exit(1)


if __name__ == "__main__":
asyncio.run(main())
76 changes: 76 additions & 0 deletions examples/async/broadcasts_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import asyncio
import os
from typing import List

import resend
import resend.broadcasts

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.

P2: Use os.environ.get (or os.getenv) so missing keys trigger 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/broadcasts_async.py, line 8:

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

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

raise EnvironmentError("RESEND_API_KEY is missing")

# Set up async HTTP client

# replace with some existing audience id
audience_id: str = "78b8d3bc-a55a-45a3-aee6-6ec0a5e13d7e"


async def main() -> None:
create_params: resend.Broadcasts.CreateParams = {
"audience_id": audience_id,
"from": "onboarding@resend.dev",
"subject": "Hello, world! (Async)",
"html": "<p>Hello, world!</p>",
"text": "Hello, world!",
"reply_to": ["foo@resend.dev", "bar@resend.dev"],
"name": "Hello, world! (Async)",
}

broadcast: resend.Broadcasts.CreateResponse = await resend.Broadcasts.create_async(
create_params
)
print("Created broadcast !")
print(broadcast)

update_params: resend.Broadcasts.UpdateParams = {
"broadcast_id": broadcast["id"],
"html": "<p>Hello, world! Updated (Async)</p>",
"text": "Hello, world! Updated (Async)",
"name": "Hello, world! Updated (Async)",
}

updated_broadcast: resend.Broadcasts.UpdateResponse = (
await resend.Broadcasts.update_async(update_params)
)
print("Updated broadcast!")
print(updated_broadcast)

send_params: resend.Broadcasts.SendParams = {
"broadcast_id": broadcast["id"],
}
sent: resend.Broadcasts.SendResponse = await resend.Broadcasts.send_async(
send_params
)
print("Sent broadcast !\n")
print(sent)

retrieved: resend.Broadcast = await resend.Broadcasts.get_async(id=broadcast["id"])
print("retrieved broadcast !\n")
print(retrieved)

if retrieved["status"] == "draft":
removed: resend.Broadcasts.RemoveResponse = (
await resend.Broadcasts.remove_async(id=broadcast["id"])
)
print("Removed broadcast !\n")
print(removed)
print("\n")
else:
print("Broadcast is not in draft status, cannot remove it.\n")

list_response: resend.Broadcasts.ListResponse = await resend.Broadcasts.list_async()
print("List of broadcasts !\n")
print(list_response)


if __name__ == "__main__":
asyncio.run(main())
75 changes: 75 additions & 0 deletions examples/async/contacts_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import asyncio
import os

import resend

if not os.environ["RESEND_API_KEY"]:
raise EnvironmentError("RESEND_API_KEY is missing")

# Set up async HTTP client

# replace with some audience id
audience_id: str = "ca4e37c5-a82a-4199-a3b8-bf912a6472aa"


async def main() -> None:
create_params: resend.Contacts.CreateParams = {
"audience_id": audience_id,
"email": "sw@exmple.com",
"first_name": "Steve",
"last_name": "Wozniak",
"unsubscribed": False,
}

contact: resend.Contacts.CreateContactResponse = await resend.Contacts.create_async(
create_params
)
print("Created contact !")
print(contact)

update_params: resend.Contacts.UpdateParams = {
"audience_id": audience_id,
"id": contact["id"],
"unsubscribed": False,
"first_name": "Steve (Async)",
}

updated: resend.Contacts.UpdateContactResponse = await resend.Contacts.update_async(
update_params
)
print("updated contact !")
print(updated)

cont_by_id: resend.Contact = await resend.Contacts.get_async(
id=contact["id"], audience_id=audience_id
)
print("Retrieved contact by ID")
print(cont_by_id)

cont_by_email: resend.Contact = await resend.Contacts.get_async(
email="sw@exmple.com", audience_id=audience_id
)
print("Retrieved contact by Email")
print(cont_by_email)

contacts: resend.Contacts.ListResponse = await resend.Contacts.list_async(
audience_id=audience_id
)
print("List of contacts")
for c in contacts["data"]:
print(c)

# remove by email
rmed = await resend.Contacts.remove_async(
audience_id=audience_id, email=cont_by_email["email"]
)

# remove by id
# rmed: resend.Contact = await resend.Contacts.remove_async(audience_id=audience_id, id=cont["id"])

print(f"Removed contact")
print(rmed)


if __name__ == "__main__":
asyncio.run(main())
55 changes: 55 additions & 0 deletions examples/async/domains_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import asyncio
import os

import resend

if not os.environ["RESEND_API_KEY"]:
raise EnvironmentError("RESEND_API_KEY is missing")

# Set up async HTTP client


async def main() -> None:
create_params: resend.Domains.CreateParams = {
"name": "example.com",
"region": "us-east-1",
"custom_return_path": "outbound",
}
domain: resend.Domains.CreateDomainResponse = await resend.Domains.create_async(
params=create_params
)
print(domain)

retrieved: resend.Domain = await resend.Domains.get_async(domain_id=domain["id"])
if retrieved["records"] is not None:
for record in retrieved["records"]:
print(record)

update_params: resend.Domains.UpdateParams = {
"id": domain["id"],
"open_tracking": True,
"click_tracking": True,
"tls": "enforced",
}

updated_domain: resend.Domain = await resend.Domains.update_async(update_params)
print(f"Updated domain: {updated_domain['id']}")

domains: resend.Domains.ListResponse = await resend.Domains.list_async()
if not domains:
print("No domains found")
for d in domains["data"]:
print(d)

verified_domain: resend.Domain = await resend.Domains.verify_async(
domain_id=domain["id"]
)
print("Verified")
print(verified_domain)

rm_domain: resend.Domain = await resend.Domains.remove_async(domain_id=domain["id"])
print(rm_domain)


if __name__ == "__main__":
asyncio.run(main())
Loading
Loading