unitpost 0.2.1__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
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: unitpost
3
- Version: 0.2.1
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
@@ -66,15 +66,16 @@ Pass the key to `Unitpost(...)`, or set `UNITPOST_API_KEY`. Create a key at
66
66
 
67
67
  | Accessor | Covers |
68
68
  | --- | --- |
69
- | `emails` | Send, batch, list, get, update, stats, inbound (`received_*`) |
69
+ | `email` | Send, batch, list, get, update, stats, inbound (`received_*`) |
70
+ | `email.topics` | Topics + contact topic prefs |
71
+ | `email.campaigns` | Draft, schedule, send, pause/resume |
72
+ | `email.templates` | Template CRUD |
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. |
70
75
  | `contacts` | CRUD, import jobs |
71
76
  | `contact_fields` | Custom field definitions |
72
77
  | `segments` | Segments + membership |
73
- | `topics` | Topics + contact topic prefs |
74
- | `campaigns` | Draft, schedule, send, pause/resume |
75
- | `templates` | Template CRUD |
76
78
  | `brand_kits` | Brand kit |
77
- | `domains` | Domains + verify |
78
79
  | `webhooks` | Endpoints + `verify()` |
79
80
  | `api_keys` | API key management |
80
81
  | `suppressions` | Suppression list |
@@ -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,9 +128,45 @@ 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
- `emails.send` and `emails.batch` are safe to retry by default — if you don't pass
169
+ `email.send` and `email.batch` are safe to retry by default — if you don't pass
133
170
  `idempotency_key`, the SDK generates one so an automatic retry can't double-send.
134
171
  Pass your own to dedupe across separate calls:
135
172
 
@@ -149,7 +186,7 @@ Transient failures are retried automatically: `429`, `5xx`, and transport errors
149
186
  `Retry-After` is honored; otherwise exponential backoff with full jitter is used.
150
187
  Client errors (`401`/`403`/`404`/`409`/`422`) are never retried. Only requests
151
188
  safe to replay are retried: `GET`s and writes carrying an idempotency key (which
152
- `emails.send` and `emails.batch` always do); any other keyless write is never
189
+ `email.send` and `email.batch` always do); any other keyless write is never
153
190
  auto-retried.
154
191
 
155
192
  ```python
@@ -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
@@ -43,15 +43,16 @@ Pass the key to `Unitpost(...)`, or set `UNITPOST_API_KEY`. Create a key at
43
43
 
44
44
  | Accessor | Covers |
45
45
  | --- | --- |
46
- | `emails` | Send, batch, list, get, update, stats, inbound (`received_*`) |
46
+ | `email` | Send, batch, list, get, update, stats, inbound (`received_*`) |
47
+ | `email.topics` | Topics + contact topic prefs |
48
+ | `email.campaigns` | Draft, schedule, send, pause/resume |
49
+ | `email.templates` | Template CRUD |
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. |
47
52
  | `contacts` | CRUD, import jobs |
48
53
  | `contact_fields` | Custom field definitions |
49
54
  | `segments` | Segments + membership |
50
- | `topics` | Topics + contact topic prefs |
51
- | `campaigns` | Draft, schedule, send, pause/resume |
52
- | `templates` | Template CRUD |
53
55
  | `brand_kits` | Brand kit |
54
- | `domains` | Domains + verify |
55
56
  | `webhooks` | Endpoints + `verify()` |
56
57
  | `api_keys` | API key management |
57
58
  | `suppressions` | Suppression list |
@@ -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,9 +105,45 @@ 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
- `emails.send` and `emails.batch` are safe to retry by default — if you don't pass
146
+ `email.send` and `email.batch` are safe to retry by default — if you don't pass
110
147
  `idempotency_key`, the SDK generates one so an automatic retry can't double-send.
111
148
  Pass your own to dedupe across separate calls:
112
149
 
@@ -126,7 +163,7 @@ Transient failures are retried automatically: `429`, `5xx`, and transport errors
126
163
  `Retry-After` is honored; otherwise exponential backoff with full jitter is used.
127
164
  Client errors (`401`/`403`/`404`/`409`/`422`) are never retried. Only requests
128
165
  safe to replay are retried: `GET`s and writes carrying an idempotency key (which
129
- `emails.send` and `emails.batch` always do); any other keyless write is never
166
+ `email.send` and `email.batch` always do); any other keyless write is never
130
167
  auto-retried.
131
168
 
132
169
  ```python
@@ -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.2.1"
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: