postilon
Python client for the Postilon transactional email API. Typed, no dependencies, Python 3.9 or later.
Full API documentation: postilon.com/docs.
Install
pip install postilon
Send an email
Create an API key in the dashboard under API Keys, and keep it in an environment variable.
import os
from postilon import Postilon
postilon = Postilon(os.environ["POSTILON_API_KEY"])
email = postilon.emails.send({
"from": "Acme <hello@acme.com>",
"to": "jane@example.com",
"subject": "Your sign-in code",
"html": "<p>Your code is <strong>482913</strong>.</p>",
"text": "Your code is 482913.",
})
# {"id": "msg_…", "status": "queued", …}
Requests are plain dicts with the API's own field names (from is a Python keyword, so there are no keyword arguments for them), and every method returns the parsed JSON response as a dict. send takes every field of POST /v1/emails: cc, bcc, reply_to, headers, tags, attachments (Base64 content, or a file_id from postilon.files.upload), template_id with variables, and scheduled_at. postilon.types has TypedDict definitions for type checkers.
The client also covers emails.send_batch, get, list and cancel, and domains, templates, files, suppressions, webhooks and inbound. Postilon(key, base_url=…, timeout=30.0) changes the API address or the timeout in seconds.
Test keys
ps_test_… keys are for development: emails are accepted, rendered and reported like real ones, but nothing is delivered and no credits are used, and any from address works. ps_live_… keys send real mail from a verified domain. The SDK treats both the same.
Idempotency
Pass an idempotency key, such as the order or event ID the email is for, so a retried request never sends twice. It is sent as the Idempotency-Key header:
postilon.emails.send(
{"from": "billing@acme.com", "to": "jane@example.com", "subject": "Receipt for order #123", "text": "Thanks."},
idempotency_key="receipt-order-123",
)
A retry with the same key and body returns the original response. The same key with a different body fails with 409 idempotency_key_reused, so use one key per email.
Errors
When the API answers with an error, the call raises PostilonError with the HTTP status, the code and the message (list of codes). Network failures raise the urllib.error.URLError that urllib raised.
from postilon import PostilonError
try:
postilon.emails.send({"from": "hello@acme.com", "to": "jane@example.com", "subject": "Hi", "text": "Hi"})
except PostilonError as err:
if err.code == "rate_limit_exceeded":
... # err.rate_limit_reset: when the window resets, in Unix seconds (X-RateLimit-Reset)
else:
print(err.status, err.code, err.message) # 403 domain_not_verified …
A workspace can make 100 requests per second; use emails.send_batch for many emails at once. 429 daily_limit_reached means the daily sending limit is reached, and retrying within the second will not help. An error response that is not JSON (from a proxy, say) gets the code http_error.
Pagination
Lists return {"object": "list", "data", "count", "total", "has_more"} and take limit (default 100, at most 1000) and offset, newest first:
offset = 0
while True:
page = postilon.emails.list(status="bounced", limit=100, offset=offset)
for email in page["data"]:
print(email["id"], email["to"])
if not page["has_more"]:
break
offset += page["count"]
Verify webhooks
Each webhook request carries X-Postilon-Signature: t=<unix>,v1=<hex>. Verify it against the raw body, before any JSON parsing, with the endpoint's signing secret (whsec_…, returned when the endpoint is created). Signatures older than 5 minutes are refused; change that with tolerance_seconds=.
import json
import os
from flask import Flask, request
from postilon import verify_webhook_signature
app = Flask(__name__)
@app.post("/webhooks/postilon")
def postilon_webhook():
body = request.get_data() # the raw bytes
if not verify_webhook_signature(body, request.headers.get("X-Postilon-Signature"), os.environ["POSTILON_WEBHOOK_SECRET"]):
return "", 400
event = json.loads(body)
if event["type"] == "email.bounced":
... # event["data"]["recipient"] bounced
return "", 200
postilon.webhooks.verify_signature(...) is the same function. Answer with a 2xx within 8 seconds and do slow work afterwards. Events can arrive more than once and out of order, so use the event id to ignore duplicates. See Webhooks.
Receive email
Turn on inbound for a verified domain (or a subdomain of it), and add the MX record it returns:
domain = postilon.domains.update("dom_…", {"inbound_domain": "reply.acme.com"})
# domain["inbound_record"]: {"type": "MX", "name": "reply.acme.com", "value": "inbound.postilon.com", "priority": 10, …}
Each message then reaches your webhooks as email.received, with its text and HTML (up to 256 KB), attachment list, SPF, DKIM and DMARC results and a spam flag. Read received email with postilon.inbound.list, get, attachment(id, index) and raw(id); the last two return bytes (email.message_from_bytes parses the raw message). See Inbound email.
License
MIT, see LICENSE.
Metadata
Release files for postilon 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| postilon-1.0.0.tar.gz | 14.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| postilon-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.3 kB
Release files / postilon-1.0.0.tar.gz
| Download URL | postilon-1.0.0.tar.gz |
|---|---|
| Size | 14.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
874d31017fae4935288fea9c55bcdcd863fe2eee70b45fe4a06a555a34817b7a
|
|
BLAKE2b-256 checksum How to use checksums |
9f9c63e30b41aa8b0a60e0878f86f5f887016bc8305011d30a2a97ea3ccca0c5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / postilon-1.0.0-py3-none-any.whl
| Download URL | postilon-1.0.0-py3-none-any.whl |
|---|---|
| Size | 13.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9441f7279ea65080ad09674b96bd93cf829168098eed4e4e7f33dfdd7bb0fa4b
|
|
BLAKE2b-256 checksum How to use checksums |
19f79578467a2f84353968b3529dc88e2098d1743d9f646b3ae4e742871ef7ba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Security for postilon 1.0.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.
To learn more about protecting yourself from malicious actors, visit our package security documentation.