Integrate Sendy API with Laravel

In this article we will look at how to integrate the Sendy API with Laravel. In order to follow along you will need a live Sendy installation.

Sendy is a is a self-hosted email newsletter software that allows you send trackable emails via Amazon Simple Email Service (SES) or other SMTP services. You can get Sendy by clicking this link.

In this tutorial we will not spend time on how to set up Sendy, but we have video tutorials on it:

In this example, we will integrate Sendy with Laravel 9. We will have admin options to set API key, list ID etc. And we will have a simple form on the frontend where people will be able to subscribe to our newsletter. The subscribed user’s details will be saved to our Sendy app email list.

Install Laravel

First of all, we need to install Laravel. You can do it via composer:

composer create-project laravel/laravel your_app_name

1. Create Migration and Seeder

You can find all Sendy API related info on this page.

We want to be able to save some Sendy related options in our admin setting, these are:

  • Sendy installation URL
  • API key
  • List id

So we need to create a table for this. But we need to create a model too, so we do this in one step. In the terminal run:

php artisan make:model SendyConfig -m

In the migration file we need to create the columns mentioned above.

Schema::create('sendy_configs', function (Blueprint $table) {
$table->id();
$table->string('install_url');
$table->string('api_key');
$table->string('list_id');
$table->timestamps();
});

Let’s create a seeder file

php artisan make:seeder SendyConfigsTableSeeder

In the seeder file add:

DB::table('sendy_configs')->insert([
'install_url' => 'https://your_sendy_installation.com',
'api_key' => 'your_sendy_api_key',
'list_id' => 'your_sendy_list_id',
]);

Don’t forget to pull in the DB facade

use Illuminate\Support\Facades\DB;

and add

SendyConfigsTableSeeder::class,

to the DatabaseSeeder.php file.

Create the table and seed the database:

php artisan migrate:fresh --seed

2. Creating the view files

I will use Bootstrap 5 to do some basic style. For this I will pull in the Bootstra cdn.

For simplicity’s sake and to save some time, I will not create layouts and multi-user authentication. I will just use the welcome.blade.php file for displaying the subscription form on the front, and I will create a separate blade file to display the admin settings, but this admin view will not be protected. If you do this on your live project, you should obviously protect this page from guests and users.

In the welcome.blade.php file remove everything from the body tags and add this code:

<section id="subscribe" class="mt-3">
<div class="container">
<div class="row">
<div class="col-lg-12 w-50 mx-auto">
<div class="title">
<h3 class="text-center mt-3">Subscribe to our Newsletter!</h3>
<div class="display-errors" style="display:none">
<span id="errors" class='alert alert-danger'></span>
</div>
<div class="display-success" style="display:none">
<span id="successes" class='alert alert-success'></span>
</div>
</div>

<div class="form-container">
<div class="mb-3">
<label for="name" class="form-label">Your name</label>
<input type="text" class="form-control" id="name">
</div>

<div class="mb-3">
<label for="email" class="form-label">Your email address</label>
<input type="email" class="form-control" id="email">
</div>

<div class="submit-btn mb-3">
<button id="submit-btn" onclick="subscribeManager()" class="btn btn-primary col-12">Subscribe</button>
</div>

<div class="form-check">
<input class="form-check-input" type="checkbox" value="" id="flexCheckDefault">
<label class="form-check-label" for="flexCheckDefault">
Accept privacy Policy
</label>
</div>
</div>
</div>
</div>
</div>
</section>

I’ve already added some IDs to the elements since later on we will do some Javascript checks, show error or success messages etc.

Now let’s create the admin blade. In the views folder create a new folder called “admin” and in here create a new file called sendy.blade.php

For now, don’t put anything here. First let’s create a controller for this.

3. Create the admin controller

In terminal create the controller for the admin

php artisan make:controller Admin/SendyController

Open the new controller file. First we will need to import the followings:

use Illuminate\Support\Facades\Validator;
use App\Models\SendyConfig;
use DB;

Then create a method that will just show us the admin page and get the data from the table.

public function sendy()
{
$config = DB::table('sendy_configs')->first();
return view('admin.sendy', compact('config'));
}

Let’s create the routes. Open Routes -> web.php

At the top, import:

use App\Http\Controllers\Admin\SendyController;

Then add two routes:

Route::get('admin/sendy', [SendyController::class, 'sendy'])->name('sendy');
Route::put('admin/sendy/update', [SendyController::class, 'updateSendy'])->name('sendy.update');

We will create the “updateSendy” method soon.

Now, we can add the markup for the admin view. To the body add the following code:

<section class="container mt-4">

