WebHook

From Shopify Wiki

Jump to: navigation, search

WebHooks are a way to tell Shopify to call a script on one of your own web servers and react to the event in any way you want.

Some possible uses for this feature include:

  • Notify your IM client or your pager when you are offline
  • Collect data for data-warehousing
  • Integrate your accounting software
  • Filter the order items and inform various drop shippers about the order
  • Create license keys for software sales

Once you register a webhook url with Shopify we will issue a HTTP Post to the url specified and pass on the Order as an XML document. Don't worry, if your server is down we will simply try again until your server confirms to us that it has successfully received the notification.

Image:webhook.png

Easy ways to create webhooks are PHP enabled servers or services such as Appjet

Contents

What does it look like

Don't assume this is exactly what is posted ... use the "test webhook" from the Shopify admin!

i.e. name is no longer included with shipping/billing info

POST /your-path
Content-Type: application/xml

<?xml version="1.0" encoding="UTF-8"?>
<order>
  <buyer-accepts-marketing type="boolean">false</buyer-accepts-marketing>
  <cart-token nil="true"></cart-token>
  <closed-at type="datetime" nil="true"></closed-at>
  <created-at type="datetime">2005-07-31T15:57:11Z</created-at>
  <currency>USD</currency>
  <email>bob@customer.com</email>
  <financial-status>paid</financial-status>
  <fulfillment-status nil="true"></fulfillment-status>
  <gateway>bogus</gateway>
  <id type="integer">516163746</id>
  <note nil="true"></note>
  <number type="integer">1</number>
  <shop-id type="integer">820947126</shop-id>
  <subtotal-price type="integer">10.00</subtotal-price>
  <taxes-included type="boolean">false</taxes-included>
  <total-discounts type="integer">0.00</total-discounts>
  <total-line-items-price type="integer">10.00</total-line-items-price>
  <total-price type="integer">11.50</total-price>
  <total-tax type="integer">1.50</total-tax>
  <total-weight type="integer">0</total-weight>
  <updated-at type="datetime">2005-08-01T15:57:11Z</updated-at>
  <name>#1001</name>
  <billing-address>
    <address1>123 Amoebobacterieae St</address1>
    <address2></address2>
    <city>Ottawa</city>
    <company></company>
    <country>Canada</country>
    <first-name>Bob</first-name>
    <id type="integer">943494288</id>
    <last-name>Bobsen</last-name>
    <phone>(555)555-5555</phone>
    <province>Ontario</province>
    <shop-id type="integer">820947126</shop-id>
    <zip>K2P0V6</zip>
    <name>Bob Bobsen</name>
  </billing-address>
  <shipping-address>
    <address1>123 Amoebobacterieae St</address1>
    <address2></address2>
    <city>Ottawa</city>
    <company></company>
    <country>Canada</country>
    <first-name>Bob</first-name>
    <id type="integer">943494288</id>
    <last-name>Bobsen</last-name>
    <phone>(555)555-5555</phone>
    <province>Ontario</province>
    <shop-id type="integer">820947126</shop-id>
    <zip>K2P0V6</zip>
    <name>Bob Bobsen</name>
  </shipping-address>
  <line-items type="array">
    <line-item>
      <fulfillment-service>manual</fulfillment-service>
      <grams type="integer">1500</grams>
      <id type="integer">642703538</id>
      <price type="integer">10.00</price>
      <quantity type="integer">1</quantity>
      <sku>1</sku>
      <title>Draft</title>
      <variant-id type="integer">47052976</variant-id>
      <vendor nil="true"></vendor>
      <name>Draft - 151cm</name>
    </line-item>
  </line-items>
</order>

Examples:

PHP Example w/ SimpleXML (PHP 5+)

The script below shows you how to get the XML data in from Shopify into your script, archive the file, and send the proper headers ...

Given that the new order subscription setup in the admin for the webhook is: http://example.com/some-script.php?key=123456789
Contents of some-script.php on http://example.com/

// Protect URL from rogue attacks/exploits/spiders
// Grab from GET variable as given in Shopify admin URL
// for the webhook
// NOTE: This is not necessary, just a good idea
// The unique shop id is also passed along from Shopify
// so you can use that additionally or instead of to ensure
// a random person isn't jacking with your script (or some
// spider just randomly hitting it to see what's there)
// If $key doesn't matched what should be passed in from the
// webhook url, the script simply exits
$key = $_GET['key']; 

if ($key != '123456789') {
  header('HTTP/1.0 403 Forbidden');
  exit();
}

// Variables used for processing/saving
$xmlString = ;  // Used to get data from Shopify into script
$name = ;  // Saves the billing address name to be used for later ...
$email = ;  // Save the email address of the user to be used for later ...
$productTitles = array();  // Saves all titles of products purchased to be used for later ... 

// Get XML data and read it into a string for use with SimpleXML
// Thanks to David Oxley (http://www.numeriq.co.uk) for help with this
$xmlData = fopen('php://input' , 'rb'); 
while (!feof($xmlData)) { $xmlString .= fread($xmlData, 4096); }
fclose($xmlData);

// Save order XML in file in orders directory
// This creates a file, write the xml for archival purposes, and closes the file ...
// If the file already exists, it appends the data ... this should create a separate
// file for every order but if two orders are processed the same second, they'll both
// be in the same file
file_put_contents('orders/order' . date('m-d-y') . '-' . time() . '.xml', $xmlString, FILE_APPEND);

// Use SimpleXML to get name, email, and product titles
// SimpleXML allows you to use the $xml object to easily
// retrieve the data ...
// Please note, if hyphens are used in the xml node, you must
// surround the call to that member with {'member-name'} as is
// shown below when getting the billing-address name & the
// line items
$xml = new SimpleXMLElement($xmlString);

$name = trim($xml->{'billing-address'}->name);
$email = trim($xml->email);

// Create productTitles array with titles from products
foreach ($xml->{'line-items'}->{'line-item'} as $lineItem) {
  array_push($productTitles, trim($lineItem->title));
}

// You would then go on using $name, $email, $productTitles in your script
// to do whatever the heck you please ...

// Once you are done doing what you need to do, let Shopify know you have 
// the data and all is well!
header('HTTP/1.0 200 OK');
exit();

// If you want to tell Shopify to try sending the data again, i.e. something
// failed with your processing and you want to try again later
header('HTTP/1.0 400 Bad request');
exit();

PHP Example with Vertical Response API Integration

Vertical Response is a email marketing company that manages subscribers, sends emails, and tracks campaigns. They offer an API that allows you to export your customer data directly into their system via Webhook's. You will need a working Vertical Response account, and request access to their API.

api.php

<?php
 /*
	*	Evisu.com Vertical Response API script
	*	Author: Lachlan Laycock - www.kennedia.com
	* Created: 25 February 2008
	*
	* This script will receive an XML Post Payload from a Shopify.com WebHook, 
	*	extract the buyer details and send it to Evisu's newsletter list via the Vertical Response API
	*
	*/

// Set variables
$security_id			= '_some_security_key_of_your_own_here_';

// Vertical Response Online Store Mailing lists
$vr_list_ids			= array('US' => '######', 'INTL' => '#######');

// List of countries in VR create member form flipped so Country name maps to country code
$country_codes = array_flip(array(
	// ... assoc array of country codes to country name ...
));


