unitpost 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unitpost
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Official Unitpost SDK for Python — send email and manage contacts, segments, campaigns, templates, brand kits, domains, suppressions, webhooks, and usage.
5
5
  Project-URL: Homepage, https://www.unitpost.com
6
6
  Project-URL: Documentation, https://www.unitpost.com/docs
@@ -71,6 +71,7 @@ Pass the key to `Unitpost(...)`, or set `UNITPOST_API_KEY`. Create a key at
71
71
  | `email.campaigns` | Draft, schedule, send, pause/resume |
72
72
  | `email.templates` | Template CRUD |
73
73
  | `email.domains` | Domains + verify |
74
+ | `sms` | Send, get, list, `brands()`, `numbers()`, contact SMS consent. Available on paid plans: on the free plan `send` returns a `403` with code `limit_reached`, while the reads keep working. |
74
75
  | `contacts` | CRUD, import jobs |
75
76
  | `contact_fields` | Custom field definitions |
76
77
  | `segments` | Segments + membership |
@@ -98,7 +99,7 @@ complaints are added automatically; you can also add your own:
98
99
  unitpost.suppressions.create({"email": "bounced@example.com", "reason": "manual"})
99
100
  unitpost.suppressions.create({"emails": ["a@example.com", "b@example.com"]})
100
101
 
101
- # Look one up by id or email; list (optionally filter scope); un-suppress.
102
+ # Look one up by id or email; list your own; un-suppress.
102
103
  unitpost.suppressions.get("bounced@example.com")
103
104
  for s in unitpost.suppressions.list_all():
104
105
  print(s["email"], s["reason"])
@@ -127,6 +128,42 @@ Or from the client (Resend-style alias):
127
128
  event = client.webhooks.verify(payload=raw_body, secret=secret, headers=request.headers)
128
129
  ```
129
130
 
131
+ ## Receive a text and answer it
132
+
133
+ US toll-free, 10DLC and Canadian numbers take replies (`two_way: True` on
134
+ `sms.numbers()`). Subscribe a webhook to `sms.received`, skip STOP and texts
135
+ Unitpost already answered, read the conversation with `participant`, and answer
136
+ without `from` so the reply leaves from the number they texted.
137
+
138
+ ```python
139
+ import os
140
+ from unitpost import Unitpost, verify_webhook
141
+
142
+ unitpost = Unitpost() # reads UNITPOST_API_KEY
143
+
144
+ # Your webhook endpoint, subscribed to sms.received. Pass the RAW body.
145
+ def on_webhook(raw_body: bytes, headers: dict) -> None:
146
+ event = verify_webhook(
147
+ payload=raw_body,
148
+ secret=os.environ["UNITPOST_WEBHOOK_SECRET"], # whsec_…
149
+ headers=headers,
150
+ )
151
+ if event["type"] != "sms.received":
152
+ return
153
+ reply = event["data"]
154
+ if reply["keyword"] == "OPT_OUT" or reply["auto_replied"]:
155
+ return # STOP, or already answered
156
+
157
+ # The whole conversation with this person, both directions, newest first.
158
+ # Other filters: direction, status, from_ (sent as `from`), to, contact_id,
159
+ # campaign_id, created_after, created_before.
160
+ history = unitpost.sms.list(participant=reply["from"], limit=20)
161
+ answer = your_agent(history.data["data"], reply["body"] or "")
162
+
163
+ # No "from": the answer goes out from the number they texted.
164
+ unitpost.sms.send({"to": reply["from"], "body": answer})
165
+ ```
166
+
130
167
  ## Idempotency
131
168
 
132
169
  `email.send` and `email.batch` are safe to retry by default — if you don't pass
@@ -162,7 +199,8 @@ unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
162
199
  | | |
163
200
  | --- | --- |
164
201
  | Docs + API reference | https://www.unitpost.com/docs |
165
- | Product / SMTP / MCP guides | https://www.unitpost.com/guides |
202
+ | Product guides | https://www.unitpost.com/guides |
203
+ | SMTP | https://www.unitpost.com/docs/smtp |
166
204
  | OpenAPI JSON | https://www.unitpost.com/api/v1/openapi.json |
167
205
  | LLM index | https://www.unitpost.com/llms.txt |
168
206
  | Dashboard / API keys | https://www.unitpost.com |
@@ -170,7 +208,7 @@ unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
170
208
  | Template gallery | https://www.unitpost.com/templates/gallery |
171
209
  | Playground | https://www.unitpost.com/playground |
172
210
  | Migration Assistant | https://www.unitpost.com/migration |
173
- | MCP + Agent Skills | https://www.unitpost.com/guides#ai-mcp · https://www.unitpost.com/guides#ai-skills |
211
+ | MCP + Agent Skills | https://www.unitpost.com/docs/ai/mcp · https://www.unitpost.com/docs/ai/skills |
174
212
  | Support | support@unitpost.com |
175
213
 
176
214
  ## License
@@ -48,6 +48,7 @@ Pass the key to `Unitpost(...)`, or set `UNITPOST_API_KEY`. Create a key at
48
48
  | `email.campaigns` | Draft, schedule, send, pause/resume |
49
49
  | `email.templates` | Template CRUD |
50
50
  | `email.domains` | Domains + verify |
51
+ | `sms` | Send, get, list, `brands()`, `numbers()`, contact SMS consent. Available on paid plans: on the free plan `send` returns a `403` with code `limit_reached`, while the reads keep working. |
51
52
  | `contacts` | CRUD, import jobs |
52
53
  | `contact_fields` | Custom field definitions |
53
54
  | `segments` | Segments + membership |
@@ -75,7 +76,7 @@ complaints are added automatically; you can also add your own:
75
76
  unitpost.suppressions.create({"email": "bounced@example.com", "reason": "manual"})
76
77
  unitpost.suppressions.create({"emails": ["a@example.com", "b@example.com"]})
77
78
 
78
- # Look one up by id or email; list (optionally filter scope); un-suppress.
79
+ # Look one up by id or email; list your own; un-suppress.
79
80
  unitpost.suppressions.get("bounced@example.com")
80
81
  for s in unitpost.suppressions.list_all():
81
82
  print(s["email"], s["reason"])
@@ -104,6 +105,42 @@ Or from the client (Resend-style alias):
104
105
  event = client.webhooks.verify(payload=raw_body, secret=secret, headers=request.headers)
105
106
  ```
