Bytloop MailDocs
Status

Getting started

  • Introduction
  • Quickstart

Sending email

  • Send a transactional email
  • Sending domains vs mailboxes

Framework guides

  • Send email from Next.js
  • Send email from Ruby on Rails
  • Send email from Django
  • Send email from Laravel

Domains

  • Verify a domain

Receiving email

  • Receive email
  • Webhooks

Reference

  • Rate limits
  • Errors
  • SDKs

Framework guides

Send email from Django

Send transactional email from Django with the requests library — as a service function and as a Celery task.

Try it live

Every endpoint in this doc runs against your workspace from the interactive API explorer. Prefill a request, hit Send, and see the response with your own keys.

This is the same POST /api/v1/emails endpoint documented in Send a transactional email, wired into a Django project as a plain service function and, for anything triggered by a request, a Celery task.

Service function#

# core/bytloop_mail.py
import requests
from django.conf import settings


class BytloopMailError(Exception):
    pass


def send_email(*, from_addr, to, subject, html, idempotency_key, tags=None):
    response = requests.post(
        "https://mail.bytloop.com/api/v1/emails",
        headers={
            "Authorization": f"Bearer {settings.BYTLOOP_API_KEY}",
            "Content-Type": "application/json",
            "Idempotency-Key": idempotency_key,
        },
        json={
            "from": from_addr,
            "to": to if isinstance(to, list) else [to],
            "subject": subject,
            "html": html,
            "tags": tags or {},
        },
        timeout=15,
    )
    if not response.ok:
        raise BytloopMailError(
            f"Bytloop Mail send failed ({response.status_code}): {response.text}"
        )
    return response.json()
# core/bytloop_mail.py
import requests
from django.conf import settings


class BytloopMailError(Exception):
    pass


def send_email(*, from_addr, to, subject, html, idempotency_key, tags=None):
    response = requests.post(
        "https://mail.bytloop.com/api/v1/emails",
        headers={
            "Authorization": f"Bearer {settings.BYTLOOP_API_KEY}",
            "Content-Type": "application/json",
            "Idempotency-Key": idempotency_key,
        },
        json={
            "from": from_addr,
            "to": to if isinstance(to, list) else [to],
            "subject": subject,
            "html": html,
            "tags": tags or {},
        },
        timeout=15,
    )
    if not response.ok:
        raise BytloopMailError(
            f"Bytloop Mail send failed ({response.status_code}): {response.text}"
        )
    return response.json()
# settings.py
import os

BYTLOOP_API_KEY = os.environ["BYTLOOP_API_KEY"]
# settings.py
import os

BYTLOOP_API_KEY = os.environ["BYTLOOP_API_KEY"]

Call it directly for a one-off send, e.g. from a view after an order is placed:

from core.bytloop_mail import send_email

send_email(
    from_addr="Support <support@send.acme.com>",
    to=order.customer_email,
    subject=f"Your receipt for order #{order.id}",
    html=render_to_string("receipts/email.html", {"order": order}),
    idempotency_key=f"receipt-{order.id}",
    tags={"receipt_for_order": str(order.id)},
)
from core.bytloop_mail import send_email

send_email(
    from_addr="Support <support@send.acme.com>",
    to=order.customer_email,
    subject=f"Your receipt for order #{order.id}",
    html=render_to_string("receipts/email.html", {"order": order}),
    idempotency_key=f"receipt-{order.id}",
    tags={"receipt_for_order": str(order.id)},
)

Celery task (recommended)#

Calling requests.post synchronously inside a view blocks the response on the API round-trip. If the project already runs Celery (common for anything with background work), wrap the send in a task so the view returns immediately and a transient failure retries automatically:

# core/tasks.py
from celery import shared_task
from django.template.loader import render_to_string

from core.bytloop_mail import BytloopMailError, send_email


@shared_task(
    bind=True,
    max_retries=5,
    default_retry_delay=30,
)
def send_receipt_email(self, order_id):
    from orders.models import Order  # local import avoids a circular import at module load

    order = Order.objects.get(pk=order_id)
    try:
        send_email(
            from_addr="Support <support@send.acme.com>",
            to=order.customer_email,
            subject=f"Your receipt for order #{order.id}",
            html=render_to_string("receipts/email.html", {"order": order}),
            idempotency_key=f"receipt-{order.id}",
            tags={"receipt_for_order": str(order.id)},
        )
    except BytloopMailError as exc:
        raise self.retry(exc=exc)
# core/tasks.py
from celery import shared_task
from django.template.loader import render_to_string

from core.bytloop_mail import BytloopMailError, send_email


@shared_task(
    bind=True,
    max_retries=5,
    default_retry_delay=30,
)
def send_receipt_email(self, order_id):
    from orders.models import Order  # local import avoids a circular import at module load

    order = Order.objects.get(pk=order_id)
    try:
        send_email(
            from_addr="Support <support@send.acme.com>",
            to=order.customer_email,
            subject=f"Your receipt for order #{order.id}",
            html=render_to_string("receipts/email.html", {"order": order}),
            idempotency_key=f"receipt-{order.id}",
            tags={"receipt_for_order": str(order.id)},
        )
    except BytloopMailError as exc:
        raise self.retry(exc=exc)
# wherever the order is confirmed
from core.tasks import send_receipt_email

send_receipt_email.delay(order.id)
# wherever the order is confirmed
from core.tasks import send_receipt_email

send_receipt_email.delay(order.id)

Common pitfalls#

  • Calling send_email synchronously from a view. Blocks the request on the API round-trip for no reason the user can see. Use the Celery task above for anything triggered by a request.
  • No max_retries / no self.retry. Without it, a transient network error or a 5xx during a deploy silently drops the email instead of trying again a few seconds later.
  • Reading BYTLOOP_API_KEY with os.environ inside the task instead of settings. Keeps the key resolution in one place (settings.py), so a missing env var fails fast at startup instead of inside a background worker where it's easy to miss.

Next#

  • Register a webhook endpoint (a Django view) to hear about delivery, bounces, and opens.
  • Read the rate limits page so a bulk send (e.g. a management command iterating Order.objects.filter(...)) doesn't get throttled mid-run.
PreviousSend email from Ruby on RailsNextSend email from Laravel

On this page

  • Service function
  • Celery task (recommended)
  • Common pitfalls
  • Next