All requests posted to the server must be cryptographically signed.

As far as security measures go, many APIs are content with just having an API key or username passed in as a parameter when an endpoint is called. The danger with that scheme is the ability for someone to steal the API token and then making their own calls with it.

For a system like Seller Center that manages large sums of money, we had to choose a more secure approach: the API key never leaves your computer and is only used to sign the request.

Additionally, to also make sure that calls to the API cannot be recorded and replayed, one of the parameters getting signed is a time stamp.

More specifically, the signature parameter we require on all calls is the SHA256 hash of
the request string.

More specifically, the signature parameter we require on all calls is the HMAC of the request string and your API key with the SHA256 digest algorithm.

So, let's look at how the signing is implemented:

API Key

First we need an API key. You create one under Settings / Manage Users:

1186

🚧

The API Key is a String

Even though the key might look like a hexadecimal number, it must be treated as a string in the context of signing.

Computing the Signature Parameter

The string to sign is...

  • the concatenated result of all request parameters,
  • ordered by name,
  • including optional parameters,
  • and excluding the signature parameter.

Names and values must be URL encoded according to RFC 3986 standard, concatenated with the character '='. Each parameter set (name=value) must be separated with the character '&'.

The following is the reference implementation PHP:

<?php

// Pay no attention to this statement.
// It's only needed if timezone in php.ini is not set correctly.
date_default_timezone_set("UTC");

// The current time. Needed to create the Timestamp parameter below.
$now = new DateTime();

// The parameters for our GET request. These will get signed.
$parameters = array(
    // The user ID for which we are making the call.
    'UserID' => '[email protected]',

    // The API version. Currently must be 1.0
    'Version' => '1.0',

    // The API method to call.
    'Action' => 'FeedList',

    // The format of the result.
    'Format' => 'XML',

    // The current time formatted as ISO8601
    'Timestamp' => $now->format(DateTime::ISO8601)
);

// Sort parameters by name.
ksort($parameters);

// URL encode the parameters.
$encoded = array();
foreach ($parameters as $name => $value) {
    $encoded[] = rawurlencode($name) . '=' . rawurlencode($value);
}

// Concatenate the sorted and URL encoded parameters into a string.
$concatenated = implode('&', $encoded);

// The API key for the user as generated in the Seller Center GUI.
// Must be an API key associated with the UserID parameter.
$api_key = 'b1bdb357ced10fe4e9a69840cdd4f0e9c03d77fe';

// Compute signature and add it to the parameters.
$parameters['Signature'] =
    rawurlencode(hash_hmac('sha256', $concatenated, $api_key, false));

If you want to verify the above reference, replace the timestamp with the following value...

'Timestamp' => '2015-07-01T11:11:11+00:00'

...and you should get the following entries in $parameters:

Action=FeedList
Format=XML
Timestamp=2015-07-01T11:11:11+00:00
[email protected]
Version=1.0
Signature=3ceb8ed91049dfc718b0d2d176fb2ed0e5fd74f76c5971f34cdab48412476041

To then actually make the GET request in PHP, you would write something like this:

<?php

// ...continued from above

// Replace with the URL of your API host.
$url = "https://sellercenter-api.linio.com.mx/";

// Build Query String
$queryString = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);

// Open cURL connection
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url."?".$queryString);

// Save response to the variable $data
curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$data = curl_exec($ch);

// Close Curl connection
curl_close($ch);

If you are more familiar with other programming languages, here are implementations in...

API Call in Java

/*
  * Sample Interface for Seller Center API
  */
