inboxjot 0.1.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.
- inboxjot-0.1.0/.gitignore +9 -0
- inboxjot-0.1.0/LICENSE +21 -0
- inboxjot-0.1.0/PKG-INFO +340 -0
- inboxjot-0.1.0/README.md +313 -0
- inboxjot-0.1.0/pyproject.toml +55 -0
- inboxjot-0.1.0/src/inboxjot/__init__.py +15 -0
- inboxjot-0.1.0/src/inboxjot/_campaigns.py +491 -0
- inboxjot-0.1.0/src/inboxjot/_client.py +303 -0
- inboxjot-0.1.0/src/inboxjot/_contacts.py +233 -0
- inboxjot-0.1.0/src/inboxjot/_domains.py +191 -0
- inboxjot-0.1.0/src/inboxjot/_emails.py +307 -0
- inboxjot-0.1.0/src/inboxjot/_errors.py +53 -0
- inboxjot-0.1.0/src/inboxjot/_events.py +50 -0
- inboxjot-0.1.0/src/inboxjot/_images.py +197 -0
- inboxjot-0.1.0/src/inboxjot/_segments.py +309 -0
- inboxjot-0.1.0/src/inboxjot/_templates.py +416 -0
- inboxjot-0.1.0/src/inboxjot/_topics.py +196 -0
- inboxjot-0.1.0/src/inboxjot/_webhooks.py +277 -0
- inboxjot-0.1.0/src/inboxjot/py.typed +0 -0
- inboxjot-0.1.0/src/inboxjot/types.py +1122 -0
- inboxjot-0.1.0/tests/conftest.py +17 -0
- inboxjot-0.1.0/tests/helpers.py +91 -0
- inboxjot-0.1.0/tests/test_campaigns.py +167 -0
- inboxjot-0.1.0/tests/test_contacts.py +125 -0
- inboxjot-0.1.0/tests/test_domains.py +102 -0
- inboxjot-0.1.0/tests/test_emails.py +137 -0
- inboxjot-0.1.0/tests/test_events.py +33 -0
- inboxjot-0.1.0/tests/test_images.py +143 -0
- inboxjot-0.1.0/tests/test_inboxjot.py +309 -0
- inboxjot-0.1.0/tests/test_segments.py +95 -0
- inboxjot-0.1.0/tests/test_templates.py +85 -0
- inboxjot-0.1.0/tests/test_topics.py +43 -0
- inboxjot-0.1.0/tests/test_webhooks.py +220 -0
inboxjot-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Arik Chakma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
inboxjot-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: inboxjot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The official Python SDK for the Inboxjot email API
|
|
5
|
+
Project-URL: Homepage, https://inboxjot.com
|
|
6
|
+
Project-URL: Documentation, https://inboxjot.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/inboxjot/inboxjot-python
|
|
8
|
+
Project-URL: Issues, https://github.com/inboxjot/inboxjot-python/issues
|
|
9
|
+
Author-email: Arik Chakma <arikchangma@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: email,email-api,inboxjot,sdk,transactional-email
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Communications :: Email
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: httpx<1,>=0.27
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
<p align="center">The official Python SDK for the Inboxjot email API. <br />Email for builders and their agents.</p>
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<a href="https://pypi.org/project/inboxjot/"><img src="https://inboxjot.com/badges/pypi.svg?v=2" alt="PyPI" height="24" /></a>
|
|
34
|
+
<a href="./LICENSE"><img src="https://inboxjot.com/badges/license.svg?v=2" alt="License" height="24" /></a>
|
|
35
|
+
<a href="https://inboxjot.com/docs"><img src="https://inboxjot.com/badges/docs.svg?v=2" alt="Docs" height="24" /></a>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
Send email, manage contacts, campaigns, templates and domains, and verify webhooks from your Python app. It needs Python 3.10+ and has one runtime dependency, [`httpx`](https://www.python-httpx.org). It has a sync client, `Inboxjot`, and an async client, `AsyncInboxjot`, with the same methods.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pip install inboxjot
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from inboxjot import Inboxjot
|
|
50
|
+
|
|
51
|
+
inboxjot = Inboxjot(api_key="ibx_...")
|
|
52
|
+
|
|
53
|
+
email = inboxjot.emails.send(
|
|
54
|
+
{
|
|
55
|
+
"from": "Acme <hi@acme.dev>",
|
|
56
|
+
"to": "jane@example.com",
|
|
57
|
+
"subject": "Welcome",
|
|
58
|
+
"html": "<p>Hi Jane</p>",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
print(email["id"])
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If you do not give an API key, the client reads `INBOXJOT_API_KEY` from the environment.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
inboxjot = Inboxjot(
|
|
68
|
+
base_url="https://api.inboxjot.com",
|
|
69
|
+
timeout=60.0,
|
|
70
|
+
max_retries=2,
|
|
71
|
+
http_client=httpx.Client(),
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The client keeps a connection pool. Use it as a context manager, or call `inboxjot.close()` when you are done.
|
|
76
|
+
|
|
77
|
+
The async client takes the same options, and an `httpx.AsyncClient` as `http_client`:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from inboxjot import AsyncInboxjot
|
|
81
|
+
|
|
82
|
+
async with AsyncInboxjot() as inboxjot:
|
|
83
|
+
email = await inboxjot.emails.get("em_...")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Emails
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
inboxjot.emails.send(
|
|
90
|
+
{
|
|
91
|
+
"from": sender,
|
|
92
|
+
"to": ["a@example.com"],
|
|
93
|
+
"subject": subject,
|
|
94
|
+
"text": text,
|
|
95
|
+
"scheduled_at": "2026-10-20T09:00:00Z",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
inboxjot.emails.batch(
|
|
99
|
+
[
|
|
100
|
+
{"from": sender, "to": "a@example.com", "subject": subject, "html": html},
|
|
101
|
+
{"from": sender, "to": "b@example.com", "subject": subject, "html": html},
|
|
102
|
+
]
|
|
103
|
+
)
|
|
104
|
+
inboxjot.emails.list({"status": "delivered", "per_page": 20})
|
|
105
|
+
inboxjot.emails.get("em_...")
|
|
106
|
+
inboxjot.emails.update("em_...", {"scheduled_at": "2026-10-21T09:00:00Z"})
|
|
107
|
+
inboxjot.emails.cancel("em_...")
|
|
108
|
+
inboxjot.emails.list_attachments("em_...")
|
|
109
|
+
inboxjot.emails.get_attachment("em_...", "att_...")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Contacts and events
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
contact = inboxjot.contacts.create({"email": "jane@example.com", "first_name": "Jane"})
|
|
116
|
+
inboxjot.contacts.list({"status": "subscribed", "per_page": 50})
|
|
117
|
+
inboxjot.contacts.get(contact["id"])
|
|
118
|
+
inboxjot.contacts.get("jane@example.com")
|
|
119
|
+
inboxjot.contacts.update(contact["id"], {"topics": [{"id": "tp_...", "status": "unsubscribed"}]})
|
|
120
|
+
inboxjot.contacts.delete(contact["id"])
|
|
121
|
+
inboxjot.contacts.erase({"email": "jane@example.com"})
|
|
122
|
+
|
|
123
|
+
inboxjot.events.send({"name": "signed_up", "email": "jane@example.com", "data": {"plan": "pro"}})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`contacts.remove` deletes the contact. `contacts.erase` queues a privacy erasure of the address, its emails, events and logs.
|
|
127
|
+
|
|
128
|
+
### Campaigns
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
campaign = inboxjot.campaigns.create({"name": "October", "subject": "News"})
|
|
132
|
+
inboxjot.campaigns.list({"status": "draft,scheduled", "per_page": 20})
|
|
133
|
+
inboxjot.campaigns.get(campaign["id"])
|
|
134
|
+
inboxjot.campaigns.update(campaign["id"], {"subject": "October news"})
|
|
135
|
+
inboxjot.campaigns.send_test(campaign["id"], {"to": ["jane@example.com"]})
|
|
136
|
+
inboxjot.campaigns.send(campaign["id"])
|
|
137
|
+
inboxjot.campaigns.send(campaign["id"], {"scheduled_at": "2026-10-20T09:00:00Z"})
|
|
138
|
+
inboxjot.campaigns.cancel(campaign["id"])
|
|
139
|
+
inboxjot.campaigns.delete(campaign["id"])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Templates
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
template = inboxjot.templates.create({"name": "Welcome", "starter_id": "welcome"})
|
|
146
|
+
inboxjot.templates.list({"sort": "name"})
|
|
147
|
+
inboxjot.templates.get(template["id"])
|
|
148
|
+
inboxjot.templates.update(template["id"], {"subject": "Welcome"})
|
|
149
|
+
inboxjot.templates.publish(template["id"])
|
|
150
|
+
inboxjot.templates.delete(template["id"])
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Versions
|
|
154
|
+
|
|
155
|
+
Campaigns and templates have the same version methods. `update_version` always takes `name`, and `None` clears it.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
version = inboxjot.templates.create_version(template["id"], {"name": "Launch"})
|
|
159
|
+
inboxjot.templates.list_versions(template["id"], {"filter": "saved"})
|
|
160
|
+
inboxjot.templates.get_version(template["id"], version["id"])
|
|
161
|
+
inboxjot.templates.update_version(template["id"], version["id"], {"name": None})
|
|
162
|
+
inboxjot.templates.restore_version(template["id"], version["id"])
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Domains
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
domain = inboxjot.domains.create({"name": "acme.dev"})
|
|
169
|
+
inboxjot.domains.list()
|
|
170
|
+
inboxjot.domains.get(domain["id"])
|
|
171
|
+
inboxjot.domains.verify(domain["id"])
|
|
172
|
+
inboxjot.domains.delete(domain["id"])
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Images
|
|
176
|
+
|
|
177
|
+
An image that is already online goes straight into email content as `<img src="https://…">`. To host a file on Inboxjot, upload it. The SDK accepts PNG, JPEG and GIF files.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
with open("logo.png", "rb") as file:
|
|
181
|
+
image = inboxjot.images.upload({"file": file.read(), "file_name": "logo.png"})
|
|
182
|
+
|
|
183
|
+
inboxjot.images.list({"type": "png"})
|
|
184
|
+
inboxjot.images.get(image["id"])
|
|
185
|
+
inboxjot.images.delete(image["id"])
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Segments and topics
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
segment = inboxjot.segments.create({"name": "Beta", "type": "static"})
|
|
192
|
+
inboxjot.segments.list({"type": "static"})
|
|
193
|
+
inboxjot.segments.get(segment["id"])
|
|
194
|
+
inboxjot.segments.update(segment["id"], {"name": "Early Access"})
|
|
195
|
+
inboxjot.segments.add_members(segment["id"], {"contact_ids": ["ct_..."]})
|
|
196
|
+
inboxjot.segments.list_members(segment["id"], {"query": "jane"})
|
|
197
|
+
inboxjot.segments.remove_members(segment["id"], {"contact_ids": ["ct_..."]})
|
|
198
|
+
inboxjot.segments.delete(segment["id"])
|
|
199
|
+
|
|
200
|
+
topic = inboxjot.topics.create({"name": "Product News"})
|
|
201
|
+
inboxjot.topics.list()
|
|
202
|
+
inboxjot.topics.get(topic["id"])
|
|
203
|
+
inboxjot.topics.update(topic["id"], {"visibility": "private"})
|
|
204
|
+
inboxjot.topics.delete(topic["id"])
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Pagination
|
|
208
|
+
|
|
209
|
+
Lists page with a cursor. Pass `next_cursor` back as `after`. It is `None` on the last page, and the client leaves a `None` cursor out of the request.
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
after = None
|
|
213
|
+
while True:
|
|
214
|
+
page = inboxjot.contacts.list({"per_page": 100, "after": after})
|
|
215
|
+
after = page["next_cursor"]
|
|
216
|
+
if after is None:
|
|
217
|
+
break
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Idempotency and retries
|
|
221
|
+
|
|
222
|
+
Every method takes request options as keyword arguments:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
inboxjot.emails.send(email, idempotency_key=f"welcome-{user.id}")
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
| Option | Use |
|
|
229
|
+
| ----------------- | ------------------------------------------- |
|
|
230
|
+
| `idempotency_key` | Sends the `Idempotency-Key` header. |
|
|
231
|
+
| `timeout` | Overrides the client timeout, in seconds. |
|
|
232
|
+
| `max_retries` | Overrides the client retry count. |
|
|
233
|
+
|
|
234
|
+
The client retries with exponential backoff, and it honors `Retry-After`. A `Retry-After` over 60 seconds raises the error with no retry:
|
|
235
|
+
|
|
236
|
+
- `429` and `409 concurrent_idempotent_requests` always retry. The API did not run these requests.
|
|
237
|
+
- `5xx`, timeouts and network errors retry only when the retry is safe: a `GET` or `DELETE`, or a request with an `idempotency_key`.
|
|
238
|
+
|
|
239
|
+
An email send without a key is never retried after a network error, because that can send the email twice. Give a key to make it safe.
|
|
240
|
+
|
|
241
|
+
### Errors
|
|
242
|
+
|
|
243
|
+
A failed request raises. An API error is an `InboxjotError` with the fields of the API error body. A network error or timeout is an `InboxjotConnectionError`.
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
from inboxjot import InboxjotError
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
inboxjot.emails.get("em_...")
|
|
250
|
+
except InboxjotError as error:
|
|
251
|
+
if error.name == "not_found":
|
|
252
|
+
... # error.status, error.message, error.errors, error.request_id, error.headers
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The SDK raises instead of returning `(data, error)`. A result tuple makes you check every call, and an unchecked error turns into `None` data later in the code. A raised error stops the flow at the failed call, and it works with `asyncio.gather` and with the retry loop. `error.name` is typed as a `Literal` of the API error names.
|
|
256
|
+
|
|
257
|
+
### Webhooks
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
webhook = inboxjot.webhooks.create(
|
|
261
|
+
{"name": "Production", "url": "https://acme.dev/hook", "events": ["email.bounced"]}
|
|
262
|
+
)
|
|
263
|
+
print(webhook["secret"])
|
|
264
|
+
inboxjot.webhooks.list()
|
|
265
|
+
inboxjot.webhooks.get(webhook["id"])
|
|
266
|
+
inboxjot.webhooks.update(webhook["id"], {"status": "disabled"})
|
|
267
|
+
inboxjot.webhooks.delete(webhook["id"])
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Inboxjot signs webhooks with the [Standard Webhooks](https://www.standardwebhooks.com) spec. Verify the raw request body before you parse it:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
import os
|
|
274
|
+
|
|
275
|
+
from fastapi import FastAPI, Request, Response
|
|
276
|
+
from inboxjot import verify_webhook
|
|
277
|
+
|
|
278
|
+
app = FastAPI()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@app.post("/webhooks/inboxjot")
|
|
282
|
+
async def handle(request: Request):
|
|
283
|
+
event = verify_webhook(
|
|
284
|
+
payload=await request.body(),
|
|
285
|
+
headers=request.headers,
|
|
286
|
+
secret=os.environ["INBOXJOT_WEBHOOK_SECRET"],
|
|
287
|
+
)
|
|
288
|
+
if event["type"] == "email.bounced":
|
|
289
|
+
print(event["data"]["email_id"], event["data"]["bounce"]["type"])
|
|
290
|
+
elif event["type"] == "contact.created":
|
|
291
|
+
print(event["data"]["contact_id"])
|
|
292
|
+
return Response(status_code=204)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`inboxjot.webhooks.verify` is the same function. The payload can be `str` or `bytes`, and the headers can be any mapping, such as a `dict` or a framework's headers object. A bad signature, a missing header or a timestamp more than 5 minutes off raises a `WebhookVerificationError`.
|
|
296
|
+
|
|
297
|
+
## Types
|
|
298
|
+
|
|
299
|
+
`inboxjot.types` has a `TypedDict` or `Literal` for each schema in the Inboxjot OpenAPI spec, with the names the TypeScript SDK uses (`SendEmailOptions`, `Email`, `Contact`, `WebhookEvent` and the others). Request and response bodies are plain dicts with the API's own keys, so `scheduled_at` stays `scheduled_at`. `WebhookEvent` is a union that a type checker narrows on `event["type"]`.
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
from inboxjot.types import Email, SendEmailOptions
|
|
303
|
+
|
|
304
|
+
email: SendEmailOptions = {"to": "jane@example.com", "subject": "Hi", "text": "Hi"}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The client does not validate responses, so a new enum value or field reaches your code as is.
|
|
308
|
+
|
|
309
|
+
If a type is wrong, fix the spec in the API and update `src/inboxjot/types.py` to match.
|
|
310
|
+
|
|
311
|
+
## Layout
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
src/inboxjot/_client.py Inboxjot, AsyncInboxjot and request(): auth, idempotency, timeout, retries
|
|
315
|
+
src/inboxjot/_emails.py emails.list, send, batch, get, update, cancel, list_attachments, get_attachment
|
|
316
|
+
src/inboxjot/_contacts.py contacts.list, create, get, update, remove, erase
|
|
317
|
+
src/inboxjot/_events.py events.send
|
|
318
|
+
src/inboxjot/_campaigns.py campaigns.list, create, get, update, remove, send, cancel, send_test, versions
|
|
319
|
+
src/inboxjot/_templates.py templates.list, create, get, update, remove, publish, versions
|
|
320
|
+
src/inboxjot/_domains.py domains.list, create, get, remove, verify
|
|
321
|
+
src/inboxjot/_images.py images.list, upload, get, remove
|
|
322
|
+
src/inboxjot/_segments.py segments.list, create, get, update, remove, list_members, add_members, remove_members
|
|
323
|
+
src/inboxjot/_topics.py topics.list, create, get, update, remove
|
|
324
|
+
src/inboxjot/_webhooks.py webhooks.list, create, get, update, remove, verify, verify_webhook
|
|
325
|
+
src/inboxjot/_errors.py InboxjotError, InboxjotConnectionError, WebhookVerificationError
|
|
326
|
+
src/inboxjot/types.py the request and response types
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Development
|
|
330
|
+
|
|
331
|
+
```sh
|
|
332
|
+
uv sync
|
|
333
|
+
uv run pytest
|
|
334
|
+
uv run ruff check
|
|
335
|
+
uv run ruff format
|
|
336
|
+
uv run pyright
|
|
337
|
+
uv build
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
To release, run `uv version --bump patch` (or `minor`), commit `chore: release vX.Y.Z`, then tag `vX.Y.Z` and push the tag. The release workflow checks, builds and publishes to PyPI.
|
inboxjot-0.1.0/README.md
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
<p align="center">The official Python SDK for the Inboxjot email API. <br />Email for builders and their agents.</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://pypi.org/project/inboxjot/"><img src="https://inboxjot.com/badges/pypi.svg?v=2" alt="PyPI" height="24" /></a>
|
|
7
|
+
<a href="./LICENSE"><img src="https://inboxjot.com/badges/license.svg?v=2" alt="License" height="24" /></a>
|
|
8
|
+
<a href="https://inboxjot.com/docs"><img src="https://inboxjot.com/badges/docs.svg?v=2" alt="Docs" height="24" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
Send email, manage contacts, campaigns, templates and domains, and verify webhooks from your Python app. It needs Python 3.10+ and has one runtime dependency, [`httpx`](https://www.python-httpx.org). It has a sync client, `Inboxjot`, and an async client, `AsyncInboxjot`, with the same methods.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install inboxjot
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from inboxjot import Inboxjot
|
|
23
|
+
|
|
24
|
+
inboxjot = Inboxjot(api_key="ibx_...")
|
|
25
|
+
|
|
26
|
+
email = inboxjot.emails.send(
|
|
27
|
+
{
|
|
28
|
+
"from": "Acme <hi@acme.dev>",
|
|
29
|
+
"to": "jane@example.com",
|
|
30
|
+
"subject": "Welcome",
|
|
31
|
+
"html": "<p>Hi Jane</p>",
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
print(email["id"])
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
If you do not give an API key, the client reads `INBOXJOT_API_KEY` from the environment.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
inboxjot = Inboxjot(
|
|
41
|
+
base_url="https://api.inboxjot.com",
|
|
42
|
+
timeout=60.0,
|
|
43
|
+
max_retries=2,
|
|
44
|
+
http_client=httpx.Client(),
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The client keeps a connection pool. Use it as a context manager, or call `inboxjot.close()` when you are done.
|
|
49
|
+
|
|
50
|
+
The async client takes the same options, and an `httpx.AsyncClient` as `http_client`:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from inboxjot import AsyncInboxjot
|
|
54
|
+
|
|
55
|
+
async with AsyncInboxjot() as inboxjot:
|
|
56
|
+
email = await inboxjot.emails.get("em_...")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Emails
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
inboxjot.emails.send(
|
|
63
|
+
{
|
|
64
|
+
"from": sender,
|
|
65
|
+
"to": ["a@example.com"],
|
|
66
|
+
"subject": subject,
|
|
67
|
+
"text": text,
|
|
68
|
+
"scheduled_at": "2026-10-20T09:00:00Z",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
inboxjot.emails.batch(
|
|
72
|
+
[
|
|
73
|
+
{"from": sender, "to": "a@example.com", "subject": subject, "html": html},
|
|
74
|
+
{"from": sender, "to": "b@example.com", "subject": subject, "html": html},
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
inboxjot.emails.list({"status": "delivered", "per_page": 20})
|
|
78
|
+
inboxjot.emails.get("em_...")
|
|
79
|
+
inboxjot.emails.update("em_...", {"scheduled_at": "2026-10-21T09:00:00Z"})
|
|
80
|
+
inboxjot.emails.cancel("em_...")
|
|
81
|
+
inboxjot.emails.list_attachments("em_...")
|
|
82
|
+
inboxjot.emails.get_attachment("em_...", "att_...")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Contacts and events
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
contact = inboxjot.contacts.create({"email": "jane@example.com", "first_name": "Jane"})
|
|
89
|
+
inboxjot.contacts.list({"status": "subscribed", "per_page": 50})
|
|
90
|
+
inboxjot.contacts.get(contact["id"])
|
|
91
|
+
inboxjot.contacts.get("jane@example.com")
|
|
92
|
+
inboxjot.contacts.update(contact["id"], {"topics": [{"id": "tp_...", "status": "unsubscribed"}]})
|
|
93
|
+
inboxjot.contacts.delete(contact["id"])
|
|
94
|
+
inboxjot.contacts.erase({"email": "jane@example.com"})
|
|
95
|
+
|
|
96
|
+
inboxjot.events.send({"name": "signed_up", "email": "jane@example.com", "data": {"plan": "pro"}})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`contacts.remove` deletes the contact. `contacts.erase` queues a privacy erasure of the address, its emails, events and logs.
|
|
100
|
+
|
|
101
|
+
### Campaigns
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
campaign = inboxjot.campaigns.create({"name": "October", "subject": "News"})
|
|
105
|
+
inboxjot.campaigns.list({"status": "draft,scheduled", "per_page": 20})
|
|
106
|
+
inboxjot.campaigns.get(campaign["id"])
|
|
107
|
+
inboxjot.campaigns.update(campaign["id"], {"subject": "October news"})
|
|
108
|
+
inboxjot.campaigns.send_test(campaign["id"], {"to": ["jane@example.com"]})
|
|
109
|
+
inboxjot.campaigns.send(campaign["id"])
|
|
110
|
+
inboxjot.campaigns.send(campaign["id"], {"scheduled_at": "2026-10-20T09:00:00Z"})
|
|
111
|
+
inboxjot.campaigns.cancel(campaign["id"])
|
|
112
|
+
inboxjot.campaigns.delete(campaign["id"])
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Templates
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
template = inboxjot.templates.create({"name": "Welcome", "starter_id": "welcome"})
|
|
119
|
+
inboxjot.templates.list({"sort": "name"})
|
|
120
|
+
inboxjot.templates.get(template["id"])
|
|
121
|
+
inboxjot.templates.update(template["id"], {"subject": "Welcome"})
|
|
122
|
+
inboxjot.templates.publish(template["id"])
|
|
123
|
+
inboxjot.templates.delete(template["id"])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Versions
|
|
127
|
+
|
|
128
|
+
Campaigns and templates have the same version methods. `update_version` always takes `name`, and `None` clears it.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
version = inboxjot.templates.create_version(template["id"], {"name": "Launch"})
|
|
132
|
+
inboxjot.templates.list_versions(template["id"], {"filter": "saved"})
|
|
133
|
+
inboxjot.templates.get_version(template["id"], version["id"])
|
|
134
|
+
inboxjot.templates.update_version(template["id"], version["id"], {"name": None})
|
|
135
|
+
inboxjot.templates.restore_version(template["id"], version["id"])
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Domains
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
domain = inboxjot.domains.create({"name": "acme.dev"})
|
|
142
|
+
inboxjot.domains.list()
|
|
143
|
+
inboxjot.domains.get(domain["id"])
|
|
144
|
+
inboxjot.domains.verify(domain["id"])
|
|
145
|
+
inboxjot.domains.delete(domain["id"])
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Images
|
|
149
|
+
|
|
150
|
+
An image that is already online goes straight into email content as `<img src="https://…">`. To host a file on Inboxjot, upload it. The SDK accepts PNG, JPEG and GIF files.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
with open("logo.png", "rb") as file:
|
|
154
|
+
image = inboxjot.images.upload({"file": file.read(), "file_name": "logo.png"})
|
|
155
|
+
|
|
156
|
+
inboxjot.images.list({"type": "png"})
|
|
157
|
+
inboxjot.images.get(image["id"])
|
|
158
|
+
inboxjot.images.delete(image["id"])
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Segments and topics
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
segment = inboxjot.segments.create({"name": "Beta", "type": "static"})
|
|
165
|
+
inboxjot.segments.list({"type": "static"})
|
|
166
|
+
inboxjot.segments.get(segment["id"])
|
|
167
|
+
inboxjot.segments.update(segment["id"], {"name": "Early Access"})
|
|
168
|
+
inboxjot.segments.add_members(segment["id"], {"contact_ids": ["ct_..."]})
|
|
169
|
+
inboxjot.segments.list_members(segment["id"], {"query": "jane"})
|
|
170
|
+
inboxjot.segments.remove_members(segment["id"], {"contact_ids": ["ct_..."]})
|
|
171
|
+
inboxjot.segments.delete(segment["id"])
|
|
172
|
+
|
|
173
|
+
topic = inboxjot.topics.create({"name": "Product News"})
|
|
174
|
+
inboxjot.topics.list()
|
|
175
|
+
inboxjot.topics.get(topic["id"])
|
|
176
|
+
inboxjot.topics.update(topic["id"], {"visibility": "private"})
|
|
177
|
+
inboxjot.topics.delete(topic["id"])
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Pagination
|
|
181
|
+
|
|
182
|
+
Lists page with a cursor. Pass `next_cursor` back as `after`. It is `None` on the last page, and the client leaves a `None` cursor out of the request.
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
after = None
|
|
186
|
+
while True:
|
|
187
|
+
page = inboxjot.contacts.list({"per_page": 100, "after": after})
|
|
188
|
+
after = page["next_cursor"]
|
|
189
|
+
if after is None:
|
|
190
|
+
break
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Idempotency and retries
|
|
194
|
+
|
|
195
|
+
Every method takes request options as keyword arguments:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
inboxjot.emails.send(email, idempotency_key=f"welcome-{user.id}")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
| Option | Use |
|
|
202
|
+
| ----------------- | ------------------------------------------- |
|
|
203
|
+
| `idempotency_key` | Sends the `Idempotency-Key` header. |
|
|
204
|
+
| `timeout` | Overrides the client timeout, in seconds. |
|
|
205
|
+
| `max_retries` | Overrides the client retry count. |
|
|
206
|
+
|
|
207
|
+
The client retries with exponential backoff, and it honors `Retry-After`. A `Retry-After` over 60 seconds raises the error with no retry:
|
|
208
|
+
|
|
209
|
+
- `429` and `409 concurrent_idempotent_requests` always retry. The API did not run these requests.
|
|
210
|
+
- `5xx`, timeouts and network errors retry only when the retry is safe: a `GET` or `DELETE`, or a request with an `idempotency_key`.
|
|
211
|
+
|
|
212
|
+
An email send without a key is never retried after a network error, because that can send the email twice. Give a key to make it safe.
|
|
213
|
+
|
|
214
|
+
### Errors
|
|
215
|
+
|
|
216
|
+
A failed request raises. An API error is an `InboxjotError` with the fields of the API error body. A network error or timeout is an `InboxjotConnectionError`.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from inboxjot import InboxjotError
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
inboxjot.emails.get("em_...")
|
|
223
|
+
except InboxjotError as error:
|
|
224
|
+
if error.name == "not_found":
|
|
225
|
+
... # error.status, error.message, error.errors, error.request_id, error.headers
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The SDK raises instead of returning `(data, error)`. A result tuple makes you check every call, and an unchecked error turns into `None` data later in the code. A raised error stops the flow at the failed call, and it works with `asyncio.gather` and with the retry loop. `error.name` is typed as a `Literal` of the API error names.
|
|
229
|
+
|
|
230
|
+
### Webhooks
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
webhook = inboxjot.webhooks.create(
|
|
234
|
+
{"name": "Production", "url": "https://acme.dev/hook", "events": ["email.bounced"]}
|
|
235
|
+
)
|
|
236
|
+
print(webhook["secret"])
|
|
237
|
+
inboxjot.webhooks.list()
|
|
238
|
+
inboxjot.webhooks.get(webhook["id"])
|
|
239
|
+
inboxjot.webhooks.update(webhook["id"], {"status": "disabled"})
|
|
240
|
+
inboxjot.webhooks.delete(webhook["id"])
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Inboxjot signs webhooks with the [Standard Webhooks](https://www.standardwebhooks.com) spec. Verify the raw request body before you parse it:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
import os
|
|
247
|
+
|
|
248
|
+
from fastapi import FastAPI, Request, Response
|
|
249
|
+
from inboxjot import verify_webhook
|
|
250
|
+
|
|
251
|
+
app = FastAPI()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@app.post("/webhooks/inboxjot")
|
|
255
|
+
async def handle(request: Request):
|
|
256
|
+
event = verify_webhook(
|
|
257
|
+
payload=await request.body(),
|
|
258
|
+
headers=request.headers,
|
|
259
|
+
secret=os.environ["INBOXJOT_WEBHOOK_SECRET"],
|
|
260
|
+
)
|
|
261
|
+
if event["type"] == "email.bounced":
|
|
262
|
+
print(event["data"]["email_id"], event["data"]["bounce"]["type"])
|
|
263
|
+
elif event["type"] == "contact.created":
|
|
264
|
+
print(event["data"]["contact_id"])
|
|
265
|
+
return Response(status_code=204)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`inboxjot.webhooks.verify` is the same function. The payload can be `str` or `bytes`, and the headers can be any mapping, such as a `dict` or a framework's headers object. A bad signature, a missing header or a timestamp more than 5 minutes off raises a `WebhookVerificationError`.
|
|
269
|
+
|
|
270
|
+
## Types
|
|
271
|
+
|
|
272
|
+
`inboxjot.types` has a `TypedDict` or `Literal` for each schema in the Inboxjot OpenAPI spec, with the names the TypeScript SDK uses (`SendEmailOptions`, `Email`, `Contact`, `WebhookEvent` and the others). Request and response bodies are plain dicts with the API's own keys, so `scheduled_at` stays `scheduled_at`. `WebhookEvent` is a union that a type checker narrows on `event["type"]`.
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from inboxjot.types import Email, SendEmailOptions
|
|
276
|
+
|
|
277
|
+
email: SendEmailOptions = {"to": "jane@example.com", "subject": "Hi", "text": "Hi"}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The client does not validate responses, so a new enum value or field reaches your code as is.
|
|
281
|
+
|
|
282
|
+
If a type is wrong, fix the spec in the API and update `src/inboxjot/types.py` to match.
|
|
283
|
+
|
|
284
|
+
## Layout
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
src/inboxjot/_client.py Inboxjot, AsyncInboxjot and request(): auth, idempotency, timeout, retries
|
|
288
|
+
src/inboxjot/_emails.py emails.list, send, batch, get, update, cancel, list_attachments, get_attachment
|
|
289
|
+
src/inboxjot/_contacts.py contacts.list, create, get, update, remove, erase
|
|
290
|
+
src/inboxjot/_events.py events.send
|
|
291
|
+
src/inboxjot/_campaigns.py campaigns.list, create, get, update, remove, send, cancel, send_test, versions
|
|
292
|
+
src/inboxjot/_templates.py templates.list, create, get, update, remove, publish, versions
|
|
293
|
+
src/inboxjot/_domains.py domains.list, create, get, remove, verify
|
|
294
|
+
src/inboxjot/_images.py images.list, upload, get, remove
|
|
295
|
+
src/inboxjot/_segments.py segments.list, create, get, update, remove, list_members, add_members, remove_members
|
|
296
|
+
src/inboxjot/_topics.py topics.list, create, get, update, remove
|
|
297
|
+
src/inboxjot/_webhooks.py webhooks.list, create, get, update, remove, verify, verify_webhook
|
|
298
|
+
src/inboxjot/_errors.py InboxjotError, InboxjotConnectionError, WebhookVerificationError
|
|
299
|
+
src/inboxjot/types.py the request and response types
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Development
|
|
303
|
+
|
|
304
|
+
```sh
|
|
305
|
+
uv sync
|
|
306
|
+
uv run pytest
|
|
307
|
+
uv run ruff check
|
|
308
|
+
uv run ruff format
|
|
309
|
+
uv run pyright
|
|
310
|
+
uv build
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
To release, run `uv version --bump patch` (or `minor`), commit `chore: release vX.Y.Z`, then tag `vX.Y.Z` and push the tag. The release workflow checks, builds and publishes to PyPI.
|