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
In our views.py file, we’ll first get the POST data we need.
views.py
def brevo_handler(request):
# Extracting the posted data from the request
= request.POST.get("email")
email = request.POST.get("last_name")
last_name = request.POST.get("first_name")
first_name = request.POST.get("captcha") captcha_code
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).
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.
views.py
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:
"listIds"] = [int(BREVO_LIST),]
obj[= {
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.
views.py
def brevo_handler(request):
#...
= requests.post(BREVO_URL, json=obj, headers=headers)
response
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.
urls.py
from django.urls import path
from . import views
= [
urlpatterns "create_contact", views.brevo_handler, name="brevo_handler"),
path( ]
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 accountduplicate_parameter
: happens when one contact is added twice, this should raise an error.
views.py
def brevo_handler(request):
#...
if response.status_code == 201: # Success
return render(request, "_success_newsletter.html")
else: # Handle Brevo API errors
= json.loads(response.text)["code"]
err_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
= request.POST.get("email")
email = request.POST.get("last_name")
last_name = request.POST.get("first_name")
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:
"listIds"] = [int(BREVO_LIST),]
obj[= {
headers "api-key": BREVO_API_KEY,
"accept": "application/json",
"content-type": "application/json",
}= requests.post(BREVO_URL, json=obj, headers=headers)
response
if response.status_code == 201: # Success
return render(request, "_success_newsletter.html")
else: # Handle Brevo API errors
= json.loads(response.text)["code"]
err_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.