Sep.27

Bluehost Affiliate Program

I was working with one of my new clients who set up her site on WordPress.com, which severely limited what I could do for her, so I recommended she signup for Bluehost. Bluehost offers ssh access and is only $4.95 / month, and I hear about a lot of affiliates who use it for their own sites. Awesome – that was my recommendation in my email.

Bluehost - Pricing

Price Increase

Of course, if I’m recommending a hosting provider, I want my cut of the deal. So after logging into CJ, I got my Bluehost affiliate link and tested it out (I always test my links before sending them). Imagine my surprise when the price went from $4.95 / month (without an affiliate cookie set) to $6.99 / month with the cookie set!

Bluehost.com from affiliate link

Big surprise there! As affiliates, we explain to shoppers that buying through affiliate links doesn’t cost them any more than if they bought directly from the merchant. But, this isn’t the first time this has happened, either (FTD – affiliate links cost more – thanks Tricia for the tip).

This is where I excel as a human, and fail as a businessman. I didn’t use my affiliate link when referring my client to Bluehost. I’d rather them save a couple of bucks, than me make a $90 commission. What’s also interesting is that going Incognito in Chome and making sure cookies were cleared still showed me the higher rate, and this…

Disclosure on Merchant Site

Another thing that surprised me was a statement on the Bluehost website when I (the shopper) was tagged as an affiliate-referred visitor:

Since you landed on this page of our site, we want to let you know you visited one of our paid endorsers while researching your purchase.

We’ve talked about Affiliates needing to disclose their relationships, and we’ve talked about OPMs needing to enforce it, but this is the first time I’ve seen a merchant disclosing the relationship.

So bravo to Bluehost for disclosing the relationship with your affiliate, but shame on you for charging more when affiliate links are used!

[fmtcpod pod=”f9d2b73e482fd540beb85113a34a52d4″ sid=”ericnagel”]
News

Mar.12

May.18

Adding CJ Products To Your Datafeed Website

I received an email last week asking how to add Commission Junction products to datafeed websites. To do this, take a look at the CJ Web Services. Particularly, the Product Catalog Search Service (REST).

What this does is allows you to specify a merchant’s ID, and get all of the product links for that merchant. In my example, I was grabbing the products from Mighty Leaf Tea.

So this PHP script will sit in your site’s root, like the other admin scripts, and when you pull it up, you’ll be presented with a form, asking for the Advertiser ID. That’s the AID when you’re pulling links, or advertiserId in the URL when you’re looking at the merchant in CJ. For Mighty Leaf Tea, it’s 2346375.

The biggest problem is that the CJ Web Service for Product Catalog Search doesn’t return the product ID! So I had to come up with a way to create an ID for a product. In this case, I based it on the SKU and ended up with:

list($nProductID, ) = explode('E', round(hexdec(md5($oCJProduct->{'sku'}))/1000000000000000000000000));
$nProductID = str_replace('.', '', $nProductID);
$nProductID = abs($nProductID);

If you don’t know what this is doing, don’t worry about it. If you’re bored, look-up those functions & let me know if you can think of a better way to make an ID from a sku. There has to be a simpler solution.

Anyway, because of this, I had to change the ProductID field to a bigint(20), since these item IDs were quite large (ex: 22229748315941). In addition, whenever the front-end scripts type-juggled the itemID into an (int), I had to remove that, as int’s are not big enough to hold this data (browse.php and item.php).

Getting back to the script, when I enter 2346375 in the form, then submit it, this script will query CJ for all product links for that merchant, insert or update them in the database, and finally remove any old products.

<?php
	include('./vars.php');
	include('./admin-password.php');
	$bNavHome = true;
	include('./header.php');

	$cTitle = 'Get CJ Datafeed';
