Skip to main content

avelto

The official Python SDK for Avelto, the email API for developers who want it to just work.

  • Python 3.9+
  • Sync and asyncio clients with the same surface
  • Typed: every response is a plain dict with a TypedDict describing its keys
  • One dependency, httpx

Install

pip install avelto

Quickstart

import os
from avelto import Avelto

avelto = Avelto(os.environ["AVELTO_API_KEY"])

email = avelto.emails.send(
    from_="Acme <hello@mail.acme.com>",
    to="jane@example.com",
    subject="Welcome to Acme",
    html="<p>Thanks for signing up.</p>",
    text="Thanks for signing up.",
)
print(email["id"])  # "5b3d…"

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.

Every method returns the API's JSON as a dict, exactly as the reference documents it, so email["id"] rather than email.id.

asyncio

from avelto import AsyncAvelto

async with AsyncAvelto(os.environ["AVELTO_API_KEY"]) as avelto:
    email = await avelto.emails.send(from_="hello@mail.acme.com", to="jane@example.com", subject="Hi", text="Hello")

Same options, same methods, each one awaited.

Options

avelto = Avelto(
    api_key,
    base_url="https://api.avelto.dev",  # also read from AVELTO_BASE_URL
    timeout=30.0,                        # per request, seconds
    max_attempts=3, base_delay=0.3, max_delay=5.0,  # the retry defaults
)

Avelto holds a connection pool. Use it as a context manager or call close() when you are done (aclose() on the async client).

Idempotency

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.

email = avelto.emails.send(
    from_="hello@mail.acme.com", to="jane@example.com", subject="Receipt #1042", text="…",
    idempotency_key="receipt-1042",
)

Retries

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 POSTs 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.

Scheduled send and cancel

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.

from datetime import datetime, timedelta, timezone

email = avelto.emails.send(
    from_="hello@mail.acme.com",
    to="jane@example.com",
    subject="Your trial ends tomorrow",
    text="…",
    scheduled_at=(datetime.now(timezone.utc) + timedelta(days=1)).isoformat(),
)

cancelled = avelto.emails.cancel(email["id"])  # cancelled["status"] == "cancelled"

Cancelling an email that is no longer scheduled raises an AveltoError with code not_scheduled.

Validate an address

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.

check = avelto.emails.validate("jane@gmial.com")
check["result"]      # "deliverable" | "risky" | "undeliverable"
check["reason"]      # None | "invalid_syntax" | "suppressed" | "no_mail_server" | "disposable" | "possible_typo" | "role_address"
check["suggestion"]  # "jane@gmail.com"
check["normalized"]  # the lower-cased address to store, or None when the syntax is invalid
check["checks"]      # {"syntax", "mail_server", "disposable", "role", "free_provider", "suppressed"}

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.

Batch send

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].

batch = avelto.emails.send_batch([
    {"from": "billing@mail.acme.com", "to": "jane@example.com", "subject": "Receipt #1042", "text": "…"},
    {"from": "billing@mail.acme.com", "to": "sam@example.com", "subject": "Receipt #1043", "text": "…"},
], idempotency_key="receipts-2026-09-24")

for r in batch["results"]:
    if r["ok"]:
        print(r["index"], r["id"])
    else:
        print(r["index"], r["error"]["code"], r["error"]["message"])

Fetch and list emails

email = avelto.emails.get(email_id)
print(email["status"])                          # "queued" | "scheduled" | "sent" | "delivered" | "bounced" | "complained" | "failed" | "cancelled"
print([e["type"] for e in email["events"]])     # ["email.queued", "email.sent", "email.delivered"]

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.

cursor = None
while True:
    page = avelto.emails.list(status="bounced", tag="onboarding", limit=100, cursor=cursor)
    for e in page["data"]:
        print(e["id"], e["to"], e["subject"])
    cursor = page["next_cursor"]
    if not cursor:
        break

Domains

Add a domain, publish the DNS records it returns, then poll get until it is verified. domains.get re-checks verification on every call.

import time

domain = avelto.domains.create("mail.acme.com")
for r in domain["dns_records"]:
    print(r["type"], r["name"], r["value"], f"({r['purpose']})", sep="\t")

# Publish the records, then:
status = domain["status"]
while status == "pending":
    time.sleep(30)
    status = avelto.domains.get(domain["id"])["status"]
print(status)  # "verified" (or "failed")

avelto.domains.list()
avelto.domains.delete(domain["id"])

Templates

tpl = avelto.templates.create(name="Welcome", subject="Hi {{name}}", html="<p>Welcome, {{name}}.</p>")
avelto.emails.send(from_="hello@mail.acme.com", to="jane@example.com", template_slug=tpl["slug"], variables={"name": "Jane"})

avelto.templates.update(tpl["id"], subject="Hello {{name}}")   # saves a new version
avelto.templates.update(tpl["id"], text=None)                  # None clears a body part; leaving it out keeps it
versions = avelto.templates.versions(tpl["id"])["data"]
avelto.templates.restore(tpl["id"], versions[-1]["version"])

Webhooks

Create an endpoint. The signing secret is returned once, on creation. Store it.

endpoint = avelto.webhooks.create(
    "https://acme.com/hooks/avelto",
    events=["email.delivered", "email.bounced", "email.complained"],  # optional; defaults to all eight
)
print(endpoint["secret"])

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.

Flask

