Skip to content

Commit 374b794

Browse files
committed
feat: idempotency key support for batch send
1 parent 4bf36f8 commit 374b794

3 files changed

Lines changed: 82 additions & 5 deletions

File tree

examples/batch_email_send.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,28 @@
2424
]
2525

2626
try:
27+
# Send batch emails
28+
print("sending without idempotency_key")
2729
emails: resend.Batch.SendResponse = resend.Batch.send(params)
2830
for email in emails["data"]:
2931
print(f"Email id: {email['id']}")
30-
except resend.exceptions.ResendError as e:
32+
except resend.exceptions.ResendError as err:
3133
print("Failed to send batch emails")
32-
print(f"Error: {e}")
34+
print(f"Error: {err}")
35+
exit(1)
36+
37+
try:
38+
# Send batch emails with idempotency_key
39+
print("sending with idempotency_key")
40+
41+
options: resend.Batch.SendOptions = {
42+
"idempotency_key": "af477dc78aa9fa91fff3b8c0d4a2e1a5",
43+
}
44+
45+
e: resend.Batch.SendResponse = resend.Batch.send(params, options=options)
46+
for email in e["data"]:
47+
print(f"Email id: {email['id']}")
48+
except resend.exceptions.ResendError as err:
49+
print("Failed to send batch emails")
50+
print(f"Error: {err}")
3351
exit(1)

resend/emails/_batch.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, List, cast
1+
from typing import Any, Dict, List, NotRequired, Optional, cast
22

33
from typing_extensions import TypedDict
44

@@ -8,6 +8,15 @@
88
from ._emails import Emails
99

1010

11+
class _SendOptions(TypedDict):
12+
idempotency_key: NotRequired[str]
13+
"""
14+
Unique key that ensures the same operation is not processed multiple times.
15+
Allows for safe retries without duplicating operations.
16+
If provided, will be sent as the `Idempotency-Key` header.
17+
"""
18+
19+
1120
class _SendResponse(TypedDict):
1221
data: List[Email]
1322
"""
@@ -17,6 +26,16 @@ class _SendResponse(TypedDict):
1726

1827
class Batch:
1928

29+
class SendOptions(_SendOptions):
30+
"""
31+
SendOptions is the class that wraps the options for the batch send method.
32+
33+
Attributes:
34+
idempotency_key (NotRequired[str]): Unique key that ensures the same operation is not processed multiple times.
35+
Allows for safe retries without duplicating operations.
36+
If provided, will be sent as the `Idempotency-Key` header.
37+
"""
38+
2039
class SendResponse(_SendResponse):
2140
"""
2241
SendResponse type that wraps a list of email objects
@@ -26,20 +45,26 @@ class SendResponse(_SendResponse):
2645
"""
2746

2847
@classmethod
29-
def send(cls, params: List[Emails.SendParams]) -> SendResponse:
48+
def send(
49+
cls, params: List[Emails.SendParams], options: Optional[SendOptions] = None
50+
) -> SendResponse:
3051
"""
3152
Trigger up to 100 batch emails at once.
3253
see more: https://resend.com/docs/api-reference/emails/send-batch-emails
3354
3455
Args:
3556
params (List[Emails.SendParams]): The list of emails to send
57+
options (Optional[SendOptions]): Batch options, ie: idempotency_key
3658
3759
Returns:
3860
SendResponse: A list of email objects
3961
"""
4062
path = "/emails/batch"
4163

4264
resp = request.Request[_SendResponse](
43-
path=path, params=cast(List[Dict[Any, Any]], params), verb="post"
65+
path=path,
66+
params=cast(List[Dict[Any, Any]], params),
67+
verb="post",
68+
options=cast(Dict[Any, Any], options),
4469
).perform_with_content()
4570
return resp

tests/batch_emails_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,40 @@ def test_batch_email_send(self) -> None:
3838
assert emails["data"][0]["id"] == "ae2014de-c168-4c61-8267-70d2662a1ce1"
3939
assert emails["data"][1]["id"] == "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb"
4040

41+
def test_batch_email_send_with_options(self) -> None:
42+
self.set_mock_json(
43+
{
44+
"data": [
45+
{"id": "ae2014de-c168-4c61-8267-70d2662a1ce1"},
46+
{"id": "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb"},
47+
]
48+
}
49+
)
50+
51+
params: List[resend.Emails.SendParams] = [
52+
{
53+
"from": "from@resend.dev",
54+
"to": ["to@resend.dev"],
55+
"subject": "hey",
56+
"html": "<strong>hello, world!</strong>",
57+
},
58+
{
59+
"from": "from@resend.dev",
60+
"to": ["to@resend.dev"],
61+
"subject": "hello",
62+
"html": "<strong>hello, world!</strong>",
63+
},
64+
]
65+
66+
options: resend.Emails.SendOptions = {
67+
"idempotency_key": "af477dc78aa9fa91fff3b8c0d4a2e1a5",
68+
}
69+
70+
emails: resend.Batch.SendResponse = resend.Batch.send(params, options=options)
71+
assert len(emails["data"]) == 2
72+
assert emails["data"][0]["id"] == "ae2014de-c168-4c61-8267-70d2662a1ce1"
73+
assert emails["data"][1]["id"] == "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb"
74+
4175
def test_should_send_batch_email_raise_exception_when_no_content(self) -> None:
4276
self.set_mock_json(None)
4377
params: List[resend.Emails.SendParams] = [

0 commit comments

Comments
 (0)