?>
<div class="post">
<h2 class="title"><?= $cTitle ?></h2>
<div class="entry">
<?php

	if (!empty($_GET['advertiser-id'])) {
		$cDevKey = 'youridhere';
		$nWebSiteID = "yourwebsiteid";

		$nMax = 1;

		// We'll use this later to determine which products can be deleted
		mysql_query("update products set bActiveProduct=0 where MerchantID=" . (int)$_GET['advertiser-id'] . "");

		function addCJProduct($oCJProduct) {
			list($nProductID, ) = explode('E', round(hexdec(md5($oCJProduct->{'sku'}))/1000000000000000000000000));
			$nProductID = str_replace('.', '', $nProductID);
			$nProductID = abs($nProductID);

			if (mysql_num_rows(mysql_query("select * from products where ProductID=" . $nProductID . " and MerchantID=" . (int)$oCJProduct->{'advertiser-id'} . " limit 1")) == 0) {
				// This is a new record
				$cQuery = "insert into products (ProductID, Name, MerchantID, Merchant, Link, Thumbnail, BigImage, Price, RetailPrice, Description, Lastupdated, Manufacturer, PartNumber, ISBN, UPC, SKU, bActiveProduct) values (" . $nProductID . ", '" . myres($oCJProduct->{'name'}) . "', '" . myres($oCJProduct->{'advertiser-id'}) . "', '" . myres($oCJProduct->{'advertiser-name'}) . "', '" . myres($oCJProduct->{'buy-url'}) . "', '" . myres($oCJProduct->{'image-url'}) . "', '" . myres($oCJProduct->{'image-url'}) . "', '" . myres($oCJProduct->{'price'}) . "', '" . myres($oCJProduct->{'retail-price'}) . "', '" . myres($oCJProduct->{'description'}) . "', now(), '" . myres($oCJProduct->{'manufacturer-name'}) . "', '" . myres($oCJProduct->{'sku'}) . "', '" . myres($oCJProduct->{'isbn'}) . "', '" . myres($oCJProduct->{'upc'}) . "', '" . myres($oCJProduct->{'sku'}) . "', 1)";
			} // ends <insert new record>
			else {
				// This is an existing record
				$cQuery = "update products set Name='" . myres($oCJProduct->{'name'}) . "', MerchantID=" . (int)$oCJProduct->{'advertiser-id'} . ", Merchant='" . myres($oCJProduct->{'advertiser-name'}) . "', Link='" . myres($oCJProduct->{'buy-url'}) . "', Thumbnail='" . myres($oCJProduct->{'image-url'}) . "', BigImage='" . myres($oCJProduct->{'image-url'}) . "', Price='" . myres($oCJProduct->{'price'}) . "', RetailPrice='" . myres($oCJProduct->{'retail-price'}) . "', Description='" . myres($oCJProduct->{'description'}) . "', Lastupdated=now(), Manufacturer='" . myres($oCJProduct->{'manufacturer-name'}) . "', PartNumber='" . myres($oCJProduct->{'sku'}) . "', ISBN='" . myres($oCJProduct->{'isbn'}) . "', UPC='" . myres($oCJProduct->{'upc'}) . "', SKU='" . myres($oCJProduct->{'sku'}) . "', bActiveProduct=1 where ProductID=" . $nProductID . " and MerchantID=" . (int)$oCJProduct->{'advertiser-id'} . " limit 1";
			} // ends else from <updating existing record>
			mysql_query($cQuery);
			if (mysql_error()) {
				echo("<p><b>MySQL Error: " . mysql_error() . "<br />\n");
				echo("$cQuery</p>");
				echo("<pre>");
				print_r($rsItem);
				echo("</pre>");
				exit();
			} // ends if (mysql_error())

		} // ends function addCJProduct($oCJProduct)

		for ($nPageNumber = 1; $nPageNumber <= $nMax; $nPageNumber++) {

			$cURL = 'https://product-search.api.cj.com/v2/product-search?';
			$cURL .= 'advertiser-ids=' . $_GET['advertiser-id'] . '&';
			$cURL .= 'records-per-page=1000&';
			$cURL .= 'page-number=' . $nPageNumber . '&';
			$cURL .= 'website-id=' . $nWebSiteID;

			$ch = curl_init();
			curl_setopt($ch, CURLOPT_URL, $cURL);
			curl_setopt($ch, CURLOPT_HTTPHEADER, array(
						  'Authorization: ' . $cDevKey,
						  'User-Agent: "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.15) Gecko/2009101601 Firefox/3.0.15 GTB6 (.NET CLR 3.5.30729)"'
						));

			curl_setopt($ch, CURLOPT_HEADER, false);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

			curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
			curl_setopt($ch, CURLOPT_TIMEOUT, 10);

			$cHTML = curl_exec($ch);
			if (curl_error($ch)) {
				echo "Curl error: " . curl_error($ch);
			} // ends if (curl_error($ch))
			else {
				$cXML = simplexml_load_string($cHTML);

				// Update nMax
				if ($cXML->products->attributes()->{'total-matched'} > 1000) {
					$nMax = floor($cXML->products->attributes()->{'total-matched'}/$cXML->products->attributes()->{'records-returned'});
				} // ends if ($cXML->products->attributes()->{'total-matched'} > 1000)

				for ($i = 0; $i < count($cXML->products->product); $i++) {
					addCJProduct($cXML->products->product[$i]);
				} // ends

			} // ends else from if (curl_error($ch))
		} // ends for loop

		mysql_query("delete from products where bActiveProduct=0 and MerchantID=" . (int)$_GET['advertiser-id'] . "");

		echo("<p><strong>" . $cXML->products->attributes()->{'total-matched'} . "</strong> products have been loaded.</p>");

		echo("<p>There are " . mysql_num_rows(mysql_query("select * from products where MerchantID=" . (int)$_GET['advertiser-id'] . "")) . " products.</p>");
	} // ends if (!empty($_GET['advertiser-id']))
	else {
		// Show form
		?>
		<form method="get" action="<?= $_SERVER['PHP_SELF'] ?>">
			CJ Advertiser ID: <input type="text" name="advertiser-id" /><br />
			<input type="submit" name="cAction" value="Get Datafeed" />
		</form>
		<?php
	} // ends else from if (!empty($_GET['advertiser-id']))