import json, os
from flask import Flask, request, abort
from avelto import verify_webhook_signature

app = Flask(__name__)

@app.post("/hooks/avelto")
def avelto_hook():
    body = request.get_data()  # raw bytes, before any parsing
    if not verify_webhook_signature(os.environ["AVELTO_WEBHOOK_SECRET"], body, request.headers.get("Avelto-Signature")):
        abort(400)
    event = json.loads(body)
    if event["type"] == "email.bounced":
        print("bounced", event["data"]["email_id"], event["data"]["details"])
    return "", 200

Django

import json, os
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from avelto import verify_webhook_signature

@csrf_exempt
@require_POST
def avelto_hook(request):
    if not verify_webhook_signature(os.environ["AVELTO_WEBHOOK_SECRET"], request.body, request.headers.get("Avelto-Signature")):
        return HttpResponseBadRequest("invalid signature")
    event = json.loads(request.body)
    print(event["type"], event["data"]["email_id"])
    return HttpResponse(status=200)

Signatures older than five minutes are rejected; pass tolerance_seconds= to change that.

Deliveries

endpoints = avelto.webhooks.list()["data"]
endpoint = avelto.webhooks.get(endpoint_id)

page = avelto.webhooks.list_deliveries(endpoint_id, limit=50)
failed = next((d for d in page["data"] if d["status"] == "failed"), None)
if failed:
    avelto.webhooks.retry_delivery(endpoint_id, failed["id"])

avelto.webhooks.delete(endpoint_id)

Suppressions

Addresses that hard-bounce or complain are suppressed automatically and sends to them are rejected. You can manage the list yourself.

page = avelto.suppressions.list(limit=100)
for s in page["data"]:
    print(s["email_address"], s["reason"])  # "bounce" | "complaint" | "manual"

avelto.suppressions.create("unsubscribed@example.com")
avelto.suppressions.delete("unsubscribed@example.com")

Account

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.

Error handling

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.

from avelto import AveltoError

try:
    avelto.emails.send(from_="hello@mail.acme.com", to="jane@example.com", subject="Hi", text="Hi")
except AveltoError as err:
    if err.code == "domain_not_verified":            # 403: verify mail.acme.com first
        ...
    elif err.code == "sandbox_recipient_not_allowed":  # 403: the sandbox sender only delivers to your account's owner email
        ...
    elif err.code == "recipient_suppressed":         # 422: address is on your suppression list
        ...
    elif err.code == "validation_error":             # 400: err.details lists the failing fields
        ...
    elif err.is_network_error:                       # status 0: no response (DNS, TLS, timeout)
        ...
    else:
        print(err.status, err.code, err.message, err.details)
Code Status When
validation_error 400 Request body or query failed validation. details lists the issues.
unauthorized 401 Missing or invalid API key.
forbidden 403 Key is not allowed to do this.
not_found 404 No such email, domain, webhook or suppression.
conflict 409 Resource already exists (for example the domain).
not_scheduled 409 emails.cancel on an email that is not scheduled.
sandbox_recipient_not_allowed 403 Sending from the sandbox domain to anyone but your account's owner email.
domain_not_verified 403 from_ uses a domain that is not added and verified on your account.
recipient_suppressed 422 A recipient is on your suppression list.
plan_limit 429 / 403 Monthly or first-week sending cap reached (429), or the plan does not allow another domain (403).
account_paused 403 Sending is paused on the account. Check the dashboard.
account_suspended 403 The account is suspended; the message says why. Nothing sends until it is lifted.
rate_limited 429 Too many requests for this key; honour retry_after_seconds.
internal_error 5xx Something went wrong on our side.
network_error 0 The request never got a response. err.__cause__ holds the underlying exception.

Test mode

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.

Use these recipients on your sandbox domain to simulate outcomes, in both live and test mode:

  • delivered@<your sandbox domain> - delivered
  • bounced@<your sandbox domain> - hard bounce (never added to suppressions)
  • complained@<your sandbox domain> - complaint (never added to suppressions)

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.

Types

Every response shape is a TypedDict in avelto.types, so an editor knows the keys:

from avelto.types import Email, EmailSummary, Domain, WebhookEndpoint, WebhookPayload, Suppression, ValidateEmailResponse

License

MIT

Metadata

Release files for avelto 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for avelto 0.1.0
File Size Uploaded Provenance
avelto-0.1.0.tar.gz 19.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for avelto 0.1.0
File Interpreter ABI Platform
avelto-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 44.5 kB

Release files / avelto-0.1.0.tar.gz

Download URL avelto-0.1.0.tar.gz
Size 19.5 kB
Tags Source
SHA-256 checksum
How to use checksums
64e8aea463cb138da1153c78154564a1a4c899d1aae15451620a78607c54818d
BLAKE2b-256 checksum
How to use checksums
caf65716a2266107bc0c78d43c82c5b8f1234ccd676e5e2c2472899e8a46d061
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / avelto-0.1.0-py3-none-any.whl

Download URL avelto-0.1.0-py3-none-any.whl
Size 24.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6ba5c7d1a2218147e4dafb6ca36b1e0eeeb313e9a42cb50b78ecb4025d70fe7b
BLAKE2b-256 checksum
How to use checksums
a0ed6afa43c09da6fae8a7b1fe478a3aef55bf851071d4d935f7eeb70d65c5e3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Security for avelto 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page