A simpler newsletter form : Creating new contacts with Brevo.

— OR —

“A Guide to Using Their API with Django”

Wrestling with an outdated documentation

Brevo is an easy-to-use newsletter service that offers a lot of great features. There’s a free plan, pre-made templates, you can use SMS, Mail… and – more importantly – they have a nice API. But there’s a dark spot on the map. Brevo hide something. Brevo wasn’t always Brevo. Brevo swept its infancy under a rug… you see, before 2023, Brevo was… SendInBlue. Ok, jokes aside, it means that their otherwise impressive API documentation refers to an outdated SDK.

For a project, I had to implement a newsletter form so that users could join a mailing list. To better integrate it to the website, we wanted to use a custom form. Simple enough. Except the contacts’ documentation refer to a SendInBlue python SDK, and the package is complicated. I wanted to reverse engineer the SDK but it is huge, it isn’t easy to make your way through the code. Lazily, I decided to use the requests library to interact with the Brevo API directly. This tutorial describes the process.

A fake problem because of an inconsistency

While writing this I realized that the github documentation of the Brevo package, linked on the new SDK page, is up-to-date. As I said, that is not the case of the API documentation of their website, which refers to a SendInBlue GitHub profile. Nevermind, let’s just say this article might help people that were as lost as me find their way between both !

What to expect in this series

This first part will only focus on making your API with. The next one will describe the front end implementation with HTMX, as a bonus. Finally I will write an article on how you can add a custom captcha to the form (or any form, for that matter).

Adding a contact to a Brevo list

File architecture in our Django folders In our views.py file, we’ll first get the POST data we need.

def brevo_handler(request):
    # Extracting the posted data from the request
    email = request.POST.get("email")
    last_name = request.POST.get("last_name")
    first_name = request.POST.get("first_name")
    captcha_code = request.POST.get("captcha")

We then use the data we got to construct our request. email is a basic field, but all the others needs to be in an attributes dict. All optional field needs to be written in full caps.

You can send your contacts as such, but you usually want them in a list, to have a better control on who you send your newsletter to. As such, you need to add a field to the dict containing a list of int (you can send the contact to multiple lists as one). The list Id can be found on the main Brevo website after a #. Sorry for the french interface, here the listId is 2 (which seems to be the default starting Id). Sorry for the french. Here the listId is 2

To send contacts to Brevo, they need to make sure we are the right person, otherwise anyone could spam our contact list. To do that, it gives use a secret key, called the API key. It’s super easy to get, and Brevo explains it well. We also need to send the dict as json, and tell it so.

def brevo_handler(request):
    #...
    # Constructing the Brevo API request payload with the posted data
    obj = {
        "email": email,
        "attributes": {
            "LNAME": last_name,
            "FNAME": first_name,
        },
    }
    if BREVO_LIST is not None:
        obj["listIds"] = [int(BREVO_LIST),]
    headers = {
        "api-key": BREVO_API_KEY,
        "accept": "application/json",
        "content-type": "application/json",
    }

The payload can now be sent in a POST request to Brevo.

def brevo_handler(request):
    #...
    response = requests.post(BREVO_URL, json=obj, headers=headers)

    if response.status_code == 201:  # Success
        return HttpResponse("Created contact")
    else:
        return HttpResponseBadRequest(f"Erreur d'inscription. Veuillez réessayer.")

To serve the API, it need to be linked to an url in the urls.py file of our app.

from django.urls import path
from . import views

urlpatterns = [
    path("create_contact", views.brevo_handler, name="brevo_handler"),
]

You should have a working API. Sending the POST request will create a new contact in your contact list.

Handling errors

We might want a bit more granularity in the way we deal with errors. Brevo returns a code in the response_text explaining the reason the request couldn’t go through. Here’s a few listed in the doc :

  • invalid_parameter, missing_parameter, permission_denied, unauthorized, account_under_validation, not_acceptable bad_request, unprocessable_entity : Should all be avoided with a proper forms and a properly set up account
  • duplicate_parameter: happens when one contact is added twice, this should raise an error.
def brevo_handler(request):
    #...
    if response.status_code == 201:  # Success
        return render(request, "_success_newsletter.html")
    else:  # Handle Brevo API errors
        err_code = json.loads(response.text)["code"]
        if err_code == "duplicate_parameter":
            return HttpResponseBadRequest(
                "Error : this contact is already in the database."
            )
        return HttpResponseBadRequest(
            f"Inscription error {response.status_code}. {response.text}"
        )

Full code

And here’s the full code to copy paste without thinking 😛 :

def brevo_handler(request):
    # Extracting the posted data from the request
    email = request.POST.get("email")
    last_name = request.POST.get("last_name")
    first_name = request.POST.get("first_name")

    # Constructing the Brevo API request payload with the posted data
    obj = {
        "email": email,
        "attributes": {
            "NOM": last_name,
            "PRENOM": first_name,
        },
    }
    if BREVO_LIST is not None:
        obj["listIds"] = [int(BREVO_LIST),]
    headers = {
        "api-key": BREVO_API_KEY,
        "accept": "application/json",
        "content-type": "application/json",
    }
    response = requests.post(BREVO_URL, json=obj, headers=headers)

    if response.status_code == 201:  # Success
        return render(request, "_success_newsletter.html")
    else:  # Handle Brevo API errors
        err_code = json.loads(response.text)["code"]
        if err_code == "duplicate_parameter":
            return HttpResponseBadRequest(
                "Error : this contact is already in the database."
            )
        return HttpResponseBadRequest(
            f"Inscription error {response.status_code}. {response.text}"
        )

Conclusion

Here you go, simple indeed. Sometimes it’s easier to not use pre-made solutions. Next time we’ll make a dynamic form to uses our API.