Wunderlist is one of the most popular task management and list keeping applications out there. It allows you to create lists to which you can then add tasks that are easily checkable and have all sorts of functionality built into them. It has native apps for all the major OS’s and all your data syncs effectively over the cloud. It’s very nice to work with if you are trying to keep yourself organized and you can get a lot done even with the free version.

Winderlist logo

Nowadays, most web apps out there provide APIs for developers to integrate with their own applications (even free ones like Trello). In 2015, it finally happened for Wunderlist as well: their API was made public and thoroughly documented.

In this article, we will look at their API and see how we can interact with a Wunderlist application programatically. To this end, we will start a very small PHP app without any kind of framework behind it, just some raw PHP files brought together by Composer for testing purposes. And since there is no official PHP SDK, we’ll make use of Guzzle to perform HTTP requests to the various endpoints.

In addition, if you are curious, check out this demo repository which contains some basic code that allows you to load up all the tasks of a given list and then check them off one by one using Ajax. Similar to the core functionality of Wunderlist, but again without any kind of framework or styling.

The playground

Inside the root folder of our project we can have a composer.json file like this:

{
    "require": {
        "php": ">=5.5.0",
        "guzzlehttp/guzzle": "~6.0"
    },

    "autoload": {
        "psr-4": {
            "Wunderlist\\": "src/"
        }
    },
    
    "require-dev": {
        "symfony/var-dumper": "~2.7"
    }
}

As you can see, we will use Guzzle 6 which is PSR-7 compliant, and any classes that we want to create will go into the src/ folder which is namespaced as Wunderlist. We can also go ahead and create this folder. Lastly, we are using the Symfony VarDumper component to print out variables to the screen while developing.

Still within the project’s root folder, let’s add three files: index.php, keys.php, .gitignore.

Inside index.php we will play with the API. We’ll use keys.php to store any access tokens and it will be referenced in the .gitignore file so that it doesn’t get committed to the repository.

.gitignore should look like this as a starter:

vendor/*
keys.php

Feel free to expand on it with rules from this one.

The Wunderlist application

Before getting into working with the API, we’ll need to go to our Wunderlist account and create a new application. When doing so, we’ll need to specify an App URL and an App callback URL which will be used by the OAuth flow that lets users grant access to your application. We won’t be going into this here so we can just add some dummy URLs.

However, to authenticate with the application, we’ll need to click on the Create access token button which will generate an admin access token that can be used to authenticate with the API as our own user (in effect, we’re now building a Wunderlist app for ourselves).

wunderlist application example

The Wunderlist class

Once the application is saved, we can head back to our PHP app and paste the following into the keys.php file:

<?php

$client_id = 'your-client-id';
$access_token = 'the-access-token-you-generated-earlier';

The information above can be retrieved from your Wunderlist account. We will then require this file inside index.php and use these variables globally. This is not a best practice but it will do for our little demonstration in order to keep things short and simple. For a better solution to the problem of loading credentials, you can check out this approach instead.

Next, let’s create a class called WunderlistClient inside the src/ folder of our app:

<?php

namespace Wunderlist;

use GuzzleHttp\ClientInterface;
use Psr\Http\Message\ResponseInterface;

class WunderlistClient {

    /**
     * @var ClientInterface
     */
    private $client;

    /**
     * Constructor
     *
     * @param ClientInterface $client
     */
    public function __construct(ClientInterface $client) {
        $this->client = $client;
    }

All our class does for now is take the Guzzle client as a parameter and set it to a private variable. We will then use this class to make calls to the Wunderlist API by adding methods responsible for various endpoints and tasks.

Back in our index.php file we can have this for now:

<?php

require 'vendor/autoload.php';
require_once 'keys.php';

use GuzzleHttp\Client;
use Wunderlist\WunderlistClient as Wunderlist;

$guzzle = new Client(
    [
        'base_uri' => 'https://a.wunderlist.com/api/v1/',
        'headers' => [
            'Content-Type' => 'application/json',
            'X-Client-ID' => $client_id,
            'X-Access-Token' => $access_token,
        ]
    ]
);
$wunderlist = new Wunderlist($guzzle);

We start by instantiating a new Guzzle client. We set the base_uri of the API and some necessary headers (most importantly for authentication). The JSON content type header is needed when sending data as JSON in the request body. Then, we create a new instance of our WunderlistClient class and pass the Guzzle client to it.

Next, we will use the $wunderlist object to make the requests and return responses to be inspected.

Returning data using the Wunderlist API

First, let’s request all the lists on our account. Inside the WunderlistClient class we can add this public method:

/**
 * Returns all the lists
 *
 * @return array
 */
public function getLists() {
    $response = $this->client->get('lists');
    $this->checkResponseStatusCode($response, 200);
    return json_decode($response->getBody(), true);
}

We use Guzzle to make a request to the relevant endpoint and use a private method called checkResponseStatusCode() to verify that it has been successful. The latter will throw an exception if that’s not the case which can be caught by the caller (we’ll see in a moment).

Additionally, you’ll notice that with Guzzle 6 we no longer have a method for decoding the response from JSON but we are rather dealing with a Stream as the response body. And since the expected data is not so big, we can just dump it to a string with its own __toString() method and then decode it.

Let’s also take a quick look at our checkResponseStatusCode() method:

/**
 * Check the response status code.
 *
 * @param ResponseInterface $response
 * @param int $expectedStatusCode
 *
 * @throws \RuntimeException on unexpected status code
 */
private function checkResponseStatusCode(ResponseInterface $response, $expectedStatusCode)
{
    $statusCode = $response->getStatusCode();

    if ($statusCode !== $expectedStatusCode) {
        throw new \RuntimeException('Wunderlist API returned status code ' . $statusCode . ' expected ' . $expectedStatusCode);
    }
}

