Every account now comes with a free mailbox — warmed automatically, no domain to buy. See how it works
Django's built-in email backends speak SMTP, and Warmerly's transactional API is REST only. A custom email backend closes the gap: it is a short class, and once it is configured every send_mail call and EmailMessage in your project goes through Warmerly with no other code changes.
Four steps, the same in every framework. Warmerly is REST only today: no SMTP relay, webhooks, templates or attachments yet.
In Warmerly open Transactional email, add the domain you send from (for example mail.example.com), publish the DNS records it shows at your DNS provider, then verify. Use a domain or subdomain you do not use for cold outreach.
Under Transactional email, API keys, create a key with the Sending permission, optionally limited to that domain. It starts with wm_tx_ and is shown once, so copy it straight into your secrets store.
Read it in settings.py from the environment (os.environ["WARMERLY_TX_KEY"]) and keep it out of version control.
A successful request answers 202 with the email's id and a status of sent (or queued, which Warmerly retries for up to 24 hours). The free plan is 5,000 emails a month with no card.
Copy these into your project. The request is the same one the API reference documents; nothing is hidden behind a package.
# myproject/warmerly_backend.py
import uuid
import requests
from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend
ENDPOINT = "https://app.warmerly.com/api/v1/transactional/emails"
class WarmerlyEmailBackend(BaseEmailBackend):
"""Send Django's EmailMessage objects through Warmerly's REST API."""
def send_messages(self, email_messages):
sent = 0
for message in email_messages:
try:
self._send(message)
sent += 1
except Exception:
if not self.fail_silently:
raise
return sent
def _send(self, message):
if message.attachments:
raise ValueError("Warmerly's transactional API does not accept attachments yet")
html = next(
(content for content, mimetype in getattr(message, "alternatives", []) if mimetype == "text/html"),
None,
)
payload = {"from": message.from_email, "to": list(message.to), "subject": message.subject}
if message.cc:
payload["cc"] = list(message.cc)
if message.bcc:
payload["bcc"] = list(message.bcc)
if message.reply_to:
payload["reply_to"] = message.reply_to[0]
if html:
payload["html"], payload["text"] = html, message.body
elif message.content_subtype == "html":
payload["html"] = message.body
else:
payload["text"] = message.body
response = requests.post(
ENDPOINT,
json=payload,
headers={
"Authorization": f"Bearer {settings.WARMERLY_TX_KEY}",
# One key per message. If you retry a message yourself, reuse its key.
"Idempotency-Key": str(uuid.uuid4()),
},
timeout=15,
)
if response.status_code != 202:
error = (response.json().get("error") or {}) if response.content else {}
raise RuntimeError(f"Warmerly {response.status_code} {error.get('code', 'unknown')}: {error.get('message', '')}")
# settings.py
import os
EMAIL_BACKEND = "myproject.warmerly_backend.WarmerlyEmailBackend"
WARMERLY_TX_KEY = os.environ["WARMERLY_TX_KEY"]
DEFAULT_FROM_EMAIL = "Acme <noreply@mail.example.com>"
from django.core.mail import send_mail
send_mail(
subject="Reset your password",
message="Reset your password: https://example.com/reset/abc", # the plain-text part
from_email=None, # uses DEFAULT_FROM_EMAIL
recipient_list=["jane@example.org"],
html_message='<p>Click <a href="https://example.com/reset/abc">here</a> to reset your password.</p>',
)
Every refusal has a code in the response body, at error.code. These are the ones to handle, and whether to retry.
| Status | Code | Meaning | Retry? |
|---|---|---|---|
| 400 | bad_request | Invalid body, or the From address is not on a verified sending domain. | No: fix the request |
| 403 | forbidden | The key is limited to a different sending domain. | No |
| 403 | sending_paused | Sending is paused for the workspace (bounce or complaint rate, or under review). | No: contact support |
| 422 | content_rejected | The message was refused by abuse screening. | No, not unchanged |
| 429 | rate_limited | The hourly or daily sending limit was reached. | Later |
| 429 | quota_exceeded | The plan's monthly allowance, plus the free buffer, is used up. | No: upgrade or wait |
| 502 | send_failed | It could not be sent or queued. details.interrupted says whether it may have been delivered. | Same key if interrupted is false |
| 503 | service_unavailable | Transactional sending is not enabled for the workspace yet. | No |
Full details, including the sending limits, are in the transactional email API reference.
No: Warmerly has no SMTP relay today. Use a custom backend like the one above, which sends each message over the REST API.
Yes. The backend implements Django's email backend interface, so send_mail, EmailMessage and EmailMultiAlternatives work unchanged, apart from attachments.
No. Warmerly's transactional API is one HTTP request, so there is nothing to install. The code on this page is the whole integration.
Not yet. The send request takes from, to, cc, bcc, reply_to, subject, html or text, headers and tags. Attachments, SMTP relay, delivery webhooks and templates are not available today.
If nothing was handed over, the email is queued rather than dropped. The request still answers 202 with a status of queued, and Warmerly retries it after 1, 5, 15 and 60 minutes, then hourly, for up to 24 hours.
Create a free account, verify a sending domain and send a test. 5,000 emails a month free, no card.