coss-django
A Django email backend that sends your app's email through the coss.com API.
Switch by changing settings, not call sites: send_mail, EmailMessage, mail_admins, password
reset emails and every other Django email keep working.
Docs: coss.com/docs/integrations/django
Django 5.2–6.1, Python 3.10+. No dependencies besides Django.
Install
pip install coss-django # or: uv add coss-django
Settings
# settings.py
EMAIL_BACKEND = "coss_django.EmailBackend"
COSS_API_KEY = env("COSS_API_KEY") # coss_live_… or coss_test_…
DEFAULT_FROM_EMAIL = "Acme <hello@acme.com>" # a verified domain of yours
On Django 6.1, which configures email with MAILERS, put the same options in OPTIONS:
MAILERS = {
"default": {
"BACKEND": "coss_django.EmailBackend",
"OPTIONS": {"api_key": env("COSS_API_KEY")},
},
}
| Setting | Backend option | Default |
|---|---|---|
COSS_API_KEY |
api_key |
the COSS_API_KEY environment variable |
COSS_BASE_URL |
base_url |
the COSS_BASE_URL environment variable, else https://api.coss.com |
COSS_TIMEOUT |
timeout |
30 (seconds, per attempt) |
COSS_MAX_RETRIES |
max_retries |
2 |
Options passed to the backend (get_connection("coss_django.EmailBackend", api_key=…) or
MAILERS OPTIONS) win over the settings. A missing API key fails the first send with
ImproperlyConfigured, never your app's startup.
Usage
Nothing changes:
from django.core.mail import EmailMultiAlternatives, send_mail
send_mail("Welcome", "Thanks for signing up!", None, ["ada@example.com"])
message = EmailMultiAlternatives(
subject="Your receipt",
body="Thanks for your order!",
to=["Ada Lovelace <ada@example.com>"],
reply_to=["support@acme.com"],
headers={"List-Unsubscribe": "<https://acme.com/unsubscribe>"},
)
message.attach_alternative("<p>Thanks for your order!</p>", "text/html")
message.attach_file("receipt.pdf")
message.send()
message.coss_email["id"] # "email_…": the email in coss.com (API, dashboard)
Each message is one request (POST /v1/emails). After a successful send, message.coss_email
holds the email as the API returned it. send_messages() returns the number of messages
coss.com accepted; messages without recipients are skipped, as Django's SMTP backend does.
Inline images are attachments with a Content-ID (<img src="cid:logo@acme"> in the HTML):
from email.message import MIMEPart # Django 6.0+; on 5.2 use email.mime.image.MIMEImage
logo = MIMEPart()
logo.set_content(
png_bytes,
maintype="image",
subtype="png",
disposition="inline",
filename="logo.png",
cid="<logo@acme>",
)
message.attach(logo)
coss.com options
Set coss on a message:
from datetime import datetime, timedelta, timezone
message.coss = {
"category": "receipts", # suppressions per category
"tags": {"plan": "pro", "order": "42"}, # or [{"name": "plan", "value": "pro"}]
"scheduled_at": datetime.now(timezone.utc) + timedelta(hours=1), # or an ISO 8601 string
"tracking": {"opens": True, "clicks": False}, # and "unsubscribe" (one-click)
"idempotency_key": "order-42", # a retry with the same key never sends twice
}
message.send()
Unknown keys and invalid values raise ValueError. A datetime is sent in UTC
(YYYY-MM-DDTHH:MM:SSZ, fractions dropped); a naive one is taken to be in Django's current time
zone (TIME_ZONE, unless you activated another one). A string is sent as given. Without an
idempotency_key, each send gets a random one, reused by its retries.
Errors
When coss.com refuses an email or can't be reached, the send raises coss_django.CossError:
from coss_django import CossError
try:
message.send()
except CossError as error:
error.code # e.g. "domain_not_verified"
error.type # "invalid_request_error", …, "connection_error", "timeout_error"
error.status # HTTP status, None without a response
error.request_id # "req_…", quote it to support
error.message # the API's message
error.param # the field it is about, when there is one
error.issues # [{"path": "to", "message": "…"}] for validation errors
str(error) reads coss.com could not send the email: <message> (<code>). Network errors,
timeouts, 409, 429 and 5xx responses are retried with backoff (honouring Retry-After up to 60
seconds) before that. With fail_silently=True, these failures and a missing API key are
swallowed and the message isn't counted.
What coss.com can't send raises ValueError before anything is sent, fail_silently or not
(like a bad header with Django's SMTP backend):
- alternatives other than one
text/htmlpart, or acontent_subtypeother thanplain/html; - a
Bccheader, aSenderorReturn-Pathother thanfrom_email, andFrom,ToorCcheaders that name other addresses than the message'sfrom_email,toorcc(Django sends those to the SMTP server separately; coss.com sends one message to the addresses it shows — matching headers are fine, and their display names are used); - a subject with a line break, no
toaddress (onlyccorbcc), a header given twice, and multipart attachments.
A Reply-To header replaces reply_to, as Django documents. Headers coss.com sets itself
(Message-ID, Date, Content-Type, …) are dropped; non-ASCII header values are sent as RFC 2047
encoded-words.
Tests
Django's test runner swaps in the locmem backend, so your tests never call coss.com.
License
MIT
Metadata
Release files for coss-django 0.1.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 | |
|---|---|---|---|---|
| coss_django-0.1.0.tar.gz | 15.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| coss_django-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.8 kB
Release files / coss_django-0.1.0.tar.gz
| Download URL | coss_django-0.1.0.tar.gz |
|---|---|
| Size | 15.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
db41127c0043d7005589ae4dd0f37151a9a29dd0b9359f1190d410b7c1279543
|
|
BLAKE2b-256 checksum How to use checksums |
648c14a942988e1ec3fc43b3c6e59c84e2bf701f1a4d054732b41727ab5c7754
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 16, 2026.
Transparency logRelease files / coss_django-0.1.0-py3-none-any.whl
| Download URL | coss_django-0.1.0-py3-none-any.whl |
|---|---|
| Size | 17.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9b39f226633d466dbc5242559970eb40db70780dd9868664f9d36c7cd062b648
|
|
BLAKE2b-256 checksum How to use checksums |
455c6492a27b191fe835695bc5eb54184e77555082b993ee3ce9fcc03b929eae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 16, 2026.
Transparency logSecurity for coss-django 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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for coss_django-0.1.0.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 16, 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.