<!-- Update messages -->
<div class="row">
<div class="col-lg-12">
@if(session('successMsg'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<strong>{{ session('successMsg') }}</strong>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif

@if(session('failureMsg'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>{!! session('failureMsg') !!}</strong>
<button type="button" class="close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
</div>
</div> <!-- /. Update messages -->

<h3 class="mt-3 mb-3 text-center">Sendy - Admin Settings</h3>

<form action="{{route('sendy.update')}}" method="POST" class="form-horizontal mt-3 pt-3">
@csrf
@method('PUT')

<div class="row">
<div class="col-sm-5">
<h6>Sendy Install URL</h6>
<p style="font-size: 14px;">The URL where you installed Sendy (without trailing slash)</p>
</div>

<div class="col-sm-7">
<input class="form-control" name="install_url" type="text" value="{{ old('install_url', $config->install_url) }}">
<p style="color:red;">{{ $errors->first('install_url') }}</p>
</div>
</div>

<hr>

<div class="row">
<div class="col-sm-5">
<h6>Sendy API Key</h6>
<p style="font-size: 14px;">The Sendy API key that you can find when you log into your Sendy installation.</p>
</div>

<div class="col-sm-7">
<input class="form-control" name="api_key" type="text" value="{{ old('api_key', $config->api_key) }}">
<p style="color:red;">{{ $errors->first('api_key') }}</p>
</div>
</div>

<hr>

<div class="row">
<div class="col-sm-5">
<h6>Sendy list ID</h6>
<p style="font-size: 14px;">This is the id that's shown when you hover over your Sendy email list.</p>
</div>

<div class="col-sm-7">
<input class="form-control" name="list_id" type="text" value="{{ old('list_id', $config->list_id) }}">
<p style="color:red;">{{ $errors->first('list_id') }}</p>
</div>
</div>

<button type="submit" class="btn btn-info mt-3">Save settings</button>

</form>

</section>

Here I’m displaying the data from the sendy table.

Now we need to create the update method so we can update the date here.

4. Create the update method

In the Admin/SendyController let’s create a new method

public function updateSendy(Request $request)
{

$config = SendyConfig::first();
if( is_null($config) )
{
return redirect()->back()->with('failureMsg', 'The config not found')->withInput();
}

$validator = Validator::make($request->all(), [
'install_url' => ['required', 'string', 'max:1000'],
'api_key' => ['required', 'string', 'max:1000'],
'list_id' => ['required', 'string', 'max:1000'],
]);

if( $validator->fails() )
{
return redirect()->back()->with('failureMsg')->withErrors($validator)->withInput();
}

$config->install_url = $request->install_url;
$config->api_key = $request->api_key;
$config->list_id = $request->list_id;
$config->save();

return redirect(route('sendy'))->with('successMsg', 'Settings updated successfully!');
}

Here we get the first record from the sendy_configs table. We perform some checks and validate the incoming date from the form. We redirect the user if the validation fails, if not we save the data and come back with a success message.

We are done with the admin part of the project.

5. Create SubscribeController

Let’s create a new controller that will handle user subscriptions.

php artisan make:controller SubscribeController

We also need a post route for this, so in web.php pull in

use App\Http\Controllers\SubscribeController;

and add the following route:

Route::post('/subscribe-to-list', [SubscribeController::class, 'subscribeToList'])->name('subscribe');

Now, let’s go back to the “SubscribeController” and create the “subscribeToList” method.

First import:

use Illuminate\Support\Facades\Validator;
use DB;
use Exception;

then add:

public function subscribeToList(Request $request)
{
$validation = Validator::make($request->all(), [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'string', 'email:filter', 'max:100'],
]);

if( $validation->fails() )
{
return redirect()->back()->with('error')->withInput();
}

$sendyConfig = DB::table('sendy_configs')->first();
$errorMsg = '';

if(is_null($sendyConfig))
{
$errorMsg = 'Sendy config not found!';
return false;
}

if(is_null($sendyConfig->api_key))
{
$errorMsg = 'Sendy API key not found!';
return false;
}

if(is_null($sendyConfig->list_id))
{
$errorMsg = 'Sendy list id not found!';
return false;
}

if(is_null($sendyConfig->install_url))
{
$errorMsg = 'Install url not found';
return false;
}

$postdata = http_build_query(
array(
'name' => $request->name,
'email' => $request->email,
'list' => $sendyConfig->list_id,
'api_key' => $sendyConfig->api_key,
'boolean' => 'true'
)
);

$opts = array(
'http' => array(
'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded',
'content' => $postdata
)
);

$context = stream_context_create($opts);

try
{
$result = file_get_contents($sendyConfig->install_url . '/subscribe', false, $context);
}
catch(Exception $e)
{
$errorMsg = 'An error occured during subscription!';
return false;

}

if($result != 1)
{
$errorMsg = $result;
return false;
}

return json_encode(array('success' => 'Thanks for subscribing!' ));

}

First we do some validations and checks. The more important part comes from where we declare the $postdata variable.

Sendy provides some example code and we can use chunks of it here. You can download the code from here.

We can take the postdata and opts variables from here and paste it into our own file. We need to modify values of the name, email, list and api_key keys in postdata to match our requests.

Then we create an array for the stream options called $opts. It contains an http element which is also an array. Here we specify the http request as “POST”. We set the http header to “application/x-www-form-urlencoded” and the content will be the postdata.

If we are sending date via a POST request we need to use the “application/x-www-form-urlencoded” content type or the or the “multipart/form-data”. But since we are not uploading any files we don’t need to use the “multipart/form-data”.

Then we call the stream_context_create function and pass in the opts array. This will return a stream context resource that we pass as the third parameter in the file_get_contents function in the try catch statement.

6. Add Javascript to the view

On the frontend, in the welcome.blade.php file we need to add a fore javascript function for error displaying and also for fetching the resource from the server.

So first let’s reference a few things:

const submitBtn = document.getElementById('submit-btn');
const errorsDiv = document.getElementsByClassName('display-errors')[0];
const errorsSpan = document.getElementById('errors');
const successDiv = document.getElementsByClassName('display-success')[0];
const successSpan = document.getElementById('successes');

Then we create functions to show error and success messages.

function showError(errMsg)
{
successSpan.innerHTML = '';
successDiv.style.display = 'none';
errorsSpan.innerHTML = errMsg;
errorsDiv.style.display = '';
submitBtn.disabled = false;
}
function showSuccess(successMsg)
{
errorsSpan.innerHTML = '';
errorsDiv.style.display = 'none';
successSpan.innerHTML = successMsg;
successDiv.style.display = '';
submitBtn.disabled = false;
}

Then we create validations for a few scenerios:

function validateFront(nameVal, emailVal)
{
if(nameVal == '')
{
showError("Provide a name!");
return false;
}
const regex = /[a-zA-z]+/;
if(!regex.test(nameVal))
{
showError("Provide a valid name!");
return false;
}
if(emailVal == '')
{
showError("Provide an email address!");
return false;
}
const regexEmail = /^([a-zA-Z0-9_\.\-\+])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
if(!regexEmail.test(emailVal))
{
showError("Provide a valid email address!");
return false;
}
const privacyCheck = document.getElementById('flexCheckDefault');
if(!privacyCheck.checked)
{
showError("Accept Privacy Policy!");
return false;
}
return true;
}

Then we create a function to send the resources from the server using the fetch method:

function subscribeEmail(name, email)
{
fetch("{{ route('subscribe') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": "{{ csrf_token() }}",
},
body: JSON.stringify({
name: name,
email: email,
}),
}).then(function(res) {
if(!res.ok)
{
const requestErr = '{"error":' + "\"" + "An unexpected error occured while sending the request!" + "\"" + '}';
return JSON.parse(requestErr);
}
else
{
return res.json();
}
}).then(function(data){
if(data.hasOwnProperty('success'))
{
showSuccess(data['success']);
}
else if((data.hasOwnProperty('error')))
{
showError(data['error']);
}
else
{
showError("Unexpected error occured!");
}
});
}

Then we create the subscribeManager function that we add with an onclick event to the form button.

function subscribeManager()
{
submitBtn.disabled = true;
errorsSpan.innerHTML = '';
errorsDiv.style.display = 'none';
successSpan.innerHTML = '';
successDiv.style.display = 'none';

let nameVal = document.getElementById('name').value;
let emailVal = document.getElementById('email').value;
let isValid = validateFront(nameVal, emailVal);

if(!isValid) return;

subscribeEmail(nameVal, emailVal);
}

 

If you did everything right, the users should be able to subscribe and their data should be saved to your Sendy email list.

References:

ChristianKovats
ChristianKovats

Christian Kovats is a London-based web designer who loves crafting innovative and user-friendly interfaces. His passion for design is only rivaled by his love for nature and hiking, often drawing inspiration from his treks through the diverse landscapes of the UK. Outside of work, Christian enjoys spending quality time with his wife and 4-year-old son. A seasoned lucid dreamer, he also spends time exploring the intricate world of dream interpretation, reflecting his ever-curious and creative mind.

ChristianKovats

Leave a reply

You must be logged in to post a comment.