Python integration: send transactional email
Updated
An official SDK is available: inboxili on PyPI. The sections below also show a plain requests client, if you prefer your own.
The API is one endpoint, so a Python integration is a small module with a timeout, exceptions that carry the API's error code, and careful retry behaviour.
Use the official package
pip install inboxili
import os
from inboxili import Inboxili
client = Inboxili(os.environ["INBOXILI_API_KEY"])
result = client.emails.send(
to="ada@example.com",
from_email="hello@yourdomain.com",
subject="Welcome, {{first_name}}",
html_body="<p>Hi {{first_name}}, your account is ready.</p>",
template_data={"first_name": "Ada"},
)
print(result.status, result.message_id)
The package has no dependencies, supports Python 3.9 and later, raises InboxiliError and InboxiliConnectionError, retries HTTP 429 only, and includes verify_webhook_signature. Source: github.com/inboxili/inboxili-python. Package: inboxili on PyPI.
Or write your own client
Install and configure
pip install requests
export INBOXILI_API_KEY="ik_live_..."
The client
Save as inboxili.py:
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Any
import requests
class InboxiliError(Exception):
def __init__(self, status: int, code: str, message: str):
super().__init__(message)
self.status = status
self.code = code
@dataclass
class SendResult:
status: str
message_id: str | None
class Inboxili:
def __init__(self, api_key: str | None = None, base_url: str = "https://api.inboxili.com/api/v1", timeout: float = 10.0):
self.api_key = api_key or os.environ["INBOXILI_API_KEY"]
self.base_url = base_url
self.timeout = timeout
self._session = requests.Session()
self._session.headers["Authorization"] = f"Bearer {self.api_key}"
def send(
self,
*,
to: str,
from_email: str,
from_name: str | None = None,
subject: str | None = None,
html_body: str | None = None,
text_body: str | None = None,
template_id: str | None = None,
template_data: dict[str, Any] | None = None,
) -> SendResult:
payload = {
"to": to,
"from_email": from_email,
"from_name": from_name,
"subject": subject,
"html_body": html_body,
"text_body": text_body,
"template_id": template_id,
"template_data": template_data or {},
}
payload = {k: v for k, v in payload.items() if v is not None}
resp = self._session.post(f"{self.base_url}/transactional/send", json=payload, timeout=self.timeout)
try:
body = resp.json()
except ValueError:
body = {}
if not resp.ok:
err = body.get("error", {})
raise InboxiliError(resp.status_code, err.get("code", "http_error"), err.get("message", resp.reason))
return SendResult(status=body["status"], message_id=body.get("message_id"))
Send an email
from inboxili import Inboxili, InboxiliError
client = Inboxili()
try:
result = client.send(
to="ada@example.com",
from_email="hello@yourdomain.com",
from_name="Acme",
subject="Welcome, {{first_name}}",
html_body="<p>Hi {{first_name}}, your account is ready.</p>",
template_data={"first_name": "Ada"},
)
print("sent", result.message_id)
except InboxiliError as e:
if e.code == "sender_not_verified":
print("Verify the sending domain first:", e)
else:
raise
Send from a template
client.send(
to="ada@example.com",
from_email="hello@yourdomain.com",
template_id="00000000-0000-0000-0000-000000000000",
template_data={"first_name": "Ada"},
)
Error handling
| e.status | e.code | Do |
|---|---|---|
| 401 | unauthorized | Check the key. Do not retry. |
| 403 | forbidden | Check scope and IP rules. |
| 422 | validation_error, sender_not_verified, send_failed | Fix the request, or inspect the message. |
| 429 | rate_limited | Back off and retry. |
requests can also raise requests.Timeout and requests.ConnectionError. Treat those as "outcome unknown". The API has no idempotency key, so a timed-out send may still have gone out.
import random, time
def send_with_backoff(client, attempts=4, **message):
for i in range(attempts):
try:
return client.send(**message)
except InboxiliError as e:
if e.status != 429 or i == attempts - 1:
raise
time.sleep(2**i + random.random() * 0.25)
Testing
from unittest.mock import patch, MagicMock
import pytest
from inboxili import Inboxili, InboxiliError
def fake_response(status, body):
r = MagicMock(ok=status < 400, status_code=status, reason="x")
r.json.return_value = body
return r
def test_maps_error():
c = Inboxili(api_key="ik_live_test")
with patch.object(c._session, "post", return_value=fake_response(422, {"error": {"code": "sender_not_verified", "message": "no"}})):
with pytest.raises(InboxiliError) as exc:
c.send(to="a@b.co", from_email="x@y.co", subject="s", html_body="<p>x</p>")
assert exc.value.code == "sender_not_verified"
Production notes
- Load the key from the environment or a secret manager.
- Send from a task queue (Celery, RQ, Dramatiq), not inside a request handler.
- Use one key per service.
- Verify webhooks. The
X-Inboxili-Signatureheader is the hex HMAC-SHA256 of the raw body:
import hashlib, hmac
def verify_webhook(raw_body: bytes, signature_hex: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_hex)
Frequently asked questions
- Which library does the client need?
- Only requests. Install it with pip install requests.
- Is there an async version?
- The same request works with httpx.AsyncClient. Swap the session and await the call.
Build with Inboxili
Create a workspace, verify a domain, and make your first API call.