avelto 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.
- avelto-0.1.0/.gitignore +36 -0
- avelto-0.1.0/LICENSE +21 -0
- avelto-0.1.0/PKG-INFO +366 -0
- avelto-0.1.0/README.md +334 -0
- avelto-0.1.0/pyproject.toml +55 -0
- avelto-0.1.0/src/avelto/__init__.py +31 -0
- avelto-0.1.0/src/avelto/_http.py +211 -0
- avelto-0.1.0/src/avelto/_specs.py +234 -0
- avelto-0.1.0/src/avelto/_version.py +1 -0
- avelto-0.1.0/src/avelto/async_client.py +288 -0
- avelto-0.1.0/src/avelto/client.py +335 -0
- avelto-0.1.0/src/avelto/errors.py +48 -0
- avelto-0.1.0/src/avelto/py.typed +0 -0
- avelto-0.1.0/src/avelto/signature.py +51 -0
- avelto-0.1.0/src/avelto/types.py +439 -0
avelto-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
.next/
|
|
4
|
+
out/
|
|
5
|
+
coverage/
|
|
6
|
+
*.log
|
|
7
|
+
.DS_Store
|
|
8
|
+
.env
|
|
9
|
+
.env.*
|
|
10
|
+
!.env.example
|
|
11
|
+
!.env.*.example
|
|
12
|
+
backups/
|
|
13
|
+
.turbo/
|
|
14
|
+
deploy/nginx/*.rendered
|
|
15
|
+
.deploy-state/
|
|
16
|
+
|
|
17
|
+
# Credentials and certificates. Nothing matching these has ever been committed; the
|
|
18
|
+
# patterns are here so that the first time somebody drops a key in the tree, git does
|
|
19
|
+
# not offer to commit it.
|
|
20
|
+
*.pem
|
|
21
|
+
*.key
|
|
22
|
+
*.p12
|
|
23
|
+
*.pfx
|
|
24
|
+
.aws/
|
|
25
|
+
|
|
26
|
+
# Playwright, which the screenshot script drives. It writes its output straight into
|
|
27
|
+
# apps/web/public/screenshots (those are committed on purpose), but a failed or traced
|
|
28
|
+
# run leaves these behind.
|
|
29
|
+
test-results/
|
|
30
|
+
playwright-report/
|
|
31
|
+
.playwright/
|
|
32
|
+
blob-report/
|
|
33
|
+
|
|
34
|
+
# Build bookkeeping and editor/host state.
|
|
35
|
+
*.tsbuildinfo
|
|
36
|
+
.vercel/
|
avelto-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Avelto
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
avelto-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: avelto
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Avelto, the email API for developers who want it to just work
|
|
5
|
+
Project-URL: Homepage, https://avelto.dev
|
|
6
|
+
Project-URL: Documentation, https://avelto.dev/docs/sdk-python
|
|
7
|
+
Project-URL: Source, https://github.com/aveltohq/avelto/tree/master/packages/sdk-python
|
|
8
|
+
Author: Avelto
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,avelto,email,sdk,transactional,webhooks
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Communications :: Email
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx<1,>=0.24
|
|
26
|
+
Requires-Dist: typing-extensions>=4.5; python_version < '3.11'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# avelto
|
|
34
|
+
|
|
35
|
+
The official Python SDK for [Avelto](https://avelto.dev), the email API for developers who want it to just work.
|
|
36
|
+
|
|
37
|
+
- Python 3.9+
|
|
38
|
+
- Sync and asyncio clients with the same surface
|
|
39
|
+
- Typed: every response is a plain dict with a `TypedDict` describing its keys
|
|
40
|
+
- One dependency, `httpx`
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pip install avelto
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quickstart
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import os
|
|
52
|
+
from avelto import Avelto
|
|
53
|
+
|
|
54
|
+
avelto = Avelto(os.environ["AVELTO_API_KEY"])
|
|
55
|
+
|
|
56
|
+
email = avelto.emails.send(
|
|
57
|
+
from_="Acme <hello@mail.acme.com>",
|
|
58
|
+
to="jane@example.com",
|
|
59
|
+
subject="Welcome to Acme",
|
|
60
|
+
html="<p>Thanks for signing up.</p>",
|
|
61
|
+
text="Thanks for signing up.",
|
|
62
|
+
)
|
|
63
|
+
print(email["id"]) # "5b3d…"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`from_` is the sender, because `from` is a Python keyword. `to`, `cc` and `bcc` take one address or a list. Addresses can be `a@b.com` or `Name <a@b.com>`. Provide `html`, `text`, or both. You can also pass `reply_to`, `headers`, `tags` (up to 10), `attachments` (dicts with `filename` and base64 `content` or an http(s) `url`, up to 10 files and 7 MB in total), `scheduled_at` and `unsubscribe_url`. To send a stored template instead of a body, pass `template_id` or `template_slug` with `variables` and leave `subject`, `html` and `text` unset.
|
|
67
|
+
|
|
68
|
+
Every method returns the API's JSON as a dict, exactly as the [reference](https://avelto.dev/docs/api) documents it, so `email["id"]` rather than `email.id`.
|
|
69
|
+
|
|
70
|
+
### asyncio
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from avelto import AsyncAvelto
|
|
74
|
+
|
|
75
|
+
async with AsyncAvelto(os.environ["AVELTO_API_KEY"]) as avelto:
|
|
76
|
+
email = await avelto.emails.send(from_="hello@mail.acme.com", to="jane@example.com", subject="Hi", text="Hello")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Same options, same methods, each one awaited.
|
|
80
|
+
|
|
81
|
+
### Options
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
avelto = Avelto(
|
|
85
|
+
api_key,
|
|
86
|
+
base_url="https://api.avelto.dev", # also read from AVELTO_BASE_URL
|
|
87
|
+
timeout=30.0, # per request, seconds
|
|
88
|
+
max_attempts=3, base_delay=0.3, max_delay=5.0, # the retry defaults
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`Avelto` holds a connection pool. Use it as a context manager or call `close()` when you are done (`aclose()` on the async client).
|
|
93
|
+
|
|
94
|
+
## Idempotency
|
|
95
|
+
|
|
96
|
+
Every `emails.send` carries an `Idempotency-Key` header: a fresh random UUID per call by default, so the SDK's own retries can never create two emails. Pass your own key (an order id, say) to make retries at your level safe too. A replay returns the id of the email that was already created and does not send again.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
email = avelto.emails.send(
|
|
100
|
+
from_="hello@mail.acme.com", to="jane@example.com", subject="Receipt #1042", text="…",
|
|
101
|
+
idempotency_key="receipt-1042",
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Retries
|
|
106
|
+
|
|
107
|
+
The SDK makes up to three attempts at a failed request (the call plus two retries) with exponential backoff and jitter; a `429` waits for the server's `Retry-After` instead. `429`, `502` and `503` are retried for every request. Network errors, timeouts and `504` (where the request may have been processed) are retried only for `GET`, `DELETE`, `emails.send`, `emails.send_batch` and `emails.validate`, never for the other `POST`s or for `PATCH`. Pass `max_attempts=1` to turn retries off. After the last attempt the `AveltoError` is raised; on `429` it carries `retry_after_seconds`.
|
|
108
|
+
|
|
109
|
+
## Scheduled send and cancel
|
|
110
|
+
|
|
111
|
+
`scheduled_at` is an ISO 8601 timestamp, in the future and at most 30 days ahead. A scheduled email can be cancelled until it is sent.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from datetime import datetime, timedelta, timezone
|
|
115
|
+
|
|
116
|
+
email = avelto.emails.send(
|
|
117
|
+
from_="hello@mail.acme.com",
|
|
118
|
+
to="jane@example.com",
|
|
119
|
+
subject="Your trial ends tomorrow",
|
|
120
|
+
text="…",
|
|
121
|
+
scheduled_at=(datetime.now(timezone.utc) + timedelta(days=1)).isoformat(),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
cancelled = avelto.emails.cancel(email["id"]) # cancelled["status"] == "cancelled"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Cancelling an email that is no longer `scheduled` raises an `AveltoError` with code `not_scheduled`.
|
|
128
|
+
|
|
129
|
+
## Validate an address
|
|
130
|
+
|
|
131
|
+
Ask whether an address is worth a send before you spend one on it, for example on every submit of a signup form. Nothing is sent or stored.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
check = avelto.emails.validate("jane@gmial.com")
|
|
135
|
+
check["result"] # "deliverable" | "risky" | "undeliverable"
|
|
136
|
+
check["reason"] # None | "invalid_syntax" | "suppressed" | "no_mail_server" | "disposable" | "possible_typo" | "role_address"
|
|
137
|
+
check["suggestion"] # "jane@gmail.com"
|
|
138
|
+
check["normalized"] # the lower-cased address to store, or None when the syntax is invalid
|
|
139
|
+
check["checks"] # {"syntax", "mail_server", "disposable", "role", "free_provider", "suppressed"}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`undeliverable` means a send would fail or be refused: bad syntax, a domain with no mail server, or an address on your own suppression list. `risky` means it may well deliver but deserves a look: a throwaway domain, a role address such as `info@`, or a domain within a typo of a well-known one, in which case `suggestion` is the corrected address to offer the user. It cannot tell you whether the mailbox exists; nobody can without sending.
|
|
143
|
+
|
|
144
|
+
## Batch send
|
|
145
|
+
|
|
146
|
+
`emails.send_batch` sends up to 100 messages in one call. Each message is a dict with the same keys as `send` (`"from"`, or `"from_"` if you prefer to avoid the keyword in a dict literal too). Each is accepted or refused on its own, so read `results` rather than assuming the call either worked or did not; `results[i]` lines up with `messages[i]`.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
batch = avelto.emails.send_batch([
|
|
150
|
+
{"from": "billing@mail.acme.com", "to": "jane@example.com", "subject": "Receipt #1042", "text": "…"},
|
|
151
|
+
{"from": "billing@mail.acme.com", "to": "sam@example.com", "subject": "Receipt #1043", "text": "…"},
|
|
152
|
+
], idempotency_key="receipts-2026-09-24")
|
|
153
|
+
|
|
154
|
+
for r in batch["results"]:
|
|
155
|
+
if r["ok"]:
|
|
156
|
+
print(r["index"], r["id"])
|
|
157
|
+
else:
|
|
158
|
+
print(r["index"], r["error"]["code"], r["error"]["message"])
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Fetch and list emails
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
email = avelto.emails.get(email_id)
|
|
165
|
+
print(email["status"]) # "queued" | "scheduled" | "sent" | "delivered" | "bounced" | "complained" | "failed" | "cancelled"
|
|
166
|
+
print([e["type"] for e in email["events"]]) # ["email.queued", "email.sent", "email.delivered"]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Lists are newest first and cursor-paginated. Filter by `status`, `tag`, `mode` and `q`, a substring search over recipient, subject and message id. A live key defaults to live and may ask for test; a test key only ever sees test emails, whatever `mode` says, and a live email is a `404` to it.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
cursor = None
|
|
173
|
+
while True:
|
|
174
|
+
page = avelto.emails.list(status="bounced", tag="onboarding", limit=100, cursor=cursor)
|
|
175
|
+
for e in page["data"]:
|
|
176
|
+
print(e["id"], e["to"], e["subject"])
|
|
177
|
+
cursor = page["next_cursor"]
|
|
178
|
+
if not cursor:
|
|
179
|
+
break
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Domains
|
|
183
|
+
|
|
184
|
+
Add a domain, publish the DNS records it returns, then poll `get` until it is verified. `domains.get` re-checks verification on every call.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
import time
|
|
188
|
+
|
|
189
|
+
domain = avelto.domains.create("mail.acme.com")
|
|
190
|
+
for r in domain["dns_records"]:
|
|
191
|
+
print(r["type"], r["name"], r["value"], f"({r['purpose']})", sep="\t")
|
|
192
|
+
|
|
193
|
+
# Publish the records, then:
|
|
194
|
+
status = domain["status"]
|
|
195
|
+
while status == "pending":
|
|
196
|
+
time.sleep(30)
|
|
197
|
+
status = avelto.domains.get(domain["id"])["status"]
|
|
198
|
+
print(status) # "verified" (or "failed")
|
|
199
|
+
|
|
200
|
+
avelto.domains.list()
|
|
201
|
+
avelto.domains.delete(domain["id"])
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Templates
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
tpl = avelto.templates.create(name="Welcome", subject="Hi {{name}}", html="<p>Welcome, {{name}}.</p>")
|
|
208
|
+
avelto.emails.send(from_="hello@mail.acme.com", to="jane@example.com", template_slug=tpl["slug"], variables={"name": "Jane"})
|
|
209
|
+
|
|
210
|
+
avelto.templates.update(tpl["id"], subject="Hello {{name}}") # saves a new version
|
|
211
|
+
avelto.templates.update(tpl["id"], text=None) # None clears a body part; leaving it out keeps it
|
|
212
|
+
versions = avelto.templates.versions(tpl["id"])["data"]
|
|
213
|
+
avelto.templates.restore(tpl["id"], versions[-1]["version"])
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Webhooks
|
|
217
|
+
|
|
218
|
+
Create an endpoint. The signing `secret` is returned once, on creation. Store it.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
endpoint = avelto.webhooks.create(
|
|
222
|
+
"https://acme.com/hooks/avelto",
|
|
223
|
+
events=["email.delivered", "email.bounced", "email.complained"], # optional; defaults to all eight
|
|
224
|
+
)
|
|
225
|
+
print(endpoint["secret"])
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Every delivery is a JSON POST with an `Avelto-Signature` header of the form `t=<unix seconds>,v1=<hex>`. Verify it against the raw body before you trust the payload. Use the event `id` to de-duplicate.
|
|
229
|
+
|
|
230
|
+
### Flask
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
import json, os
|
|
234
|
+
from flask import Flask, request, abort
|
|
235
|
+
from avelto import verify_webhook_signature
|
|
236
|
+
|
|
237
|
+
app = Flask(__name__)
|
|
238
|
+
|
|
239
|
+
@app.post("/hooks/avelto")
|
|
240
|
+
def avelto_hook():
|
|
241
|
+
body = request.get_data() # raw bytes, before any parsing
|
|
242
|
+
if not verify_webhook_signature(os.environ["AVELTO_WEBHOOK_SECRET"], body, request.headers.get("Avelto-Signature")):
|
|
243
|
+
abort(400)
|
|
244
|
+
event = json.loads(body)
|
|
245
|
+
if event["type"] == "email.bounced":
|
|
246
|
+
print("bounced", event["data"]["email_id"], event["data"]["details"])
|
|
247
|
+
return "", 200
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Django
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
import json, os
|
|
254
|
+
from django.http import HttpResponse, HttpResponseBadRequest
|
|
255
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
256
|
+
from django.views.decorators.http import require_POST
|
|
257
|
+
from avelto import verify_webhook_signature
|
|
258
|
+
|
|
259
|
+
@csrf_exempt
|
|
260
|
+
@require_POST
|
|
261
|
+
def avelto_hook(request):
|
|
262
|
+
if not verify_webhook_signature(os.environ["AVELTO_WEBHOOK_SECRET"], request.body, request.headers.get("Avelto-Signature")):
|
|
263
|
+
return HttpResponseBadRequest("invalid signature")
|
|
264
|
+
event = json.loads(request.body)
|
|
265
|
+
print(event["type"], event["data"]["email_id"])
|
|
266
|
+
return HttpResponse(status=200)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Signatures older than five minutes are rejected; pass `tolerance_seconds=` to change that.
|
|
270
|
+
|
|
271
|
+
### Deliveries
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
endpoints = avelto.webhooks.list()["data"]
|
|
275
|
+
endpoint = avelto.webhooks.get(endpoint_id)
|
|
276
|
+
|
|
277
|
+
page = avelto.webhooks.list_deliveries(endpoint_id, limit=50)
|
|
278
|
+
failed = next((d for d in page["data"] if d["status"] == "failed"), None)
|
|
279
|
+
if failed:
|
|
280
|
+
avelto.webhooks.retry_delivery(endpoint_id, failed["id"])
|
|
281
|
+
|
|
282
|
+
avelto.webhooks.delete(endpoint_id)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Suppressions
|
|
286
|
+
|
|
287
|
+
Addresses that hard-bounce or complain are suppressed automatically and sends to them are rejected. You can manage the list yourself.
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
page = avelto.suppressions.list(limit=100)
|
|
291
|
+
for s in page["data"]:
|
|
292
|
+
print(s["email_address"], s["reason"]) # "bounce" | "complaint" | "manual"
|
|
293
|
+
|
|
294
|
+
avelto.suppressions.create("unsubscribed@example.com")
|
|
295
|
+
avelto.suppressions.delete("unsubscribed@example.com")
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Account
|
|
299
|
+
|
|
300
|
+
`account.get()` returns the key's mode and scopes, the sandbox domain with every recipient a sandbox send may reach, and the domains, webhook endpoints and templates that exist. Read it first when wiring up a new project.
|
|
301
|
+
|
|
302
|
+
## Error handling
|
|
303
|
+
|
|
304
|
+
Every failed request raises `AveltoError` with the HTTP `status`, the API `code`, the `message`, optional `details`, the `request_id` the API answered with, and on a `429` the `retry_after_seconds`. A request that got no response at all has status `0` and code `network_error`, with the underlying exception as `__cause__`; `is_network_error` is true for exactly those.
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from avelto import AveltoError
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
avelto.emails.send(from_="hello@mail.acme.com", to="jane@example.com", subject="Hi", text="Hi")
|
|
311
|
+
except AveltoError as err:
|
|
312
|
+
if err.code == "domain_not_verified": # 403: verify mail.acme.com first
|
|
313
|
+
...
|
|
314
|
+
elif err.code == "sandbox_recipient_not_allowed": # 403: the sandbox sender only delivers to your account's owner email
|
|
315
|
+
...
|
|
316
|
+
elif err.code == "recipient_suppressed": # 422: address is on your suppression list
|
|
317
|
+
...
|
|
318
|
+
elif err.code == "validation_error": # 400: err.details lists the failing fields
|
|
319
|
+
...
|
|
320
|
+
elif err.is_network_error: # status 0: no response (DNS, TLS, timeout)
|
|
321
|
+
...
|
|
322
|
+
else:
|
|
323
|
+
print(err.status, err.code, err.message, err.details)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
| Code | Status | When |
|
|
327
|
+
| --- | --- | --- |
|
|
328
|
+
| `validation_error` | 400 | Request body or query failed validation. `details` lists the issues. |
|
|
329
|
+
| `unauthorized` | 401 | Missing or invalid API key. |
|
|
330
|
+
| `forbidden` | 403 | Key is not allowed to do this. |
|
|
331
|
+
| `not_found` | 404 | No such email, domain, webhook or suppression. |
|
|
332
|
+
| `conflict` | 409 | Resource already exists (for example the domain). |
|
|
333
|
+
| `not_scheduled` | 409 | `emails.cancel` on an email that is not scheduled. |
|
|
334
|
+
| `sandbox_recipient_not_allowed` | 403 | Sending from the sandbox domain to anyone but your account's owner email. |
|
|
335
|
+
| `domain_not_verified` | 403 | `from_` uses a domain that is not added and verified on your account. |
|
|
336
|
+
| `recipient_suppressed` | 422 | A recipient is on your suppression list. |
|
|
337
|
+
| `plan_limit` | 429 / 403 | Monthly or first-week sending cap reached (429), or the plan does not allow another domain (403). |
|
|
338
|
+
| `account_paused` | 403 | Sending is paused on the account. Check the dashboard. |
|
|
339
|
+
| `account_suspended` | 403 | The account is suspended; the message says why. Nothing sends until it is lifted. |
|
|
340
|
+
| `rate_limited` | 429 | Too many requests for this key; honour `retry_after_seconds`. |
|
|
341
|
+
| `internal_error` | 5xx | Something went wrong on our side. |
|
|
342
|
+
| `network_error` | 0 | The request never got a response. `err.__cause__` holds the underlying exception. |
|
|
343
|
+
|
|
344
|
+
## Test mode
|
|
345
|
+
|
|
346
|
+
Keys starting with `av_test_` never send real email. They run the full pipeline and emit the same events and webhooks, so you can build against them without touching an inbox.
|
|
347
|
+
|
|
348
|
+
Use these recipients on your sandbox domain to simulate outcomes, in both live and test mode:
|
|
349
|
+
|
|
350
|
+
- `delivered@<your sandbox domain>` - delivered
|
|
351
|
+
- `bounced@<your sandbox domain>` - hard bounce (never added to suppressions)
|
|
352
|
+
- `complained@<your sandbox domain>` - complaint (never added to suppressions)
|
|
353
|
+
|
|
354
|
+
Your sandbox domain is shown in the dashboard. Sending from it to any other address only works for your account's owner email; verify a domain to send to anyone.
|
|
355
|
+
|
|
356
|
+
## Types
|
|
357
|
+
|
|
358
|
+
Every response shape is a `TypedDict` in `avelto.types`, so an editor knows the keys:
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
from avelto.types import Email, EmailSummary, Domain, WebhookEndpoint, WebhookPayload, Suppression, ValidateEmailResponse
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## License
|
|
365
|
+
|
|
366
|
+
MIT
|