Skip to content

Commit 546ab8c

Browse files
authored
feat: segments API and contact changes (#177)
1 parent 1386a6d commit 546ab8c

20 files changed

Lines changed: 1248 additions & 64 deletions

examples/contacts_global.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import os
2+
3+
import resend
4+
5+
if not os.environ["RESEND_API_KEY"]:
6+
raise EnvironmentError("RESEND_API_KEY is missing")
7+
8+
print("\nGlobal contacts are organization-wide and support custom properties.")
9+
print("They are not tied to any specific audience.\n")
10+
11+
# Create a global contact with properties
12+
print("\n--- Creating global contact with custom properties ---")
13+
create_params: resend.Contacts.CreateParams = {
14+
"email": "global.user@example.com",
15+
"first_name": "Global",
16+
"last_name": "User",
17+
"unsubscribed": False,
18+
"properties": {
19+
"tier": "premium",
20+
"role": "admin",
21+
"signup_source": "website",
22+
},
23+
}
24+
25+
contact: resend.Contacts.CreateContactResponse = resend.Contacts.create(create_params)
26+
print(f"Created global contact with ID: {contact['id']}")
27+
print(contact)
28+
29+
# Update global contact with new properties
30+
print("\n--- Updating global contact ---")
31+
update_params: resend.Contacts.UpdateParams = {
32+
"id": contact["id"],
33+
"first_name": "Updated Global",
34+
"properties": {
35+
"tier": "enterprise", # Update existing property
36+
"role": "super_admin", # Update existing property
37+
"last_login": "2024-01-15", # Add new property
38+
},
39+
}
40+
41+
updated: resend.Contacts.UpdateContactResponse = resend.Contacts.update(update_params)
42+
print(f"Updated contact with ID: {updated['id']}")
43+
print(updated)
44+
45+
# Get contact by ID
46+
print("\n--- Retrieving global contact by ID ---")
47+
cont_by_id: resend.Contact = resend.Contacts.get(id=contact["id"])
48+
print("Retrieved contact by ID:")
49+
print(cont_by_id)
50+
if "properties" in cont_by_id:
51+
print(f"Custom properties: {cont_by_id['properties']}")
52+
53+
# List all global contacts
54+
print("\n--- Listing all global contacts ---")
55+
contacts: resend.Contacts.ListResponse = resend.Contacts.list()
56+
print(f"Found {len(contacts['data'])} global contacts")
57+
print(f"Has more contacts: {contacts['has_more']}")
58+
for c in contacts["data"]:
59+
print(f" - {c['email']} (ID: {c['id']})")
60+
if "properties" in c:
61+
print(f" Properties: {c.get('properties')}")
62+
63+
# List with pagination
64+
print("\n--- Using pagination parameters ---")
65+
if contacts["data"]:
66+
paginated_params: resend.Contacts.ListParams = {
67+
"limit": 2,
68+
"after": contacts["data"][0]["id"],
69+
}
70+
paginated_contacts: resend.Contacts.ListResponse = resend.Contacts.list(
71+
params=paginated_params
72+
)
73+
print(f"Retrieved {len(paginated_contacts['data'])} contacts with pagination")
74+
print(f"Has more contacts: {paginated_contacts['has_more']}")
75+
else:
76+
print("No contacts available for pagination example")
77+
78+
print("\n--- Removing global contact by ID ---")
79+
rmed: resend.Contacts.RemoveContactResponse = resend.Contacts.remove(id=contact["id"])
80+
print(f"Removed contact with ID: {rmed['contact']}")
81+
print(rmed)

examples/segments.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import os
2+
from typing import List
3+
4+
import resend
5+
6+
if not os.environ["RESEND_API_KEY"]:
7+
raise EnvironmentError("RESEND_API_KEY is missing")
8+
9+
# Create a new segment
10+
create_params: resend.Segments.CreateParams = {
11+
"name": "VIP Newsletter Subscribers",
12+
}
13+
segment: resend.Segments.CreateSegmentResponse = resend.Segments.create(create_params)
14+
print(f"\n✓ Created segment: {segment['id']}")
15+
print(f" Name: {segment['name']}")
16+
17+
# Get segment details
18+
seg: resend.Segment = resend.Segments.get(segment["id"])
19+
print(f"\n✓ Retrieved segment: {seg['name']}")
20+
print(f" Created at: {seg['created_at']}")
21+
22+
# List all segments
23+
segments: resend.Segments.ListResponse = resend.Segments.list()
24+
print(f"\n✓ List of segments: {[s['name'] for s in segments['data']]}")
25+
print(f" Has more: {segments['has_more']}")
26+
27+
# List with pagination
28+
if segments["data"]:
29+
paginated_params: resend.Segments.ListParams = {
30+
"limit": 5,
31+
"after": segments["data"][0]["id"],
32+
}
33+
paginated_segments: resend.Segments.ListResponse = resend.Segments.list(
34+
params=paginated_params
35+
)
36+
print(f"\n✓ Paginated results: {len(paginated_segments['data'])} segments")
37+
38+
# First, create a contact (using the legacy audience_id approach)
39+
# Note: Contacts API still uses audience_id, not segment_id
40+
contact_params: resend.Contacts.CreateParams = {
41+
"audience_id": segment["id"], # Segments and audiences are now the same
42+
"email": "vip@example.com",
43+
"first_name": "VIP",
44+
"last_name": "User",
45+
}
46+
contact = resend.Contacts.create(contact_params)
47+
print(f"\n✓ Created contact: {contact['id']}")
48+
49+
# Add contact to segment (using the new Contacts.segments sub-API)
50+
add_params: resend.ContactSegments.AddParams = {
51+
"segment_id": segment["id"],
52+
"contact_id": contact["id"],
53+
}
54+
add_response = resend.Contacts.Segments.add(add_params)
55+
print(f"\n✓ Added contact to segment: {add_response['id']}")
56+
57+
# Alternative: Add by email instead of contact_id
58+
add_by_email_params: resend.ContactSegments.AddParams = {
59+
"segment_id": segment["id"],
60+
"email": "another-vip@example.com",
61+
}
62+
# add_response2 = resend.Contacts.Segments.add(add_by_email_params)
63+
# print(f"✓ Added contact (by email) to segment: {add_response2['id']}")
64+
65+
# List all segments for a contact
66+
list_params: resend.ContactSegments.ListParams = {
67+
"contact_id": contact["id"],
68+
}
69+
contact_segments = resend.Contacts.Segments.list(list_params)
70+
print(f"\n✓ Contact is in {len(contact_segments['data'])} segment(s):")
71+
for cs in contact_segments["data"]:
72+
print(f" - {cs['name']} (ID: {cs['id']})")
73+
74+
# Alternative: List by email
75+
list_by_email_params: resend.ContactSegments.ListParams = {
76+
"email": "vip@example.com",
77+
}
78+
# contact_segments2 = resend.Contacts.Segments.list(list_by_email_params)
79+
80+
# Remove contact from segment
81+
remove_params: resend.ContactSegments.RemoveParams = {
82+
"segment_id": segment["id"],
83+
"contact_id": contact["id"],
84+
}
85+
remove_response = resend.Contacts.Segments.remove(remove_params)
86+
print(f"\n✓ Removed contact from segment")
87+
print(f" Deleted: {remove_response['deleted']}")
88+
89+
# Clean up: Remove the contact
90+
resend.Contacts.remove(audience_id=segment["id"], id=contact["id"])
91+
print(f"\n✓ Deleted contact: {contact['id']}")
92+
93+
# Clean up: Remove the segment
94+
rmed: resend.Segments.RemoveSegmentResponse = resend.Segments.remove(id=segment["id"])
95+
print(f"\n✓ Deleted segment: {segment['id']}")
96+
print(f" Deleted: {rmed['deleted']}")

resend/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from .contacts._contact_topic import ContactTopic, TopicSubscriptionUpdate
1313
from .contacts._contacts import Contacts
1414
from .contacts._topics import Topics as ContactsTopics
15+
from .contacts.segments._contact_segment import ContactSegment
16+
from .contacts.segments._contact_segments import ContactSegments
1517
from .domains._domain import Domain
1618
from .domains._domains import Domains
1719
from .emails._attachment import Attachment, RemoteAttachment
@@ -26,6 +28,8 @@
2628
from .http_client import HTTPClient
2729
from .http_client_requests import RequestsClient
2830
from .request import Request
31+
from .segments._segment import Segment
32+
from .segments._segments import Segments
2933
from .templates._template import Template, TemplateListItem, Variable
3034
from .templates._templates import Templates
3135
from .topics._topic import Topic
@@ -54,12 +58,15 @@
5458
"Contacts",
5559
"ContactProperties",
5660
"Broadcasts",
61+
"Segments",
5762
"Templates",
5863
"Webhooks",
5964
"Topics",
6065
# Types
6166
"Audience",
6267
"Contact",
68+
"ContactSegment",
69+
"ContactSegments",
6370
"ContactProperty",
6471
"ContactTopic",
6572
"TopicSubscriptionUpdate",
@@ -71,6 +78,7 @@
7178
"EmailTemplate",
7279
"Tag",
7380
"Broadcast",
81+
"Segment",
7482
"Template",
7583
"TemplateListItem",
7684
"Variable",

resend/audiences/_audiences.py

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
from typing import Any, Dict, List, Optional, cast
1+
import warnings
2+
from typing import List, Optional
23

34
from typing_extensions import NotRequired, TypedDict
45

5-
from resend import request
6-
from resend.pagination_helper import PaginationHelper
6+
from resend.segments._segments import Segments
77

88
from ._audience import Audience
99

@@ -114,13 +114,16 @@ def create(cls, params: CreateParams) -> CreateAudienceResponse:
114114
115115
Returns:
116116
CreateAudienceResponse: The created audience response
117-
"""
118117
119-
path = "/audiences"
120-
resp = request.Request[Audiences.CreateAudienceResponse](
121-
path=path, params=cast(Dict[Any, Any], params), verb="post"
122-
).perform_with_content()
123-
return resp
118+
.. deprecated::
119+
Use Segments.create() instead. Audiences is now an alias for Segments.
120+
"""
121+
warnings.warn(
122+
"Audiences is deprecated. Use Segments instead.",
123+
DeprecationWarning,
124+
stacklevel=2,
125+
)
126+
return Segments.create(params)
124127

125128
@classmethod
126129
def list(cls, params: Optional[ListParams] = None) -> ListResponse:
@@ -137,14 +140,16 @@ def list(cls, params: Optional[ListParams] = None) -> ListResponse:
137140
138141
Returns:
139142
ListResponse: A list of audience objects
143+
144+
.. deprecated::
145+
Use Segments.list() instead. Audiences is now an alias for Segments.
140146
"""
141-
base_path = "/audiences"
142-
query_params = cast(Dict[Any, Any], params) if params else None
143-
path = PaginationHelper.build_paginated_path(base_path, query_params)
144-
resp = request.Request[Audiences.ListResponse](
145-
path=path, params={}, verb="get"
146-
).perform_with_content()
147-
return resp
147+
warnings.warn(
148+
"Audiences is deprecated. Use Segments instead.",
149+
DeprecationWarning,
150+
stacklevel=2,
151+
)
152+
return Segments.list(params)
148153

149154
@classmethod
150155
def get(cls, id: str) -> Audience:
@@ -157,12 +162,16 @@ def get(cls, id: str) -> Audience:
157162
158163
Returns:
159164
Audience: The audience object
165+
166+
.. deprecated::
167+
Use Segments.get() instead. Audiences is now an alias for Segments.
160168
"""
161-
path = f"/audiences/{id}"
162-
resp = request.Request[Audience](
163-
path=path, params={}, verb="get"
164-
).perform_with_content()
165-
return resp
169+
warnings.warn(
170+
"Audiences is deprecated. Use Segments instead.",
171+
DeprecationWarning,
172+
stacklevel=2,
173+
)
174+
return Segments.get(id)
166175

167176
@classmethod
168177
def remove(cls, id: str) -> RemoveAudienceResponse:
@@ -175,9 +184,13 @@ def remove(cls, id: str) -> RemoveAudienceResponse:
175184
176185
Returns:
177186
RemoveAudienceResponse: The removed audience response
187+
188+
.. deprecated::
189+
Use Segments.remove() instead. Audiences is now an alias for Segments.
178190
"""
179-
path = f"/audiences/{id}"
180-
resp = request.Request[Audiences.RemoveAudienceResponse](
181-
path=path, params={}, verb="delete"
182-
).perform_with_content()
183-
return resp
191+
warnings.warn(
192+
"Audiences is deprecated. Use Segments instead.",
193+
DeprecationWarning,
194+
stacklevel=2,
195+
)
196+
return Segments.remove(id)

resend/broadcasts/_broadcast.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,16 @@ class Broadcast(_FromParam):
2020
"""
2121
The unique identifier of the broadcast.
2222
"""
23-
audience_id: str
23+
segment_id: Union[str, None]
24+
"""
25+
The unique identifier of the segment.
26+
"""
27+
audience_id: Union[str, None]
2428
"""
2529
The unique identifier of the audience.
30+
31+
.. deprecated::
32+
Use segment_id instead.
2633
"""
2734
name: str
2835
"""

resend/broadcasts/_broadcasts.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,25 @@ class CreateParams(_CreateParamsFrom):
3232
3333
Attributes:
3434
from (str): The sender email address
35-
audience_id (str): The ID of the audience you want to send to.
35+
segment_id (NotRequired[str]): The ID of the segment you want to send to.
36+
audience_id (NotRequired[str]): The ID of the audience you want to send to. (deprecated: use segment_id)
3637
subject (str): Email subject.
3738
reply_to (NotRequired[Union[List[str], str]]): Reply-to email address(es).
3839
html (NotRequired[str]): The HTML version of the message.
3940
text (NotRequired[str]): The text version of the message.
4041
name (NotRequired[str]): The friendly name of the broadcast. Only used for internal reference.
4142
"""
4243

43-
audience_id: str
44+
segment_id: NotRequired[str]
45+
"""
46+
The ID of the segment you want to send to.
47+
"""
48+
audience_id: NotRequired[str]
4449
"""
4550
The ID of the audience you want to send to.
51+
52+
.. deprecated::
53+
Use segment_id instead.
4654
"""
4755
subject: str
4856
"""
@@ -70,8 +78,9 @@ class UpdateParams(_UpdateParamsFrom):
7078
7179
Attributes:
7280
broadcast_id (str): The ID of the broadcast you want to update.
73-
audience_id (str): The ID of the audience you want to send to.
74-
from (str): The sender email address
81+
segment_id (NotRequired[str]): The ID of the segment you want to send to.
82+
audience_id (NotRequired[str]): The ID of the audience you want to send to. (deprecated: use segment_id)
83+
from (NotRequired[str]): The sender email address
7584
subject (NotRequired[str]): Email subject.
7685
reply_to (NotRequired[Union[List[str], str]]): Reply-to email address(es).
7786
html (NotRequired[str]): The HTML version of the message.
@@ -83,9 +92,16 @@ class UpdateParams(_UpdateParamsFrom):
8392
"""
8493
The ID of the broadcast you want to update.
8594
"""
95+
segment_id: NotRequired[str]
96+
"""
97+
The ID of the segment you want to send to.
98+
"""
8699
audience_id: NotRequired[str]
87100
"""
88101
The ID of the audience you want to send to.
102+
103+
.. deprecated::
104+
Use segment_id instead.
89105
"""
90106
subject: NotRequired[str]
91107
"""

0 commit comments

Comments
 (0)