CL-SOAP - Accessing the Google AdWords API

Introduction

The initial goal of the CL-SOAP project was to access the Google AdWords API ( http://www.google.com/apis/adwords/ )

This milestone has now been reached. Please see the file test/test-google-adwords.lisp for even more code.

Tutorial

First XML namespaces have to be set up correctly.

(defconstant +google-adwords-ns-uri+ "https://adwords.google.com/api/adwords/v2")

(defpackage :google
  (:nicknames "google")
  (:documentation "Package for symbols in the Google Adwords API XML Namespace"))

(defparameter *google-adwords-ns* (s-xml:register-namespace +google-adwords-ns-uri+ "google" :google))

;;; apparently there are different XML Schema Defintion namespace URIs, Google is using this one:
 
(s-xml:register-namespace "http://www.w3.org/2001/XMLSchema" "xsd" :xsd)
We are now ready to look at some services.

Example 1: Calling getMethodCost in the InfoService

Parse the WSDL of the service that you want to access and have a look at it.

CL-SOAP 11 > (describe-wsdl-soap (wsdl-cache-get "https://adwords.google.com:443/api/adwords/v2/InfoService?wsdl"))
WSDL Document Definitions
  Service: InfoService
    Port: InfoService
    SOAP Address Location "https://adwords.google.com:443/api/adwords/v2/InfoService"
    Binding: api:InfoServiceSoapBinding SOAP style [document]
      Operation: getUnitCountForMethod
        Input: getUnitCountForMethodRequest
          (getUnitCountForMethod
            ("service" :STRING) 1 
            ("method" :STRING) 1 
            ("startDate" :DATE) 1 
            ("endDate" :DATE) 1 ) 1 
        Output: getUnitCountForMethodResponse
          (getUnitCountForMethodResponse
            ("getUnitCountForMethodReturn" :LONG) 1 ) 1 
      Operation: getUnitCount
        Input: getUnitCountRequest
          (getUnitCount
            ("startDate" :DATE) 1 
            ("endDate" :DATE) 1 ) 1 
        Output: getUnitCountResponse
          (getUnitCountResponse
            ("getUnitCountReturn" :LONG) 1 ) 1 
      Operation: getMethodCost
        Input: getMethodCostRequest
          (getMethodCost
            ("service" :STRING) 1 
            ("method" :STRING) 1 
            ("date" :DATE) 1 ) 1 
        Output: getMethodCostResponse
          (getMethodCostResponse
            ("getMethodCostReturn" :INT) 1 ) 1 
      Operation: getUsageQuotaThisMonth
        Input: getUsageQuotaThisMonthRequest
          ("getUsageQuotaThisMonth") 1
        Output: getUsageQuotaThisMonthResponse
          (getUsageQuotaThisMonthResponse
            ("getUsageQuotaThisMonthReturn" :LONG) 1 ) 1 
      Operation: getOperationsQuotaThisMonth
        Input: getOperationsQuotaThisMonthRequest
          ("getOperationsQuotaThisMonth") 1
        Output: getOperationsQuotaThisMonthResponse
          (getOperationsQuotaThisMonthResponse
            ("getOperationsQuotaThisMonthReturn" :LONG) 1 ) 1 
      Operation: getOperationCount
        Input: getOperationCountRequest
          (getOperationCount
            ("startDate" :DATE) 1 
            ("endDate" :DATE) 1 ) 1 
        Output: getOperationCountResponse
          (getOperationCountResponse
            ("getOperationCountReturn" :LONG) 1 ) 1 
The input and output characteristics of each operation are described in some detail. Example lisp template code of actual input and output are also given (along with multiplicity indication, whether elements are required, optional, can occur zero ore more times, or one or more times).

Given that you understand a particular operation, you can implement it quite easily.

(defun get-method-cost (service method &optional (date (ut)))
  (wsdl-soap-call (wsdl-cache-get "https://adwords.google.com:443/api/adwords/v2/InfoService?wsdl")
                  "getMethodCost"
                  :input `("getMethodCost" ("service" ,service "method" ,method "date" ,date))
                  :headers (make-google-headers)))
For the Google AdWords API, some headers are required. The method implemented above requires some parameters, specified to the framework as a structured alist. With debugging enabled (using (setf *debug-stream* *output-stream*)), executing the call looks as follows:
CL-SOAP 85 > (get-method-cost "InfoService" "getMethodCost")

;; SOAP CALL sending: 
<soapenv:Envelope 
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsd="http://www.w3.org/1999/XMLSchema" 
    xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" 
    xmlns:google="https://adwords.google.com/api/adwords/v2" 
    xmlns="https://adwords.google.com/api/adwords/v2">
  <soapenv:Header>
    <token>XXX</token>
    <useragent>cl-soap-testing</useragent>
    <password>XXX</password>
    <clientEmail>sven@beta9.be</clientEmail>
    <email>svc@mac.com</email>
  </soapenv:Header>
  <soapenv:Body>
    <getMethodCost>
      <service>InfoService</service>
      <method>getMethodCost</method>
      <date>2005-09-27</date>
    </getMethodCost>
  </soapenv:Body>
</soapenv:Envelope>

;; SOAP CALL receiving: 
<soapenv:Envelope 
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsd="http://www.w3.org/1999/XMLSchema" 
    xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance">
  <soapenv:Header>
    <responseTime 
        soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" 
        soapenv:mustUnderstand="0" 
        xmlns="https://adwords.google.com/api/adwords/v2">
      393
    </responseTime>
    <operations 
        soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" 
        soapenv:mustUnderstand="0" 
        xmlns="https://adwords.google.com/api/adwords/v2">
      1
    </operations>
    <units 
        soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" 
        soapenv:mustUnderstand="0" 
        xmlns="https://adwords.google.com/api/adwords/v2">
      1
    </units>
  </soapenv:Header>
  <soapenv:Body>
    <getMethodCostResponse xmlns="https://adwords.google.com/api/adwords/v2">
      <getMethodCostReturn>1</getMethodCostReturn>
    </getMethodCostResponse>
  </soapenv:Body>
</soapenv:Envelope>

("getMethodCostResponse" ("getMethodCostReturn" 1))

(("responseTime" 393) ("operations" 1) ("units" 1))
Some sensitive data fields have been marked with X's. XML was beautified a little bit ;-) Note that two values are returned: the actual value and the headers.

Example 2: Calling estimateKeywordList in the TrafficEstimatorService

This more complicated example is also described in the introduction to the Google AdWords API: estimating the traffic for keywords. Again, we first look at the interpreted WSDL:

CL-SOAP 43 > (describe-wsdl-soap (wsdl-cache-get "https://adwords.google.com:443/api/adwords/v2/TrafficEstimatorService?wsdl"))
WSDL Document Definitions
  Service: TrafficEstimatorService
    Port: TrafficEstimatorService
    SOAP Address Location "https://adwords.google.com:443/api/adwords/v2/TrafficEstimatorService"
    Binding: api:TrafficEstimatorServiceSoapBinding SOAP style [document]
      Operation: estimateKeywordList
        Input: estimateKeywordListRequest
          (estimateKeywordList
            (keywordRequests (
              ("id" :LONG) ? 
              ("type" :STRING) ? 
              ("text" :STRING) ? 
              ("maxCpc" :LONG) ? 
              ("negative" :BOOLEAN) ? ) + )) 1 
        Output: estimateKeywordListResponse
          (estimateKeywordListResponse
            (estimateKeywordListReturn (
              ("id" :LONG) ? 
              ("impressions" :INT) 1 
              ("ctr" :FLOAT) 1 
              ("cpc" :LONG) 1 
              ("avgPosition" :FLOAT) 1 
              ("notShownPerDay" :INT) 1 ) + )) 1 
      Operation: estimateAdGroupList
        Input: estimateAdGroupListRequest
          (estimateAdGroupList
            (adGroupRequests (
              ("id" :INT) ? 
              ("maxCpc" :LONG) ? 
              (keywordRequests (
                ("id" :LONG) ? 
                ("type" :STRING) ? 
                ("text" :STRING) ? 
                ("maxCpc" :LONG) ? 
                ("negative" :BOOLEAN) ? ) + )) + )) 1 
        Output: estimateAdGroupListResponse
          (estimateAdGroupListResponse
            (estimateAdGroupListReturn (
              ("id" :INT) ? 
              (keywordEstimates (
                ("id" :LONG) ? 
                ("impressions" :INT) 1 
                ("ctr" :FLOAT) 1 
                ("cpc" :LONG) 1 
                ("avgPosition" :FLOAT) 1 
                ("notShownPerDay" :INT) 1 ) + )) + )) 1 
      Operation: estimateCampaignList
        Input: estimateCampaignListRequest
          (estimateCampaignList
            (campaignRequests (
              ("id" :INT) ? 
              ("optInSearchNetwork" :BOOLEAN) ? 
              ("optInContentNetwork" :BOOLEAN) ? 
              (geoTargeting
                ("countries" (:STRING) * )
                ("regions" (:STRING) * )
                ("metros" (:STRING) * )
                ("cities" (:STRING) * )) ? 
              (languageTargeting
                ("languages" (:STRING) * )) ? 
              (adGroupRequests (
                ("id" :INT) ? 
                ("maxCpc" :LONG) ? 
                (keywordRequests (
                  ("id" :LONG) ? 
                  ("type" :STRING) ? 
                  ("text" :STRING) ? 
                  ("maxCpc" :LONG) ? 
                  ("negative" :BOOLEAN) ? ) + )) + )) + )) 1 
        Output: estimateCampaignListResponse
          (estimateCampaignListResponse
            (estimateCampaignListReturn (
              ("id" :INT) ? 
              (adGroupEstimates (
                ("id" :INT) ? 
                (keywordEstimates (
                  ("id" :LONG) ? 
                  ("impressions" :INT) 1 
                  ("ctr" :FLOAT) 1 
                  ("cpc" :LONG) 1 
                  ("avgPosition" :FLOAT) 1 
                  ("notShownPerDay" :INT) 1 ) + )) + )) + )) 1
Next we define our little lisp function:
(defun estimate-keyword-list (keywords)
  "((text type max-cpc)*) where type is Broad|Phrase|Exact"
  (wsdl-soap-call (wsdl-cache-get "https://adwords.google.com:443/api/adwords/v2/TrafficEstimatorService?wsdl")
                  "estimateKeywordList"
                  :input `("estimateKeywordList"
                           ("keywordRequests"
                            ,(mapcar #'(lambda (keyword)
                                         (destructuring-bind (text type max-cpc)
                                             keyword
                                           `("text" ,text "type" ,type "maxCpc" ,max-cpc)))
                                     keywords)))
                  :headers (make-google-headers)))
Now we can test it, passing multiple requests and receiving multiple responses:
CL-SOAP 192 > (estimate-keyword-list '(("flowers" "Broad" 50000) ("tree" "Broad" 50000)))

;; SOAP CALL sending: 
<soapenv:Envelope 
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsd="http://www.w3.org/1999/XMLSchema" 
    xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" 
    xmlns:google="https://adwords.google.com/api/adwords/v2" 
    xmlns="https://adwords.google.com/api/adwords/v2">
  <soapenv:Header>
    <token>XXX</token>
    <useragent>cl-soap-testing</useragent>
    <password>XXX</password>
    <clientEmail>sven@beta9.be</clientEmail>
    <email>svc@mac.com</email>
  </soapenv:Header>
  <soapenv:Body>
    <estimateKeywordList>
      <keywordRequests>
        <type>Broad</type>
        <text>flowers</text>
        <maxCpc>50000</maxCpc>
      </keywordRequests>
      <keywordRequests>
        <type>Broad</type>
        <text>tree</text>
        <maxCpc>50000</maxCpc>
      </keywordRequests>
    </estimateKeywordList>
  </soapenv:Body>
</soapenv:Envelope>

;; SOAP CALL receiving: 
<soapenv:Envelope 
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsd="http://www.w3.org/1999/XMLSchema" 
    xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance">
  <soapenv:Header>
    <responseTime 
        soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" 
        soapenv:mustUnderstand="0" 
        xmlns="https://adwords.google.com/api/adwords/v2">
      412
    </responseTime>
    <operations 
        soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" 
        soapenv:mustUnderstand="0" 
        xmlns="https://adwords.google.com/api/adwords/v2">
      2
    </operations>
    <units 
        soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" 
        soapenv:mustUnderstand="0" 
        xmlns="https://adwords.google.com/api/adwords/v2">
      50
    </units>
  </soapenv:Header>
  <soapenv:Body>
    <estimateKeywordListResponse xmlns="https://adwords.google.com/api/adwords/v2">
      <estimateKeywordListReturn>
        <avgPosition>4.537519</avgPosition>
        <cpc>50000</cpc>
        <ctr>0.01547993</ctr>
        <id>-1</id>
        <impressions>7263</impressions>
        <notShownPerDay>288532</notShownPerDay>
      </estimateKeywordListReturn>
      <estimateKeywordListReturn>
        <avgPosition>2.8274193</avgPosition>
        <cpc>50000</cpc>
        <ctr>0.012525387</ctr>
        <id>-1</id>
        <impressions>18177</impressions>
        <notShownPerDay>397941</notShownPerDay>
      </estimateKeywordListReturn>
    </estimateKeywordListResponse>
  </soapenv:Body>
</soapenv:Envelope>

("estimateKeywordListResponse" 
 ("estimateKeywordListReturn" 
  (("id" -1 "impressions" 7263 "ctr" 0.01547993 "cpc" 50000 "avgPosition" 4.537519 "notShownPerDay" 288532) 
   ("id" -1 "impressions" 18177 "ctr" 0.012525387 "cpc" 50000 "avgPosition" 2.8274193 "notShownPerDay" 397941))))

(("responseTime" 412) ("operations" 2) ("units" 50))
That is all for now! More example and testing code can be found in the project's source tree.

$Id: google-adwords-api.html,v 1.6 2005/10/03 12:28:26 scaekenberghe Exp $