?>

</div>
</div><!-- ends class="post" -->

<p align="center">[ <a href="<?= $_SERVER['PHP_SELF'] ?>"><?= $cTitle  ?> Home</a> | <a href="./admin.php" target="_top">Admin Home</a> ]</p>
<?php
	include('./footer.php');
?>

Because CJ products do not include categories, I had to change my browse.php to search the product title for the category (green, black, white). To create a quality site, however, I recommend you put products in their appropriate category using the custom category script.

You’ll notice I output 2 numbers at the end – the first is how many products were returned, and the second is how many were loaded into the database. For Mighty Leaf Tea, it loaded 413 products, but only 410 were in the database when I was done. Either 3 products had duplicate SKUs, or 3 products ended up with duplicate ProductIDs after my process was done to generate a ProductID from SKU. I’m not sure, but 410/413 is pretty good, so I left it alone.

This script was really hacked together – let me know if you have questions / problems.

Resources

May.03

Tracking CJ Commissions with Prosper202

CJ + Prosper202 logosI got a call today from a guy in NJ looking to track CJ sales with Prosper202. He saw my post on Tracking ShareASale Commissions with Prosper202 and another post on using the Commission Junction Web Services and wanted some info on how to combine the two.

It’s actually pretty simple… just take the code from the CJ page, then the part where 202 is pinged from the SAS+202 post, and you get:

