Chapter 2
UPDATED: 10/19/2008 I have updated this for easier viewing of the recipes, and I have also added more "Chapters" to this Mini Cookbook. See below for those links to the "Chapters".
- AMFPHP Recipes
- Web Service Recipes
Air RecipesCairngorm Recipes
Introduction
We are going to take alook at the different ways for a Flex or Air application can use to access server side services. Each recipe has a problem and a solution, followed by a discussion of what happened and how we did what we did.
Table of Contents
Web Service Recipes
- Recipe 2.1 - Getting Started with XML-RPC
- Recipe 2.2 - Requests with XML-RPC
- Recipe 2.3 - Authentication and XML-RPC
- Recipe 2.4 - Getting Started with REST
- Recipe 2.5 - REST Service Class
Recipe 2.1 - Getting Started with XML-RPC
Problem: You want to connect to a XML-RPC enabled weblog to get you posts or even just to connect.
Solution: Pick up this RPC ActionScript 3 library from Google code, you have to fix one thing inside one of the classes its on the Wiki, its a quick fix that we have to do because they haven’t updated it yet, and you will be ready to go. Here a simple call to WordPress’s XML-RPC.
Since we are going to make some calls to WordPress lets take a look at one method that we are going to call, examine it and see how we can make this call from Flex.
Below is the "blogger.getUserInfo" method, the method says that it excepts 2 parameters, a username and a password. Here is the function.
1: /* blogger.getUsersInfo gives your client some info about you, so you don't have to */
2: function blogger_getUserInfo($args) {
3:
4: $this->escape($args);
5:
6: $user_login = $args[1];
7: $user_pass = $args[2];
8:
9: if (!$this->login_pass_ok($user_login, $user_pass)) {
10: return $this->error;
11: }
12:
13: set_current_user( 0, $user_login );
14: if( !current_user_can( 'edit_posts' ) )
15: return new IXR_Error( 401, __( 'Sorry, you do not have access to user data on this blog.' ) );
16:
17: do_action('xmlrpc_call', 'blogger.getUserInfo');
18:
19: $user_data = get_userdatabylogin($user_login);
20:
21: $struct = array(
22: 'nickname' => $user_data->nickname,
23: 'userid' => $user_data->ID,
24: 'url' => $user_data->user_url,
25: 'lastname' => $user_data->last_name,
26: 'firstname' => $user_data->first_name
27: );
28:
29: return $struct;
30: }
But if we use this code below to call this method from Flex, to the average Joe it looks ok right?
1: /**
2: * blogger.getUserInfo
3: *
4: * @args username password
5: */
6: private function getUserInfo():void
7: {
8: service.call( "blogger.getUserInfo", txt_username.text,
9: txt_password.text );
10: }
Wrong, we have to insert some jumble code I guess before our username and password for the call to actually work. I think it has something to do with blogger API using and apikey before or something.
So we use this code instead.
1: /**
2: * blogger.getUserInfo
3: *
4: * @args username password
5: */
6: private function getUserInfo():void
7: {
8: service.call( "blogger.getUserInfo", "0000000", txt_username.text,
9: txt_password.text );
10: }
For WordPress methods they are straight forward, take this for example to get the users blogs:
Discussion: Now that we have figured out why the blogger methods weren’t working we can start creating a class to be able to call all of the methods that are defined inside the XML-RPC script on our server. I’ll leave that to you though.
Recipe 2.2 - Requests with XML-RPC
Problem: You want to authenticate the user before making calls, well you just want to send over some arguments and stuff.
Solution: Create some forms and code up some new functions to do exactly what you need.
Here are some functions that will get you prepared to send and receive data.
1: //This holds our data
2: [Bindable] private var returnedData:ArrayCollection;
3:
4: //Wordpress URL
5: [Bindable] private var wordpressEndpoint:String = "http://localhost/wordpress";
6:
7: //Our Service variable that is a xmlrpc object
8: private var service:XMLRPCObject;
9:
10: private function init():void
11: {
12: //Create a new service xmlrpc object
13: service = new XMLRPCObject();
14:
15: //Specify the endpoint
16: service.endpoint = wordpressEndpoint;
17:
18: //We know that the destination is /xmlrpc.php
19: service.destination = "/xmlrpc.php";
20:
21: //Set a fault handler
22: service.addEventListener( FaultEvent.FAULT, onFault );
23:
24: //Set a result handler
25: service.addEventListener( ResultEvent.RESULT, onResult );
26: }
27:
28: /**
29: * wp.getUsersBlogs
30: *
31: * @args username password
32: */
33: private function getUsersBlogs():void
34: {
35: service.call( "wp.getUsersBlogs", txt_username.text,
36: txt_password.text );
37: }
38:
39: /**
40: * blogger.getRecentPosts
41: *
42: * @args blog_id, username, password, how many
43: */
44: private function getRecentPosts():void
45: {
46: service.call( "blogger.getRecentPosts", "00000000", txt_blogid.value,
47: txt_username.text,
48: txt_password.text,
49: txt_howmany.value );
50: }
51: //Result handler
52: private function onResult( w_result:ResultEvent ):void
53: {
54: returnedData = new ArrayCollection( ArrayUtil.toArray( w_result.result ) );
55:
56: //Trace
57: trace( w_result.result );
58: }
59:
60: //Fault handler
61: private function onFault( w_fault:FaultEvent ):void
62: {
63: Alert.show( w_fault.fault.faultString, w_fault.fault.faultCode );
64:
65: //Trace
66: trace( w_fault.fault.faultDetail );
67: }
And here is the view that will make the calls and show the data.
1: <mx:Form>
2:
3: <!--Wordpress Username-->
4: <mx:FormItem label="Username:"
5: required="true">
6: <mx:TextInput id="txt_username"/>
7: </mx:FormItem>
8:
9: <!--Wordpress Password-->
10: <mx:FormItem label="Password:"
11: required="true">
12: <mx:TextInput id="txt_password"
13: displayAsPassword="true"/>
14: </mx:FormItem>
15:
16: <!--Wordpress Submit Credentials-->
17: <mx:FormItem>
18: <mx:Button label="Check"
19: click="getUserInfo()"/>
20: </mx:FormItem>
21:
22: <mx:FormHeading label="Recent Posts"/>
23:
24: <!--Wordpress Blog ID, usually 1-->
25: <mx:FormItem label="Blog ID:"
26: required="true">
27: <mx:NumericStepper id="txt_blogid"/>
28: </mx:FormItem>
29:
30: <!--Wordpress, How Many Posts?-->
31: <mx:FormItem label="How Many:"
32: required="true">
33: <mx:NumericStepper id="txt_howmany"
34: maximum="25"
35: minimum="1"/>
36: </mx:FormItem>
37:
38: <mx:FormItem>
39: <mx:Button label="Get Recent Posts"
40: click="getRecentPosts()"/>
41: </mx:FormItem>
42: </mx:Form>
43:
44: <mx:HBox width="100%">
45: <mx:Button label="Get Users Blogs"
46: click="getUsersBlogs()"/>
47: </mx:HBox>
48:
49: <mx:DataGrid id="dg_data"
50: width="100%"
51: height="100%"
52: dataProvider="{ returnedData }"/>
Discussion: Now that you got the hang of things, you can start making a full service class that will allow creating and editing posts, pages, and categories.
Recipe 2.3 - Authentication and XML-RPC
Problem: Ok I got the hang of this XML-RPC thing, but what if my server requires authentication before making any requests for data? What am I suppose to do?
Solution: Simple, we will slightly modify the calls to our service by adding authentication headers with the call, encrypting the username and password with every request on the server, supplying its requirements and receiving our data.
We are going to be using the same as3rpc library as before and for the service we are going to be using the big social networking site Buzznet. You need an apikey to make calls so grab one and you will be set.
First add a xmlrpc object to your view like this.
1: <xmlrpc:XMLRPCObject id="buzznetSrv"
2: destination="xmlrpc/?key={ apikey }"
3: endpoint="http://bnedit.buzznet.com/interface/"
4: fault="onFault( event )"
5: result="onResult( event )"/>
Here is how to set up the authentication for the calls.
1: /* Authenticate the User */
2: private function doLogin():void
3: {
4: var encode:Base64Encoder = new Base64Encoder();
5: encode.encode( txt_username.text + ":" + txt_password.text );
6:
7: buzznetSrv.method = "POST";
8: buzznetSrv.contentType = "application/xml";
9: buzznetSrv.useProxy = false;
10: buzznetSrv.headers = { Authorization: "Basic " + encode, Accept: "application/xml" };
11: buzznetSrv.call( "buzznet.getOnlineNow" );
12: }
What we do here is first create a variable that is type Base64Encoder and then we tell it to encode the values from the username input plus a ":" and then the values of the password input. This makes one string with a username password seperated by a colon to add to our header.
Then we set up the method, the content type, and create the header, we then set the Authorization: "Basic" with our encoded credentials to the header. Finishing it off by sending a request to get online users. And we do get a response and this is what we do with that.
1: //Data holder for the requests
2: [Bindable] private var dataCollection:ArrayCollection;
3:
4: //Hides loading and creates a new arraycollection from the result
5: private function onResult( event:ResultEvent ):void
6: {
7: //Create a new arraycollection from the result
8: dataCollection = new ArrayCollection( ArrayUtil.toArray( event.result ) );
9: }
And now to show the data.
1: <mx:VBox width="225" height="100%" styleName="paddingBox">
2:
3: <!--Buzznet Username-->
4: <mx:Label text="Username:"
5: fontWeight="bold"/>
6: <mx:TextInput id="txt_username"
7: width="100%"
8: text=""/>
9:
10: <!--Buzznet Password-->
11: <mx:Label text="Password:"
12: fontWeight="bold"/>
13: <mx:TextInput id="txt_password"
14: displayAsPassword="true"
15: width="100%"
16: text=""/>
17:
18: <!--Authenticate Button-->
19: <mx:Button
20: label="Authenticate"
21: click="doLogin()"/>
22:
23: </mx:VBox>
24:
25: <mx:DataGrid id="dg_data"
26: width="100%"
27: height="100%"
28: dataProvider="{ dataCollection }"/>
Discussion: Wow we just about can do anything now, XML-RPC, authentication, man we are on top of the world right now!
Recipe 2.4 - Getting Started with REST
Problem: I want to make some RESTful calls to an API, but I don’t know where to start.
Solution: Create a function to test our REST calls to the 30boxes API and see what happens.
All calls to 30boxes must have a APIKEY, also for user data you must send a token that the user has been given allowing your application to access the users data. We will take care of that next.
First lets call one of there sample methods "test.Ping".
We need to set up our calls and make it easy to send out requests so lets use the following.
1: private var service:HTTPService;
2: private var baseURL:String = "http://30boxes.com/api/api.php?";
3:
4: private function init():void
5: {
6: service = new HTTPService();
7: }
8:
9: /**
10: * Helper for sending our queries
11: *
12: * @param aURI the built url to send
13: * @param aResultHandler a result handler for the call
14: *
15: */
16: private function sendQuery( aURI:URI, aResultHandler:Function ):void
17: {
18: service.contentType = HTTPService.CONTENT_TYPE_XML;
19:
20: service.addEventListener( ResultEvent.RESULT, aResultHandler );
21: service.addEventListener( FaultEvent.FAULT, onFault );
22:
23: service.url = aURI.toString();
24: service.send( );
25: }
26:
27: /**
28: * Test the API
29: *
30: * @param aApiKey
31: *
32: */
33: public function testPing( aApiKey:String ):void
34: {
35: var theURL:URI = new URI( baseURL );
36: theURL.setQueryValue( "method", "test.Ping" );
37: theURL.setQueryValue( "apiKey", aApiKey );
38:
39: sendQuery( theURL, onResult_testPing );
40: trace( theURL.toString() );
41: }
42:
43: private function onResult_testPing( t_result:ResultEvent ):void
44: {
45: trace( t_result.result as XML );
46: }
Discussion: Ok we start out by setting up our request the we create a function called sendQuery, and what this function does is it takes the service variable and builds it up so that when other functions call on this sendQuery function the passed arguments are being used to send the query and handle the result.
When we create the testPing function we are requiring one argument which is a apikey and then creating a variable type URI so we can set up name/value pairs for the call. We declare the method and the name and the "test.Ping" as the value, then since this method only requires an apikey parameter we set up that name/value pair with "apiKey" and the value aApiKey.
Once we have that set up we call the sendQuery function and filling the arguments with our URI and a result function.
Recipe 2.5 - REST Service Class
Problem: We are set up for making calls, and familiar with REST. Now can we see how to make more calls to 30boxes.
Solution: We will create a small class that will be the container for all of our calls we want to make to 30boxes.
Create a new class called T30boxesResultEvent.as enter the following
1: package com.jonniespratley.webapis.T30boxes.events
2: {
3: import mx.messaging.messages.IMessage;
4: import mx.rpc.AsyncToken;
5: import mx.rpc.events.ResultEvent;
6:
7:
8: public class T30boxesResultEvent extends ResultEvent
9: {
10: static public const OK:String = "ok";
11: static public const TOKEN_LOADED:String = "TOKEN_LOADED";
12: static public const AUTHORIZED:String = "AUTHORIZED";
13: static public const USER_LOADED:String = "USER_LOADED";
14: static public const EVENTS_RESULT:String = "EVENTS_RESULT";
15: static public const TODOS_RESULT:String = "TODOS_RESULT";
16:
17: public function T30boxesResultEvent( type:String,
18: bubbles:Boolean=false,
19: cancelable:Boolean=true,
20: result:Object=null,
21: token:AsyncToken=null,
22: message:IMessage=null )
23: {
24: super( type, bubbles, cancelable, result, token, message );
25: }
26: }
27: }
This result even class is what is going to get dispatched when we receive a result from the server and we will pass our data along with the event.
Now we need to create our fault class, here is the code for that.
T30boxesFaultEvent.as:
1: package com.jonniespratley.webapis.T30boxes.events
2: {
3: import mx.messaging.messages.IMessage;
4: import mx.rpc.AsyncToken;
5: import mx.rpc.Fault;
6: import mx.rpc.events.FaultEvent;
7:
8: public class T30boxesFaultEvent extends FaultEvent
9: {
10: static public const FAIL:String = "fail";
11: static public const USER_FAULT:String = "USER_FAULT";
12: static public const EVENTS_FAULT:String = "EVENTS_FAULT";
13: static public const TODOS_FAULT:String = "TODOS_FAULT";
14:
15: public function T30boxesFaultEvent( type:String,
16: bubbles:Boolean=false,
17: cancelable:Boolean=true,
18: fault:Fault=null,
19: token:AsyncToken=null,
20: message:IMessage=null )
21: {
22: super( type, bubbles, cancelable, fault, token, message );
23: }
24: }
25: }
I like using interfaces so lets create a new interface called IT30boxesService.as
1: package com.jonniespratley.webapis.T30boxes
2: {
3: public interface I30boxesService
4: {
5: function testPing( aApiKey:String ):void
6:
7: function userAuthorize( aApiKey:String,
8: aApplicationName:String,
9: aApplicationLogo:String,
10: aReturnUrl:String = null ):String
11:
12: function userGetAllInfo( aApiKey:String,
13: aAuthorizedToken:String ):void
14: }
15: }
Our interface is just going to hold the functions that we want to call, this will be a very small interface for this example, but when we choose to extend the service class we must change the interface before the class because when a class implements a interface it basically signs a contract to include all of the functions defined inside of the interface. But the interface holds no functionality, its like guidelines for classes that choose to implement it.
Functions Explained
- testPing; This function requires an API Key before sending
- userAuthorize; This function returns a string, in that string will be all of the necessary elements to redirect your use to 30boxes.com were they will get a token. This token basically says ok I know that this application is accessing my data.
- userGetAllInfo; This function requires an API Key and the token from the user, returning all information about that user who’s token was sent.
Then we need to an service class that will implement our interface. This class is where we bring functionality to the functions we defined inside our interface.
Here is a portion of our T30boxesService.as:
1: package com.jonniespratley.webapis.T30boxes
2: {
3: import com.adobe.net.URI;
4: import com.jonniespratley.webapis.T30boxes.events.*;
5: import flash.events.EventDispatcher;
6:
7: import mx.rpc.AsyncToken;
8: import mx.rpc.events.FaultEvent;
9: import mx.rpc.events.ResultEvent;
10: import mx.rpc.http.HTTPService;
11:
12: /**
13: * 30boxes.com API Service Class. This class is a starting point for
14: * the 30boxes.com AS3 Library. It provides most of the calls that
15: * 30boxes supports with there REST API.
16: *
17: * @langversion ActionScript 3.0
18: * @playerversion Flash 9
19: * @author Jonnie
20: *
21: */
22: public class T30boxesService extends EventDispatcher implements I30boxesService
23: {
24: private var service:HTTPService;
25: private var baseURL:String = "http://30boxes.com/api/api.php?";
26:
27: //Authorize Application Info
28: public var APP_NAME:String = "";
29: public var APP_LOGO:String = "";
30: public var APP_RETURN_URL:String = "";
31:
32: public function T30boxesService()
33: {
34: service = new HTTPService();
35: }
36:
37: /**
38: * Helper for sending our queries
39: *
40: * @param aURI the built url to send
41: * @param aResultHandler a result handler for the call
42: *
43: */
44: private function sendQuery( aURI:URI, aResultHandler:Function ):void
45: {
46: service.contentType = HTTPService.CONTENT_TYPE_XML;
47: service.resultFormat = HTTPService.RESULT_FORMAT_E4X;
48:
49: service.addEventListener( ResultEvent.RESULT, aResultHandler );
50: service.addEventListener( FaultEvent.FAULT, onFault );
51:
52: service.url = aURI.toString();
53: service.send( );
54: var token:AsyncToken = service.send();
55: }
56:
57: /**
58: * Test the API
59: *
60: * @param aApiKey
61: *
62: */
63: public function testPing(aApiKey:String):void
64: {
65: var theURL:URI = new URI( baseURL );
66: theURL.setQueryValue( "method", "test.Ping" );
67: theURL.setQueryValue( "apiKey", aApiKey );
68:
69: sendQuery( theURL, onResult_testPing );
70: trace( theURL.toString() );
71: }
72:
73: /**
74: * Unlike all other methods that return XML, this URL presents the user with a
75: * message asking them if they want to give you permission for your application to
76: * access their data. They may be required to log in first. If they choose to
77: * give permission, we will provide (through the returnUrl, or a form in your application)
78: * an authorizedUserToken that you need when accessing their data. This token will not
79: * expire for a given user, so you may wish to store it.
80: *
81: * Note that you cannot authorize a specific user; you are getting authorization to
82: * access whomever logs in. Upon success, we tell you their identification.
83: *
84: * @param aApiKey
85: * @param aApplicationName - the name of your application, up to 150 characters
86: * @param aApplicationLogo - the url-encoded location of your logo
87: * @param aReturnUrl - the url-encoded location where we will return to with a user token.
88: * All web-based applications should provide a URL. If it is not provided (which would be the
89: * case for client applications), we will instruct the user to copy the token to your application
90: *
91: * @return - built url for redirection
92: *
93: */
94: public function userAuthorize( aApiKey:String,
95: aApplicationName:String,
96: aApplicationLogo:String,
97: aReturnUrl:String=null):String
98: {
99: var theURL:URI = new URI( baseURL );
100: theURL.setQueryValue( "method", "user.Authorize" );
101: theURL.setQueryValue( "apiKey", aApiKey );
102: theURL.setQueryValue( "applicationName", aApplicationName );
103: theURL.setQueryValue( "applicationLogoUrl", aApplicationLogo );
104: if ( aReturnUrl != null )
105: {
106: theURL.setQueryValue( "returnUrl", aReturnUrl );
107: }
108:
109: return theURL.toString();
110: trace( theURL.toString() );
111: }
112:
113: /**
114: * This method returns private contact info and a list of the user's buddies.
115: *
116: * @param aApiKey
117: * @param aAuthorizedToken
118: *
119: */
120: public function userGetAllInfo(aApiKey:String, aAuthorizedToken:String):void
121: {
122: var theURL:URI = new URI( baseURL );
123: theURL.setQueryValue( "method", "user.GetAllInfo" );
124: theURL.setQueryValue( "apiKey", aApiKey );
125: theURL.setQueryValue( "authorizedUserToken", aAuthorizedToken );
126:
127: sendQuery( theURL, onResult_data );
128: }
129:
130: private function onResult_data( t_result:ResultEvent ):void
131: {
132: dispatchEvent( new T30boxesResultEvent(
133: T30boxesResultEvent.OK, false, false, t_result.result, null, null ) );
134: }
135:
136: /**
137: * Dispatchs a T30boxes fault event
138: * @param t_fault
139: *
140: */
141: private function onFault( t_fault:FaultEvent ):void
142: {
143: trace( t_fault.fault );
144: dispatchEvent( new T30boxesFaultEvent(
145: T30boxesFaultEvent.FAIL, false, false, t_fault.fault, t_fault.token, t_fault.message ) );
146: }
147: }
148: }
This class is the holder of some methods that are on 30boxes.com, so we are bundling them up into a class for easy calling. When a function is called, we wait for a result or fault from the call, once we get that result, we dispatch one of our event classes depending on what the result was. If it was successful we dispatch a "T30boxesResultEvent.ok" we are letting whoever is listing for this event know that it happened and here is the data.
So when it comes to putting this all together to make it work, we can create a simple API Tester component that we can try out our new services. Here is the code for APITester.mxml:
1: <mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml"
2: creationComplete="init()"
3: width="100%"
4: height="100%">
5: <mx:Script>
6: <![CDATA[
7: import mx.utils.ArrayUtil;
8: import com.jonniespratley.webapis.T30boxes.events.T30boxesResultEvent;
9: import mx.collections.ArrayCollection;
10: import mx.controls.Alert;
11: import com.jonniespratley.webapis.T30boxes.T30boxesService;
12:
13: private var service:T30boxesService;
14: [Bindable] private var resultData:ArrayCollection;
15:
16: //Creates a new service
17: private function init():void
18: {
19: service = new T30boxesService();
20: }
21:
22: //Authorize the user
23: private function authorizeUser():void
24: {
25: navigateToURL( new URLRequest( service.userAuthorize(
26: txt_apikey.text,
27: "30Boxes of Air",
28: "http://jonniespratley.com/images/air_boxes_logo.png",
29: null ) ) );
30:
31: Alert.show( "Please click 'OK' once you have authorized this application on 30boxes.com.",
32: "30Boxes Authorization" );
33: }
34:
35: //Gets the Users info
36: private function getInfo():void
37: {
38: service.userGetAllInfo( txt_apikey.text, txt_token.text );
39: service.addEventListener( T30boxesResultEvent.USER_LOADED, onResult );
40: }
41:
42: //Handles the result
43: private function onResult( event:T30boxesResultEvent ):void
44: {
45: trace( "Got Event" );
46: resultData = new ArrayCollection( ArrayUtil.toArray( event.result ) );
47: }
48:
49:
50: ]]>
51: </mx:Script>
52:
53: <mx:ApplicationControlBar width="100%">
54: <mx:VBox width="100%">
55: <mx:HBox width="100%">
56:
57: <!--User Token-->
58: <mx:Label text="User Token:"
59: fontWeight="bold" width="75"/>
60: <mx:TextInput id="txt_token"
61: width="100%"
62: text=""/>
63:
64: </mx:HBox>
65: <mx:HBox width="100%">
66:
67: <!--Developers API Key-->
68: <mx:Label text="API Key:"
69: fontWeight="bold" width="75"/>
70: <mx:TextInput id="txt_apikey"
71: width="100%"
72: text="7613785-2b78f23f6143a741e1ee7445447766b5"/>
73:
74: </mx:HBox>
75: </mx:VBox>
76:
77: <!--Get Info Button-->
78: <mx:Button label="Get My Info"
79: click="getInfo()"/>
80: <!--Launches Web browser to get token-->
81: <mx:Button label="Authorize"
82: click="authorizeUser()"/>
83:
84: </mx:ApplicationControlBar>
85:
86: <mx:DataGrid
87: width="100%"
88: height="100%"
89: dataProvider="{ resultData }">
90: <mx:columns>
91:
92: <mx:DataGridColumn
93: headerText="Avatar"
94: dataField="avatar">
95: <mx:itemRenderer>
96: <mx:Component>
97: <mx:Image source="{ data.avatar }"/>
98: </mx:Component>
99: </mx:itemRenderer>
100: </mx:DataGridColumn>
101:
102: <mx:DataGridColumn
103: headerText="First Name"
104: dataField="firstName"/>
105:
106: <mx:DataGridColumn
107: headerText="Last Name"
108: dataField="lastName"/>
109:
110: <mx:DataGridColumn
111: headerText="Time Zone"
112: dataField="timeZone"/>
113:
114: </mx:columns>
115: </mx:DataGrid>
116:
117: </mx:VBox>
I have included my API Key for you to test it out, please don’t abuse it :(. But this should have you up and running with some good calls to 30boxes.com API.
Discussion: That wasn’t to bad was it, now we are going to have to create a function to parse that XML data coming back. That is where you help me and show me how you do that, because I simply can’t figure it out.