// Ensure Shopify Webhook POST is valid
if ($_GET['id'] == $security_id) {
	
	try {
		
		// Get list id from params
		if (isset($_GET['store']) && isset($vr_list_ids[$_GET['store']])) {
			$vr_list_id = $vr_list_ids[$_GET['store']];
		} else {
			// Raise error
			throw "Non-valid store parameter passed! Is this an attack?";
		}
		
		// Get XML data and read it into a string for use with SimpleXML
		$xmlData = fopen('php://input' , 'rb'); 
		while (!feof($xmlData)) { $xmlString .= fread($xmlData, 4096); }
		fclose($xmlData);
		$xml = new SimpleXMLElement($xmlString);

		// Extract the name, email, country
		$user_email				= trim($xml->email);;
		$user_first_name	= trim($xml->{'billing-address'}->{'first-name'});;
		$user_last_name		= trim($xml->{'billing-address'}->{'last-name'});
		$user_address1		= trim($xml->{'billing-address'}->address1);
		$user_address2		= trim($xml->{'billing-address'}->address2);
		$user_phone				= trim($xml->{'billing-address'}->phone);
		$user_province		= trim($xml->{'billing-address'}->province);
		$user_zip					= trim($xml->{'billing-address'}->zip);
		$user_city				= trim($xml->{'billing-address'}->city);
		$user_country			= trim($xml->{'billing-address'}->country);
		$user_ctry_code		= array_key_exists($user_country, $country_codes) ? $country_codes[$user_country] : $user_country;

		// Include VR API login details
		include_once('login.php');

		// Login to get session id
	  $sid = $vr->login(
	                    array(
	                          'username' => $user,
	                          'password' => $pass,
	                          'session_duration_minutes' => $ses_time
	                          )
	                    );

		// Check to see if user already exists in list
		$user_exists = true;
		try {
			$vr->getListMemberByEmailAddress( array(
			    'session_id'    => $sid,
			    'list_id'       => $vr_list_id,
			    'email_address' => $user_email
			) );
		
		// Seems the only way to check the API is member exists is to request it, and will error if not found
		} catch (Exception $e) {
			$user_exists = false;
		}

		if (!$user_exists) {
			// Send new member to list
			$alm =  array( 
				'session_id'  => $sid, 
				'list_member' => array(
					'list_id' => $vr_list_id,
					'member_data' => array(
						array(
							'name' => "email_address",
							'value' => $user_email,
						),
						array(
							'name' => 'first_name',
							'value' => $user_first_name,
						),
						array(
							'name' => 'last_name',
							'value' => $user_last_name,
						),
						array(
							'name' => 'address_1',
							'value' => $user_address1,
						),
						array(
							'name' => 'address_2',
							'value' => $user_address2,
						),
						array(
							'name' => 'city',
							'value' => $user_city,
						),
						array(
							'name' => 'state',
							'value' => $user_province,
						),
						array(
							'name' => 'postalcode',
							'value' => $user_zip,
						),
						array(
							'name' => 'country',
							'value' => $user_ctry_code,
						)
					)
				)
			);
			$one = $vr->addListMember($alm);
		}

		// Return OK
		header("HTTP/1.0 200 OK");

	} catch (Exception $e) {

		// Return Error encountered
		header('HTTP/1.0 400 Bad request');

		// Uncomment for debugging
		// throw new Exception("Error Processing Request: $e", 1);
		
	}
	
} else {
	// Return incorrect request header
	header("HTTP/1.0 403 Forbidden");

}
?>

login.php:

<?php

$wsdl = "https://api.verticalresponse.com/wsdl/1.0/VRAPI.wsdl"; //location of the wsdl
$user = "########"; // VR usernaname
$pass = "########"; //VR passowrd
$cert = "*.pem"; //location of the .pem certificate
$cert_pass = ""; // password for .pem certificate
$ses_time = 3;  // duration of session in minutes

$vr = new SoapClient($wsdl, array (
	"login"      => $user, 
	"password"   => $pass,
));

?>

What should be returned

Shopify will simply discard the results of the WebHook. You just have to make sure that the status code of the resulting page is between 200 and 399 ( preferably 200 OK ) which indicates success.

Shopify will retry to send the XML until a success code is returned.

How to test

Shopify lets you test your web hooks. After you create a web hook you will see it in the list of order notifications. You will also see a Test link. This Test link allows you to send an example order to the address provided.

References

Personal tools