Once all this is in place, we can call getLists() from index.php and dump out all the contents of the result:

try {
  $lists = $wunderlist->getLists();
  dump($lists);
}
catch(\Exception $exception) {
  dump($exception);
}

Loading the file in the browser should allow us to inspect all the information returned about all the lists in our account. Neat.

Once we have all of our lists and figure out the ID of one we want, we can return more information about it:

/**
 * Returns a specific list
 *
 * @param int $id
 *
 * @return mixed
 */
public function getList($id) {
    if (!is_numeric($id)) {
        throw new \InvalidArgumentException('The list id must be numeric.');
    }

    $response = $this->client->get('lists/' . $id);
    $this->checkResponseStatusCode($response, 200);
    return json_decode($response->getBody(), true);
}

Here we do a basic check to see if the ID is numeric, make sure the request was successful and return the JSON decoded response from the Wunderlist API. Now, inside index.php:

try {
  $list = $wunderlist->getList($id);
  dump($list);
}
catch(\Exception $exception) {
  dump($exception);
}

We should see some information about the list itself.

But what if we want to load all the tasks in this list? We can implement something like this:

/**
 * Return all the tasks of a given list
 *
 * @param int $list_id
 *
 * @return array()
 */
public function getListTasks($list_id) {
    if (!is_numeric($list_id)) {
        throw new \InvalidArgumentException('The list id must be numeric.');
    }

    $response = $this->client->get('tasks', ['query' => ['list_id' => $list_id]]);
    $this->checkResponseStatusCode($response, 200);
    return json_decode($response->getBody());
}

In order to specify for which list we’d like the tasks returned, we need to pass its ID as a query parameter to the GET request. The rest is about the same as before. Inside index.php we can debug all the data related to the (uncompleted) tasks from this list:

try {
  $tasks = $wunderlist->getListTasks($id);
  dump($tasks);
}
catch(\Exception $exception) {
  dump($exception);
}

Creating and updating data with the Wunderlist API

All the requests we’ve made so far have been GET calls to load information. Let’s try making a few by which we create and update resources using the API. To start, we can create a task like this:

/**
 * Creates a new task
 *
 * @param string $name
 * @param int $list_id
 * @param array $task
 *
 * @return mixed
 */
public function createTask($name, $list_id, $task = []) {
    if (!is_numeric($list_id)) {
        throw new \InvalidArgumentException('The list id must be numeric.');
    }
    $task['name'] = $name;
    $task['list_id'] = $list_id;
    $response = $this->client->post('tasks', ['body' => json_encode($task)]);
    $this->checkResponseStatusCode($response, 201);
    return json_decode($response->getBody());
}

The data of the new task needs to be sent in the POST request body as JSON. To use this method inside index.php, we can do something like this:

try {
  $created = $wunderlist->createTask('My new task', 'the-list-id');
  dump($created);
}
catch(\Exception $exception) {
  dump($exception);
}

These are the only two mandatory parameters that need to exist in the task we send to Wunderlist but we can of course add more inside an array passed as the third parameter to our method. Feel free to check out the relevant documentation page for more information.

If all goes well, Wunderlist sends back a status of 201 (Created). And what we are returning here is an array of information related to the task we just created. This is what we can inspect in the browser if we load up index.php.

In order to now update the task we created, we need to send a PATCH request to its endpoint with only the data that we want changed and the current revision (which is to ensure different apps don’t overwrite each other’s data). So by sending the boolean TRUE for the completed property of the task, we are essentially marking it as checked. We can create this method dedicated to completing tasks:

/**
 * Completes a task
 *
 * @param int $task_id
 * @param int $revision
 * @return mixed
 */
public function completeTask($task_id, $revision) {
    if (!is_numeric($task_id)) {
        throw new \InvalidArgumentException('The list id must be numeric.');
    } elseif (!is_numeric($revision)) {
        throw new \InvalidArgumentException('The revision must be numeric.');
    }

    $response = $this->client->patch('tasks/' . $task_id, ['body' => json_encode(['revision' => (int) $revision, 'completed' => true])]);
    $this->checkResponseStatusCode($response, 200);
    return json_decode($response->getBody());
}

The endpoint we are PATCHING contains the ID of the task and we send as the request body the JSON representation of the values we want changed on the task. As mentioned earlier, this needs to contain the last revision of the resource which we can learn from the getListTasks() method we wrote earlier.

Assuming that we know the last revision and ID of the task in index.php, we can complete a task like so:

try {
  $response = $wunderlist->completeTask($task_id, $revision);
  dump($response)
}
catch(\Exception $exception) {
  dump($exception);
}

If all goes well, the response we get in the body is the JSON representation of our task that we can decode into an array.

Conclusion

There are, of course, many more endpoints and much more you can do with each endpoint. Go ahead and check out the documentation pages to get an overview of all you can do.

A very important aspect of integrating with Wunderlist is also to allow users to grant access to their own Wunderlist account. You can do this using the OAuth flow we mentioned earlier – here are some tutorials on how to accomplish that.

Finally, if you are curious, take a look at the repository where you can find a super tiny extension of what we discussed in this article with which you can supply a list ID, have its tasks printed on the screen and then check them off one by one via Ajax. Looks ugly, but you should be able to get the point.

Did you write a Wunderlist app yet? Show us!

Tags: api, guzzle, OOPHP, PHP, to-do, todo, wunderlist
Daniel Sipos is a Drupal developer who lives in Brussels, Belgium. He works professionally with Drupal but likes to use other PHP frameworks and technologies as well. He runs webomelette.com, a Drupal blog where he writes articles and tutorials about Drupal development, theming and site building.

No Reader comments

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!