package com.rocket.sellercenter;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.text.DateFormat;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class SellercenterAPI {
  private static final String ScApiHost = "https://sellercenter-api.linio.com.mx/"; 
  private static final String HASH_ALGORITHM = "HmacSHA256";
  private static final String CHAR_UTF_8 = "UTF-8";
  private static final String CHAR_ASCII = "ASCII";
  public static void main(String[] args) {
    Map<String, String> params = new HashMap<String, String>();
    params.put("UserID", "[email protected]");
    params.put("Timestamp", getCurrentTimestamp());
    params.put("Version", "1.0");
    params.put("Action", "ProductUpdate");
    final String apiKey = "55f86f79f3b4388507aba8c21a7bfd0d25626551";
    final String XML = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?
      ><Request><Product><SellerSku>4105382173aaee4</SellerSku><Price>12</Price></Product></Request>";
      final String out = getSellercenterApiResponse(params, apiKey, XML); // provide XML as an empty string
    when not needed
      System.out.println(out); // print out the XML response
  }

  /**
  * calculates the signature and sends the request
  *
  * @param params Map - request parameters
  * @param apiKey String - user's API Key
  * @param XML String - Request Body
  */
  public static String getSellercenterApiResponse(Map<String, String> params, String apiKey, String XML) {
    String queryString = "";
    String Output = "";
    HttpURLConnection connection = null;
    URL url = null;
    Map<String, String> sortedParams = new TreeMap<String, String>(params);
    queryString = toQueryString(sortedParams);
    final String signature = hmacDigest(queryString, apiKey, HASH_ALGORITHM);
    queryString = queryString.concat("&Signature=".concat(signature));
    final String request = ScApiHost.concat("?".concat(queryString));
    try {
      url = new URL(request);
      connection = (HttpURLConnection) url.openConnection();
      connection.setDoOutput(true);
      connection.setDoInput(true);
      connection.setInstanceFollowRedirects(false);
      connection.setRequestMethod("POST");
      connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
      connection.setRequestProperty("charset", CHAR_UTF_8);
      connection.setUseCaches(false);
      if (!XML.equals("")) {
        connection.setRequestProperty("Content-Length", "" + Integer.toString(XML.getBytes().length));
        DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
        wr.writeBytes(XML);
        wr.flush();
        wr.close();
      }
      String line;
      BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
      while ((line = reader.readLine()) != null) {
        Output += line + "\n";
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    return Output;
  }

  /**
  * generates hash key
  *
  * @param msg
  * @param keyString
  * @param algo
  * @return string
  */
  private static String hmacDigest(String msg, String keyString, String algo) {
    String digest = null;
    try {
      SecretKeySpec key = new SecretKeySpec((keyString).getBytes(CHAR_UTF_8), algo);
      Mac mac = Mac.getInstance(algo);
      mac.init(key);
      final byte[] bytes = mac.doFinal(msg.getBytes(CHAR_ASCII));
      StringBuffer hash = new StringBuffer();
      for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(0xFF & bytes[i]);
        if (hex.length() == 1) {
          hash.append('0');
        }
        hash.append(hex);
      }
      digest = hash.toString();
    } catch (UnsupportedEncodingException e) {
      e.printStackTrace();
    } catch (InvalidKeyException e) {
      e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
    }
    return digest;
  }
  
  /**
  * build querystring out of params map
  *
  * @param data map of params
  * @return string
  * @throws UnsupportedEncodingException
  */
  private static String toQueryString(Map<String, String> data) {
    String queryString = "";
    try{
      StringBuffer params = new StringBuffer();
      for (Map.Entry<String, String> pair : data.entrySet()) {
        params.append(URLEncoder.encode((String) pair.getKey(), CHAR_UTF_8) + "=");
        params.append(URLEncoder.encode((String) pair.getValue(), CHAR_UTF_8) + "&");
      }
      if (params.length() > 0) {
        params.deleteCharAt(params.length() - 1);
      }
      queryString = params.toString();
    } catch(UnsupportedEncodingException e){
      e.printStackTrace();
    }
    return queryString;
  }
  
  /**
  * returns the current timestamp
  * @return current timestamp in ISO 8601 format
  */
  private static String getCurrentTimestamp(){
    final TimeZone tz = TimeZone.getTimeZone("UTC");
    final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ");
    df.setTimeZone(tz);
    final String nowAsISO = df.format(new Date());
    return nowAsISO;
  }
}

API Call in Python

import urllib
from hashlib import sha256
from hmac import HMAC
from datetime import datetime

parameters = {
  'UserID': '[email protected]',
  'Version': '1.0',
  'Action': 'FeedList',
  'Format':'XML',
  'Timestamp': datetime.now().isoformat()\n}
api_key = 'b1bdb357ced10fe4e9a69840cdd4f0e9c03d77fe'
concatenated = urllib.urlencode(sorted(parameters.items()))
parameters['Signature'] = HMAC(api_key, concatenated, sha256).hexdigest()

API Call in Visual Basic

Imports System
Public Module modmain
	Sub Main()
		' add your data here:	
    Dim userId As String = "" 'login name / your email
    Dim password As String = "" 'your API key/password
    Dim version As String = "1.0"
    Dim action As String = "ProductCreate"
    Dim url As String = ""
    'e.g.: "https://sellercenter-api.linio.com.mx/"
    Dim result As String
    
    ' this is where the magic happens:
    result = generateRequest(url, userId, password, version, action)
    Console.WriteLine (result)
    End Sub

		Function generateRequest(Url As String, user As String, key as String, version As String, action As
String) As String
      Dim timeStamp as String = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss-0000")
      ' ATTENTION: parameters must be in alphabetical order
      Dim stringToHash As String = _
      "Action=" + URLEncode(action) + _
      "&Timestamp=" + URLEncode(timeStamp) + _
      "&UserID=" + URLEncode(user) + _
      "&Version=" + URLEncode(version)
      Dim hash As String = HashString(stringToHash, key)
      ' ATTENTION: parameters must be in alphabetical order
      Dim request As String = _
      "Action=" + URLEncode(action) + _
      "&Signature=" + URLEncode(hash) + _
      "&Timestamp=" + URLEncode(timeStamp) + _
      "&UserID=" + URLEncode(user) + _
      "&Version=" + URLEncode(version)
      return url + "?" + request
		End Function
    
    ' use this function instead of HttpServerUtility.UrlEncode()
    ' because we need uppercase letters
    Function URLEncode(EncodeStr As String) As String
    	Dim i As Integer
      Dim erg As String
      erg = EncodeStr
      erg = Replace(erg, "%", Chr(1))
      erg = Replace(erg, "+", Chr(2))
      For i = 0 To 255
        Select Case i
        ' *** Allowed 'regular' characters
          Case 37, 43, 45, 46, 48 To 57, 65 To 90, 95, 97 To 122, 126
          Case 1 ' *** Replace original % erg = Replace(erg, Chr(i), "%25")
          Case 2 ' *** Replace original + erg = Replace(erg, Chr(i), "%2B")
          Case 32 erg = Replace(erg, Chr(i), "+")
          Case 3 To 15 erg = Replace(erg, Chr(i), "%0" & Hex(i))
          Case Else
            erg = Replace(erg, Chr(i), "%" & Hex(i))
        End Select
      Next
      return erg
    End Function
    
    Function HashString(ByVal StringToHash As String, ByVal HachKey As String) As String
      Dim myEncoder As New System.Text.UTF8Encoding
      Dim Key() As Byte = myEncoder.GetBytes(HachKey)
      Dim Text() As Byte = myEncoder.GetBytes(StringToHash)
      Dim myHMACSHA256 As New System.Security.Cryptography.HMACSHA256(Key)
      Dim HashCode As Byte() = myHMACSHA256.ComputeHash(Text)
      Dim hash As String = Replace(BitConverter.ToString(HashCode), "-", "")
	    Return hash.ToLower
		End Function
End Module

API Call in Adobe ColdFusion

<!--- this is a sample Adobe ColdFusion script for sending API request to Seller Center --->
<!--- hashing function --->
<cffunction name="HMAC_SHA256" returntype="string" access="private" output="false">
  <cfargument name="Data" type="string" required="true" />
  <cfargument name="Key" type="string" required="true" />
  <cfargument name="Bits" type="numeric" required="false" default="256" />
  <cfset var i = 0 />
  <cfset var HexData = "" />
  <cfset var HexKey = "" />
  <cfset var KeyLen = 0 />
  <cfset var KeyI = "" />
  <cfset var KeyO = "" />
  <cfset HexData = BinaryEncode(CharsetDecode(Arguments.data, "iso-8859-1"), "hex") />
  <cfset HexKey = BinaryEncode(CharsetDecode(Arguments.key, "iso-8859-1"), "hex") />
  <cfset KeyLen = Len(HexKey)/2 />
  <cfif KeyLen gt 64>
    <cfset HexKey = Hash(CharsetEncode(BinaryDecode(HexKey, "hex"), "iso-8859-1"), "SHA-256", "iso-8859-1") />
    <cfset KeyLen = Len(HexKey)/2 />
  </cfif>
  <cfloop index="i" from="1" to="#KeyLen#">
    <cfset KeyI = KeyI & Right("0"&FormatBaseN(BitXor(InputBaseN(Mid(HexKey,2*i-
           1,2),16),InputBaseN("36",16)),16),2) />
    <cfset KeyO = KeyO & Right("0"&FormatBaseN(BitXor(InputBaseN(Mid(HexKey,2*i-
           1,2),16),InputBaseN("5c",16)),16),2) />
  </cfloop>
  <cfset KeyI = KeyI & RepeatString("36",64-KeyLen) />
  <cfset KeyO = KeyO & RepeatString("5c",64-KeyLen) />
  <cfset HexKey = Hash(CharsetEncode(BinaryDecode(KeyI&HexData, "hex"), "iso-8859-1"), "SHA-256", "iso-8859-1")
         />
  <cfset HexKey = Hash(CharsetEncode(BinaryDecode(KeyO&HexKey, "hex"), "iso-8859-1"), "SHA-256", "iso-8859-
                                                                                                  1") />
  <cfreturn Left(HexKey,arguments.Bits/4) />
</cffunction>
<!---/ hashing function --->
<!--- define --->
<cfset send_xml = false><!--- for APIs that need XML request body, like xxxxProduct APIs --->
  <cfset secret_key="562aeae4090d3a62ef171b6646cc2bdac6417473"/>
  <cfset sc_api_host="http://sellercenter-api.local/"/>
  <!---/ define --->
  <!--- request params --->
  <cfset params = structNew() />
  <cfset params["[email protected]"] = "UserID"/>
  <cfset params["GetShipmentProviders"] = "Action"/>
  <cfset params["2014-07-24T20:06:33+02:00"] = "Timestamp"/>
  <cfset params["1.0"] = "Version"/>
  <cfsavecontent variable="strXML">
    <?xml version="1.0" encoding="UTF-8" ?>
    <Request>
      <Product>
        <SellerSku>4105382173aaee4</SellerSku>
        <Price>12</Price>
      </Product>
    </Request>
  </cfsavecontent>
  <!---/ request params --->
  <!--- generate signature --->
  <cfset strtohash = "" />
  <cfloop list="#ArrayToList(StructSort(params, "text", "asc"))#" index="key" >
    <cfset strtohash = strtohash & #params[key]# & "=" & #URLEncodedFormat(key)# & "&" />
  </cfloop>
  <cfset strtohash = #RemoveChars(strtohash, len(strtohash), 1)#/>
  <cfset strtohash = #Replace(strtohash, "%2D", "-", "All")#/>
  <cfset strtohash = #Replace(strtohash, "%2E", ".", "All")#/>
  <cfset signature="#LCase(HMAC_SHA256(strtohash, secret_key))#"/>
  <!---/ generate signature --->
  <!--- issue the request --->
  <cfif send_xml>
    <cfset http_method = "post" />
  <cfelse>
    <cfset http_method = "get" />
  </cfif>
	<cfhttp method="#http_method#" url="#sc_api_host#">
	<cfloop list="#ArrayToList( StructSort(params, "text", "asc") )#" index="key" >
		<cfhttpparam type="url" name="#params[key]#" value="#key#"></cfhttpparam>
	</cfloop>
	<cfhttpparam type="xml" value="#strXML.Trim()#"></cfhttpparam>
	<cfhttpparam type="url" name="Signature" value="#signature#"></cfhttpparam>
	</cfhttp>
  <!---/ issue the request --->
  <!--- XML response --->
	<cfoutput>#cfhttp.filecontent#</cfoutput>