UniOne Django
Django integration of the UniOne Python SDK (``): the API client in the settings (get_client()), a webhook endpoint with signature check, a Django email transport that sends through `email/send`.
API reference · Docs for LLMs: llms.txt
Install
pip install unione-django
Requirements: Python 3.8+, Django 4.2+.
Configuration
Get an API key in the control panel and keep it in the environment, never in code:
export UNIONE_API_KEY="your-api-key"
Settings are read from the environment by default:
# settings.py
INSTALLED_APPS = [..., "unione_wrapper"]
UNIONE = {
"API_KEY": os.environ["UNIONE_API_KEY"],
"HOST": "us1.unione.io", # optional
}
MAILERS = {"default": {"BACKEND": "unione_wrapper.mail.EmailBackend"}} # Django 6.1+
# EMAIL_BACKEND = "unione_wrapper.mail.EmailBackend" # Django < 6.1
DEFAULT_FROM_EMAIL = "Shop <shop@example.com>"
# urls.py
urlpatterns = [..., path("unione/", include("unione_wrapper.urls"))] # /unione/webhook
us1.unione.io in the example is for an account on another datacenter; without the host setting the package uses api.unione.io.
| Option | Environment variable | Default | Description |
|---|---|---|---|
UNIONE["API_KEY"] |
UNIONE_API_KEY |
User or project API key (required). | |
UNIONE["HOST"] |
UNIONE_HOST |
api.unione.io |
API host (datacenter: api.unione.io, eu1.unione.io, us1.unione.io) or a full base URL. |
UNIONE["TIMEOUT"] |
UNIONE_TIMEOUT |
30 |
Request timeout, seconds. |
UNIONE["WEBHOOK_KEY"] |
UNIONE_WEBHOOK_KEY |
API key the webhooks are signed with (the key of the user/project of the webhook), if it differs from the API key. |
Settings of the UNIONE dict win over the environment variables. The webhook URL is set by include("unione_wrapper.urls"); the view is csrf_exempt.
Sending mail
Messages of the Django email go through email/send: from, to/cc/bcc, reply-to, subject, HTML and text parts, attachments, inline images and X-* headers.
from django.core.mail import EmailMultiAlternatives, send_mail
send_mail("Order shipped", "Your order is on its way", None, ["user@example.com"])
message = EmailMultiAlternatives("Order shipped", "Text", to=["User <user@example.com>"], headers={"X-Order": "42"})
message.attach_alternative("<b>HTML</b>", "text/html")
message.tags = ["orders"] # -> tags
message.metadata = {"order_id": "42"} # -> global_metadata
message.send()
print(message.job_id)
No verified domain yet? Send from the test sandbox domain.
Webhooks
The endpoint answers GET with 200 OK (the URL check of webhook/set) and accepts the POST notifications: it checks the auth field (MD5 of the request body in which the auth value is replaced by the API key), answers 401 to a wrong signature, and passes the notification and each of its events to the application.
from django.dispatch import receiver
from unione_wrapper.signals import webhook_event_received
@receiver(webhook_event_received)
def on_event(sender, event, user, **kwargs):
data = event.event_data
print(event.event_name, data.email, data.status)
Register the URL of the endpoint with webhook/set, e.g. https://example.com/unione/webhook.
Answer 200 within 3 seconds and process the events in the background (a queue): otherwise UniOne repeats the request every 10 minutes for 24 hours (
retry_countgrows), and a handler that failed every request for at least 10 events of a day is turned inactive. The handler must take at least 5 parallel requests of up to 100 events each; deduplicate, a repeat delivers the same events again.
API client
Every section of the API is available on the client, see the SDK docs.
from unione_wrapper import get_client
info = get_client().system.info()
print(info.accounting)
Sending through the API
Besides the Django email, mail can be sent through the API client directly. The client calls email/send with all of its options: templates, substitutions, metadata, tracking. Fields of the request and other examples: Python SDK docs.
from django.http import JsonResponse
from unione.api.email import EmailSendRequest, MessageObject, MessageObjectRecipientsItem as Recipient
from unione.common import BodyObject
from unione_wrapper import get_client
def send_email(request):
client = get_client()
response = client.email.send(EmailSendRequest(
message=MessageObject(
recipients=[Recipient.from_address("Alex <alex@example.com>")],
from_email="shop@example.com",
from_name="Shop",
subject="Your order",
body=BodyObject.from_html("<p>Hello, {{to_name}}! Your order has shipped.</p>"),
),
))
return JsonResponse({"job_id": response.job_id})
No domain yet: the test sandbox domain
Until your domain is verified, send from the sandbox domain: a test domain every account has, already verified, with DKIM and SPF set. It delivers only to addresses you confirm (the confirmation link comes by email, valid 48 hours), up to 100 emails a day. The sandbox domain is deleted 31 days after it was created, but you can get it again: the next domain/get-sandbox-domain call creates a new one (with a new name, so read it from the call, do not hardcode it).
The examples use the API client of the package (see the configuration above).
from django.http import JsonResponse
from unione.api.confirmed_email import ConfirmedEmailRequest
from unione.api.email import EmailSendRequest, MessageObject, MessageObjectRecipientsItem as Recipient
from unione.common import BodyObject
from unione_wrapper import get_client
def send_from_sandbox(request):
client = get_client()
# 1. Your sandbox domain: created on the first call, the same one afterwards
domain = client.domain.get_sandbox_domain().domain
# 2. Confirm your inbox: UniOne sends a link, click it within 48 hours
client.confirmed_email.create(ConfirmedEmailRequest(email="you@example.com"))
# 3. Send from any address on the sandbox domain to the confirmed one
client.email.send(EmailSendRequest(
message=MessageObject(
recipients=[Recipient.from_address("you@example.com")],
from_email=f"noreply@{domain}",
subject="Hello from UniOne",
body=BodyObject.from_html("<p>It works!</p>"),
),
))
return JsonResponse({"domain": domain})
Sandbox limits
- Sends only to confirmed addresses: up to 10 per account.
- Up to 100 emails a day.
- The sandbox domain expires after 31 days; call
domain/get-sandbox-domainagain to get a new one. - Works only with the account API key, not a project key.
- Not listed in
domain/list; the otherdomain/*methods do not apply to it.
The sandbox is for development and testing. Do not use a sandbox domain as the sender in production.
The same with curl
BASE=https://api.unione.io/en/transactional/api/v1
curl -s -X POST $BASE/domain/get-sandbox-domain.json \
-H "Content-Type: application/json" -H "X-API-KEY: $UNIONE_API_KEY" -d '{}'
curl -s -X POST $BASE/confirmed-email/create.json \
-H "Content-Type: application/json" -H "X-API-KEY: $UNIONE_API_KEY" \
-d '{"email": "you@example.com"}'
curl -s -X POST $BASE/email/send.json \
-H "Content-Type: application/json" -H "X-API-KEY: $UNIONE_API_KEY" \
-d '{
"message": {
"recipients": [
{
"email": "you@example.com"
}
],
"from_email": "noreply@<sandbox-domain>",
"subject": "Hello from UniOne",
"body": {
"html": "<p>It works!</p>"
}
}
}'
Going to production
1. Verify your sending domain. domain/get-dns-records returns the verification record, the DKIM key and the DMARC target; add the records at your DNS provider, then domain/validate-verification-record and domain/validate-dkim.
| Type | Host | Value | Where it comes from |
|---|---|---|---|
| TXT | @ |
unione-validate-hash=… |
verification-record of the response, as is |
| TXT | <selector>._domainkey (the selector is on the domain page of the control panel, e.g. us) |
v=DKIM1; k=rsa; p=<dkim> |
dkim of the response after v=DKIM1; k=rsa; p= |
| TXT | @ |
v=spf1 include:spf.unione.io ~all |
not in the response; add the include to your SPF record if you have one |
| CNAME | _dmarc |
<domain>.dmarc.… |
dmarc of the response (UniOne publishes the DMARC policy); keep your own DMARC TXT record instead if the domain has one |
2. Make sending reliable.
| Retries | 429 and 5xx with backoff and the same idempotence_key; fix other errors instead of retrying |
| Limits | 500 recipients and 10 MB per request, attachments included: base64 makes a file about a third larger |
| Body | always html or plaintext (a template can supply it) |
| Delivery status | a webhook, not polling |
| Queue | send from a background job, not inside the web request |
| Config | from_email from config, so the sandbox → production switch needs no code change |
| Defaults | keep the unsubscribe footer and tracking; changing them needs UniOne support |
Help
- Integration guides
- Python SDK
- AI coding agents: llms.txt, the MCP server, skills:
npx skills add https://github.com/unione-repo/unione-skills.git - Support · Issues
- Contributing: AGENTS.md — the package is generated from the OpenAPI spec.
Metadata
Release files for unione-django 1.2.3
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 | Actions |
|---|---|---|---|---|
| unione_django-1.2.3.tar.gz | 11.7 kB | With provenance | Details for unione_django-1.2.3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| unione_django-1.2.3-py3-none-any.whl | Python 3 | none | any | Details for unione_django-1.2.3-py3-none-any.whl |
Total release size: 25.7 kB
Release files / unione_django-1.2.3.tar.gz
| Download URL | unione_django-1.2.3.tar.gz |
|---|---|
| Size | 11.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
15c6f91c658c22da533a1eee1e81d96c504dee41832e6b8de7f2baedec44a22d
|
|
BLAKE2b-256 checksum How to use checksums |
4cc7cf9d552a6f94d24ae18685d21eccccee40196ae23be5ddf0737284ab38e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logRelease files / unione_django-1.2.3-py3-none-any.whl
| Download URL | unione_django-1.2.3-py3-none-any.whl |
|---|---|
| Size | 14.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
db1a6bb01edff39d292e00ea20a789db4706a70745d3f0f236c109c2100f08c2
|
|
BLAKE2b-256 checksum How to use checksums |
47a8d1e5cd9e02d40b6bce99d199389cc5e17ce04334012f994dbb6e3def6ead
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logSecurity for unione-django 1.2.3
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for unione_django-1.2.3.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logA 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.