106
107
 
108
+ ## Receive a text and answer it
109
+
110
+ US toll-free, 10DLC and Canadian numbers take replies (`two_way: True` on
111
+ `sms.numbers()`). Subscribe a webhook to `sms.received`, skip STOP and texts
112
+ Unitpost already answered, read the conversation with `participant`, and answer
113
+ without `from` so the reply leaves from the number they texted.
114
+
115
+ ```python
116
+ import os
117
+ from unitpost import Unitpost, verify_webhook
118
+
119
+ unitpost = Unitpost() # reads UNITPOST_API_KEY
120
+
121
+ # Your webhook endpoint, subscribed to sms.received. Pass the RAW body.
122
+ def on_webhook(raw_body: bytes, headers: dict) -> None:
123
+ event = verify_webhook(
124
+ payload=raw_body,
125
+ secret=os.environ["UNITPOST_WEBHOOK_SECRET"], # whsec_…
126
+ headers=headers,
127
+ )
128
+ if event["type"] != "sms.received":
129
+ return
130
+ reply = event["data"]
131
+ if reply["keyword"] == "OPT_OUT" or reply["auto_replied"]:
132
+ return # STOP, or already answered
133
+
134
+ # The whole conversation with this person, both directions, newest first.
135
+ # Other filters: direction, status, from_ (sent as `from`), to, contact_id,
136
+ # campaign_id, created_after, created_before.
137
+ history = unitpost.sms.list(participant=reply["from"], limit=20)
138
+ answer = your_agent(history.data["data"], reply["body"] or "")
139
+
140
+ # No "from": the answer goes out from the number they texted.
141
+ unitpost.sms.send({"to": reply["from"], "body": answer})
142
+ ```
143
+
107
144
  ## Idempotency
108
145
 
109
146
  `email.send` and `email.batch` are safe to retry by default — if you don't pass
@@ -139,7 +176,8 @@ unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
139
176
  | | |
140
177
  | --- | --- |
141
178
  | Docs + API reference | https://www.unitpost.com/docs |
142
- | Product / SMTP / MCP guides | https://www.unitpost.com/guides |
179
+ | Product guides | https://www.unitpost.com/guides |
180
+ | SMTP | https://www.unitpost.com/docs/smtp |
143
181
  | OpenAPI JSON | https://www.unitpost.com/api/v1/openapi.json |
144
182
  | LLM index | https://www.unitpost.com/llms.txt |
145
183
  | Dashboard / API keys | https://www.unitpost.com |
@@ -147,7 +185,7 @@ unitpost = Unitpost(max_retries=2, retry_base_delay=0.5, max_retry_delay=20.0)
147
185
  | Template gallery | https://www.unitpost.com/templates/gallery |
148
186
  | Playground | https://www.unitpost.com/playground |
149
187
  | Migration Assistant | https://www.unitpost.com/migration |
150
- | MCP + Agent Skills | https://www.unitpost.com/guides#ai-mcp · https://www.unitpost.com/guides#ai-skills |
188
+ | MCP + Agent Skills | https://www.unitpost.com/docs/ai/mcp · https://www.unitpost.com/docs/ai/skills |
151
189
  | Support | support@unitpost.com |
152
190
 
153
191
  ## License
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "unitpost"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Official Unitpost SDK for Python — send email and manage contacts, segments, campaigns, templates, brand kits, domains, suppressions, webhooks, and usage."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -29,7 +29,9 @@ from .resources import (
29
29
  Contacts,
30
30
  Email,
31
31
  Segments,
32
+ Sms,
32
33
  Suppressions,
34
+ Jobs,
33
35
  Usage,
34
36
  Webhooks,
35
37
  )
@@ -62,6 +64,9 @@ class Unitpost:
62
64
  )
63
65
  self._http = http
64
66
  self.email = Email(http)
67
+ # SMS channel. Available on paid plans: on the free plan `send` returns
68
+ # a 403 with code `limit_reached`; reads keep working.
69
+ self.sms = Sms(http)
65
70
  self.contacts = Contacts(http)
66
71
  self.contact_fields = ContactFields(http)
67
72
  self.segments = Segments(http)
@@ -71,6 +76,7 @@ class Unitpost:
71
76
  self.suppressions = Suppressions(http)
72
77
  # NOTE (pre-launch): `events` is intentionally absent while the
73
78
  # custom-events surface is behind the launch gate.
79
+ self.jobs = Jobs(http)
74
80
  self.usage = Usage(http)
75
81
 
76
82
  def close(self) -> None: