Build a ShareASale Proxy with Slim PHP Framework
Recently, I launched a new (and extremely dumb) website, FootballCropTop.com, that makes use of the ShareASale Product Search API. If you've ever worked with ShareASale before you are likely familiar with some of the roadblocks thrown up by the enforcement of their security policies. I want to share one approach I took to solve for these roadblocks as part of a larger effort to explain how I built and maintain an automated affiliate website with an ecommerce experience for pennies per month.
In order to access the Product Search API offered by ShareASale you will need to configure some basic security and access controls in your account. One such control is an IP whitelist feature. This requires you to explicitly define all (up to five) of the IP addresses that will be allowed to access the API using your access credentials. This means you will need to have a static IP address somewhere out in the wild.
For my personal projects I do some light hosting on Digital Ocean and each of my Droplets are issued a static IP, something that I am already paying for (~$5/month). For this project I decided to build a small proxy app in PHP using the Slim Framework and deploy it to one of my existing Droploets.
My proxy app is a "hello world" installation of Slim, following the installation guide. It depends on only one additional library, league/csv by The League of Extraordinary Packages.
In addition to the static IP whitelisting requirement, ShareASale also requires that each request be signed with an encrypted token which must be done on each request. I'd like to use other tools (like Postman locally or AWS Lambdas in the cloud) to access the ShareASale data through the static IP address associated with the Droplet hosting this small proxy app.
I came up with a basic design that would allow me to set my secret credential information via headers and forward a request through the proxy. In turn the proxy will sign and authenticate the request, fetch the results, cleanup and format a nice response (ShareASale is not the best in this area), and finally send it back to the requesting client. Here is the code from my Slim project that does this work:
<?php
use GuzzleHttp\Client;
use League\Csv\Reader;
use Slim\Http\Request;
use Slim\Http\Response;
// Routes
$app->group('/shareasale', function () {
$this->get('/{action}', function ($request, $response, $args) {
// Extract our ShareASale query params from inbound request
try {
// 'keyword' => 'crop top',
// 'merchantId' => 1234,
parse_str($request->getUri()->getQuery(), $query);
} catch (\Exception $e) {
$query = [];
}
// Extract our ShareASale action from the route
$action = $args['action'];
$serviceUrl = 'https://api.shareasale.com/x.cfm';
// Extract our ShareASale auth parts from inbound headers
$versionHeaders = $request->getHeader('x-shareasale-version');
$version = array_shift($versionHeaders);
$affiliateIdHeaders = $request->getHeader('x-shareasale-affiliateid');
$affiliateId = array_shift($affiliateIdHeaders);
$tokenHeaders = $request->getHeader('x-shareasale-token');
$token = array_shift($tokenHeaders);
$secretKeyHeaders = $request->getHeader('x-shareasale-secretkey');
$secretKey = array_shift($secretKeyHeaders);
// Create our ShareASale authentication from auth parts
$timestamp = gmdate(DATE_RFC1123);
$signature = $token.':'.$timestamp.':'.$action.':'.$secretKey;
$signatureHash = hash("sha256", $signature);
// Prepare our ShareASale request
$headers = [
'x-ShareASale-Date' => $timestamp,
'x-ShareASale-Authentication' => $signatureHash,
];
$options = array_merge($query, [
'action' => $action,
'affiliateId' => $affiliateId,
'version' => $version,
'token' => $token,
'format' => 'csv'
]);
// Get our ShareASale response from our request
$client = new Client();
$res = $client->request('GET', 'https://api.shareasale.com/x.cfm', [
'headers' => $headers,
'query' => $options
]);
$status = $res->getStatusCode();
$json = [];
// Handle and format our ShareASale response
try {
$csv = Reader::createFromString($res->getBody())->jsonSerialize();
$csvHeaders = array_shift($csv);
$csvHeaders = array_map(function ($property) {
$property = preg_replace('/([a-z])([A-Z])/',"$1_$2", $property);
$property = strtolower($property);
$property = preg_replace('/ /', '_', $property);
return $property;
}, $csvHeaders);
array_walk($csv, function ($csvRow) use ($csvHeaders, &$json) {
array_push($json, array_combine($csvHeaders, $csvRow));
});
} catch (\Exception $e) {
$status = $e->getCode();
$json['status'] = $status;
$json['message'] = (string) $res->getBody();
$json['error'] = $e->getMessage();
}
// Return our reformatted ShareASale response
return $response->withStatus($status)->withJson($json);
});
});
With this little piece of infrastructure in place I am free to begin consuming the product feed data from basically anywhere.
You can read more about this project in the full technical walkthrough.
Written by stevenmaguire
Related protips
1 Response
Wow