<?php
	$cDevKey = 'your-developer-key';

	$cURL = 'https://commission-detail.api.cj.com/v3/commissions?';
	$cURL .= 'date-type=event&';

	$ch = curl_init();
	curl_setopt($ch, CURLOPT_URL, $cURL);
	curl_setopt($ch, CURLOPT_HTTPHEADER, array(
				  'Authorization: ' . $cDevKey,
				  'User-Agent: "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.15) Gecko/2009101601 Firefox/3.0.15 GTB6 (.NET CLR 3.5.30729)"'
				));

	curl_setopt($ch, CURLOPT_HEADER, false);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

	curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
	curl_setopt($ch, CURLOPT_TIMEOUT, 10);

	$cHTML = curl_exec($ch);
	if (curl_error($ch)) {
		echo "Curl error: " . curl_error($ch);
	} // ends if (curl_error($ch))
	else {
		$cXML = simplexml_load_string($cHTML);

		for ($i = 0; $i < count($cXML->commissions->commission); $i++) {
			$oCJCommission = $cXML->commissions->commission[$i];

			$cPostback = 'http://yourdomain.com/tracking202/static/gpb.php?amount=' . urlencode($oCJCommission->{'commission-amount'}) . '&subid=' . urlencode($oCJCommission->sid);
			$fpPostback = @fopen($cPostback, "r");
			if ($fpPostback !== false) {
				fclose($fpPostback);
			} // ends if ($fpPostback !== false)
		} // ends for ($i = 0; $i < count($cXML->commissions->commission); $i++)
	} // ends else from if (curl_error($ch))
?>

Cron this script (I have mine run every day about 7am) and you’ll see your CJ commissions in Prosper202

How To & Tips

Dec.16

Commission Junction Web Services

Next in my series of affiliate network API programming examples, I’ve decided to tackle Commission Junction. The goal was to get a list of my stats from yesterday for one website, and store them in my custom tracking script.

Commission JunctionCJ has two choices when it comes to how to interface with them: REST and SOAP. Since I already had some SOAP code laying around, I decided to go that route (using the Daily Publisher Commission Service) . However, after I had my script done (took about 20 minutes), I realized CJ was only giving back half of my sales! From their website, I saw 6 sales for this one website:

  • 971450243
  • 971361785
  • 971179939
  • 971144045
  • 971097290
  • 971086202

Yet the SOAP API only returned 3 of them:

  • 971086202
  • 971144045
  • 971450243

So I emailed CJ about this (have yet to hear back from them) but being the impatient person that I am, I checked out the REST API. To my surprise, there is no equivalent to their SOAP Daily Publisher Commission Service – the closest I saw was their Commission Detail Service (REST). However, this service doesn’t take in any date parameters – your choice is yesterday or nothing. For this project, that’s OK as that’s what I want.

So the first thing to do is build the query. Starting with the base URL, add on the parameters you’d like. In my case, it’s the date-type and website-ids.

$cURL = 'https://commission-detail.api.cj.com/v3/commissions?';
$cURL .= 'date-type=event&';
$cURL .= 'website-ids=' . $nWebSiteID;

Then, using curl, grab the results:

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $cURL);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
			  'Authorization: ' . $cDevKey,
			  'User-Agent: "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.15) Gecko/2009101601 Firefox/3.0.15 GTB6 (.NET CLR 3.5.30729)"'
			));

curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);

$cHTML = curl_exec($ch);
if (curl_error($ch)) {
	echo "Curl error: " . curl_error($ch);
} // ends if (curl_error($ch))
else {
	$cXML = simplexml_load_string($cHTML);
	// var_dump($cXML);

	for ($i = 0; $i < count($cXML->commissions->commission); $i++) {
		addCJCommission($cXML->commissions->commission[$i]);
	} // ends for ($i = 0; $i < count($cXML->commissions->commission); $i++)
} // ends else from if (curl_error($ch))

What I have in the loop is a function, addCJCommission(), which takes the commission object as a parameter. This function will add the commission to my database. I’m not going to get into details about that (as it’s specific to my project, and not what you’re doing) but one thing I found out is that objects with a dash in the property name require some special coding. For example, getting the date from the event-date property:

Don’t do this:

$dDate = date("Y-m-d", strtotime($oCJCommission->event-date));

Do this:

$dDate = date("Y-m-d", strtotime($oCJCommission->{'event-date'}));

To get started with Commission Junction Web Services, visit http://help.cj.com/en/web_services/web_services.htm. Thanks to forums.digitalpoint.com for the discussion on setting the Authorization header with curl.

How To & Tips