How To Write A Simple PHP/MySQL Web Service for an iOS App

Ray Wenderlich

This post is also available in: Spanish

Web Services Rule!

Web Services Rule!

As an iPhone/iPad developer, it can be really useful to be able to write your own simple web services that integrate with your apps.

For example, you may wish to display some news updates that come from your web server, and display it on startup. Or perhaps store some user data “in the cloud”. Your imagination is the only limit!

In this first tutorial in this two-part series, you’ll go step-by-step through the process of creating a simple web service, based on a promo code system I included in my latest app, Wild Fables. In the next part of this series, you’ll write an iOS app that integrates with this web service!

To run through all of the steps on this tutorial, you’ll need a web server with MySQL and PHP. If you do not have a web server already, you have three options:

  • If you want to enable Apache/MySQL/PHP directly on your Mac (for free), there are lots of good guides out there, here’s one I found with a quick Google search.
  • If you want to rent a web server online (usually for $$), there are many good choices out there, but the one I personally use (and enjoy) is Linode – check this tutorial for more information.
  • And if you’re just too lazy to do either of the above, you can just read through the steps below, and use the web service I’ve already made in part 2 of series :]

You don’t necessarily need to know PHP or MySQL to go through this tutorial (although it will be helpful!), as the tutorial includes all of the code you’ll need.

What You’ll Make

As you might know already if you’ve added In-App Purchases into your app, there is no built-in system provided by Apple to give out promo codes for your in-app purchases.

However, it can be extremely helpful to build your own promo code system for your in-app purchases, for several reasons:

  1. First, it’s great to be able to give out promo codes for in-app purchase content to professional app reviewers.
  2. Second, it’s also nice to be able to give out promo codes to your friends so they can check out your app.
  3. Finally, if you build your system right, it also provides a great way to track out which of your marketing vectors pay off (and which don’t) – more on this in the second part of the series!

So in this tutorial, we’re going to build a system where you can enter a code into your app, and it will connect to a web service to see if the code is valid or not. If it’s valid, the app will then “unlock” some content.

Don’t worry if you have no plans on adding this particular system into your app – you’ll learn the general technique of developing a web service and integrating it with an iPhone app as well!

Creating the Database

The first step of this project is to create the database tables you’ll need. For the purposes of this tutorial, you’ll need three database tables:

  • rw_app: A table to keep track of the apps we’re using the promo code system for. This way, you can use the same database tables for multiple apps.
    • id: Unique id for the app.
    • app_id: Unique string identifying the app (mainly for your own purposes).
  • rw_promo_code: A table to keep track of what promo codes are available.
    • id: Unique id for the code.
    • rw_app_id: The app id this code is for (from rw_app).
    • code: Alphanumeric code that the user types in to unlock something.
    • unlock_code: Alphanumeric string you’ll pass back to the app so it can know what to unlock.
    • uses_remaining: You’ll set things up so that codes can be used more than once – this way you can give out the same code to all of our friends on Twitter, for example. You’ll use this field to specify how many uses the code should have, and every time it is used this will be decremented by one. When iti hits 0, the code is no longer valid.
  • rw_promo_code_redeemed: A table to keep track some info each time a promo code that is redeemed. This will help us prevent one device from redeeming the same code multiple times (if it’s a multi-use code), by simply checking to see if the device has already used the code.
    • id: Unique id for the app.
    • rw_promo_code_id: The id of the promo code redeemed (from rw_promo_code).
    • device_id: The device identifier of the redeemer.
    • redeemed_time: A timestamp of when the code was redeemed.

Here are the MySQL statements you’ll need to create these tables:

DROP TABLE IF EXISTS rw_promo_code;
DROP TABLE IF EXISTS rw_promo_code_redeemed;

CREATE TABLE rw_promo_code (
    rw_app_id tinyint NOT NULL, 
    code varchar(255) NOT NULL,
    unlock_code varchar(255) NOT NULL,
    uses_remaining smallint NOT NULL

    app_id varchar(255) NOT NULL

CREATE TABLE rw_promo_code_redeemed (
    rw_promo_code_id mediumint NOT NULL,
    device_id varchar(255) NOT NULL,

On your web server, you need to create a MySQL database and create the three tables as specified above. The way you do this varies depending on your web host, but just in case it’s useful I’ll tell you the steps I use on my web host, where I have full command-line access.

I save all of the above SQL statements to a file called create.sql, then I create and populate a new database with the following commands:

rwenderlich@kermit:~$ mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1286
Server version: 5.1.37-1ubuntu5.1-log (Ubuntu)

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create database promos;
Query OK, 1 row affected (0.00 sec)

mysql> use promos;
Database changed
mysql> grant all privileges on promos.* to 'username'@'localhost' identified by 'password';
Query OK, 0 rows affected (0.00 sec)

mysql> exit

rwenderlich@kermit:~$ mysql -u username -p promos < create.sql
Enter password: 
rwenderlich@kermit:~$ mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 1417
Server version: 5.1.37-1ubuntu5.1-log (Ubuntu)

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use promos;
Database changed
mysql> show tables ;
| Tables_in_promos       |
| rw_app                 | 
| rw_promo_code          | 
| rw_promo_code_redeemed | 
3 rows in set (0.00 sec)

Now you should have three empty database tables in a new database. Next, go ahead and add a test app and a test code with the following statements:

INSERT INTO rw_app VALUES(1, 'com.razeware.test');
INSERT INTO rw_promo_code VALUES(1, 1, 'test', 'com.razeware.test.unlock.cake', 10000);

OK! Now that the database is connected and populated, on to writing the PHP web service!

Verifying PHP/MySQL functionality

Before you start implementing the PHP web service, first run a quick check to make sure PHP is working on your server OK. Create a new directory on your web server called promos, and create a new file inside called index.php with the following:

class RedeemAPI {
    // Main method to redeem a code
    function redeem() {
        echo "Hello, PHP!";
// This is the first thing that gets called when this page is loaded
// Creates a new instance of the RedeemAPI class and calls the redeem method
$api = new RedeemAPI;

This is a very basic PHP file that create an instance of a class (RedeemAPI) and calls a method on it that just outputs “Hello, PHP!”

You can test this by navigating to the URL on your web server with your browser. Even better, you can test this on the command line with a handy utility called curl similar to the following (but replace the URL with your own):

Ray-Wenderlichs-Mac-mini-2:~ rwenderlich$ curl
Hello, PHP!

Next, extend the class to make sure the service can connect to your database OK by replacing the RedeemAPI class with the following:

class RedeemAPI {
    private $db;
    // Constructor - open DB connection
    function __construct() {
        $this->db = new mysqli('localhost', 'username', 'password', 'promos');
    // Destructor - close DB connection
    function __destruct() {
    // Main method to redeem a code
    function redeem() {
        // Print all codes in database
        $stmt = $this->db->prepare('SELECT id, code, unlock_code, uses_remaining FROM rw_promo_code');
        $stmt->bind_result($id, $code, $unlock_code, $uses_remaining);
        while ($stmt->fetch()) {
            echo "$code has $uses_remaining uses remaining!";

This adds a constructor that connects to your database given a username and password and a destructor that closes the database connection. The redeem loop is modified to run a MySQL statement to select all of the entries in rw_promo_code, and loop through to print a line about each entry.

Once again you can test this with curl to make sure it’s working:

Ray-Wenderlichs-Mac-mini-2:~ rwenderlich$ curl
test has 10000 uses remaining!

Web Service Strategy: GET vs POST

OK, now that we know things are working, it’s almost time to implement the full behavior. But first, let’s talk about our strategy for the web service.

We know we need to pass some data from the iPhone app to our web service. Specifically, we need to tell the web service the ID of the app, the code to redeem, and the device id that is trying to redeem.

But how can we pass this data? If you aren’t familiar already, there are two ways to pass data to a web service – via GET (the normal way), or via POST (typically used for posting data to a web form).

Depending on which one you choose, the parameters get passed differently:

  • If you choose GET, the parameters are part of the URL.
  • If you choose POST, the parameters are passed as part of the request body.

Either one would work, but usually when you’re trying to “do something” like redeem a code (rather than just passively retrieving data), you would pick the POST method, so that’s what we’re goint to do here.

What does this mean in practice? All it means is if we want to access the passed parameters in PHP, we get them from the built in $_POST array, as follows:


And when we’re using ASIHTTPRequest to connect to the web service later, we’ll use the ASIFormDataRequest class, which sends the request as a POST, as follows:

ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:@"1" forKey:@"rw_app_id"];

For more information on GET vs POST, check out the Wikipedia entry on the HTTP protocol.

Update: Also, check out @smpdawg’s comment in the forum topic for this tutorial some additional must-read tips and info on this!

Web Service Strategy: The Algorithm

Next, let’s go over the general algorithm the web service will take:

  1. Make sure the required parameters are passed in via POST.
  2. Make sure the code is actually in the database.
  3. Make sure the code still has uses remaining.
  4. Make sure the device hasn’t already used the code.
  5. If we got this far, success!
    • Add an entry into rw_promo_code_redeemed to track the redemption.
    • Decrement the uses_remaining in rw_promo_code.
    • Return the unlock_code to the app, so it can give content to the user.

OK now that we have a strategy in hand, onto the implementation!

Web Service Implementation

First, add two helper methods to the top of your PHP file that you’ll use to easily return HTTP status messages on success and failure:

// Helper method to get a string description for an HTTP status code
// From 
function getStatusCodeMessage($status)
    // these could be stored in a .ini file and loaded
    // via parse_ini_file()... however, this will suffice
    // for an example
    $codes = Array(
        100 => 'Continue',
        101 => 'Switching Protocols',
        200 => 'OK',
        201 => 'Created',
        202 => 'Accepted',
        203 => 'Non-Authoritative Information',
        204 => 'No Content',
        205 => 'Reset Content',
        206 => 'Partial Content',
        300 => 'Multiple Choices',
        301 => 'Moved Permanently',
        302 => 'Found',
        303 => 'See Other',
        304 => 'Not Modified',
        305 => 'Use Proxy',
        306 => '(Unused)',
        307 => 'Temporary Redirect',
        400 => 'Bad Request',
        401 => 'Unauthorized',
        402 => 'Payment Required',
        403 => 'Forbidden',
        404 => 'Not Found',
        405 => 'Method Not Allowed',
        406 => 'Not Acceptable',
        407 => 'Proxy Authentication Required',
        408 => 'Request Timeout',
        409 => 'Conflict',
        410 => 'Gone',
        411 => 'Length Required',
        412 => 'Precondition Failed',
        413 => 'Request Entity Too Large',
        414 => 'Request-URI Too Long',
        415 => 'Unsupported Media Type',
        416 => 'Requested Range Not Satisfiable',
        417 => 'Expectation Failed',
        500 => 'Internal Server Error',
        501 => 'Not Implemented',
        502 => 'Bad Gateway',
        503 => 'Service Unavailable',
        504 => 'Gateway Timeout',
        505 => 'HTTP Version Not Supported'
    return (isset($codes[$status])) ? $codes[$status] : '';
// Helper method to send a HTTP response code/message
function sendResponse($status = 200, $body = '', $content_type = 'text/html')
    $status_header = 'HTTP/1.1 ' . $status . ' ' . getStatusCodeMessage($status);
    header('Content-type: ' . $content_type);
    echo $body;

If you’re confused why we need this, since this is a web service that conforms to the HTTP protocol, when you send a response you can give a header that specifies any error code and descripton that occurs. There are standard error codes to use, so these methods help make that a bit easier to work with.

As you can see, I found the function to convert status codes to HTML messages from a great tutorial on creating a REST API with PHP.

Next, onto the actual implementation! Replace your redeem method with the following:

function redeem() {
    // Check for required parameters
    if (isset($_POST["rw_app_id"]) && isset($_POST["code"]) && isset($_POST["device_id"])) {
        // Put parameters into local variables
        $rw_app_id = $_POST["rw_app_id"];
        $code = $_POST["code"];
        $device_id = $_POST["device_id"];
        // Look up code in database
        $user_id = 0;
        $stmt = $this->db->prepare('SELECT id, unlock_code, uses_remaining FROM rw_promo_code WHERE rw_app_id=? AND code=?');
        $stmt->bind_param("is", $rw_app_id, $code);
        $stmt->bind_result($id, $unlock_code, $uses_remaining);
        while ($stmt->fetch()) {
        // Bail if code doesn't exist
        if ($id <= 0) {
            sendResponse(400, 'Invalid code');
            return false;
        // Bail if code already used		
        if ($uses_remaining <= 0) {
            sendResponse(403, 'Code already used');
            return false;
        // Check to see if this device already redeemed	
        $stmt = $this->db->prepare('SELECT id FROM rw_promo_code_redeemed WHERE device_id=? AND rw_promo_code_id=?');
        $stmt->bind_param("si", $device_id, $id);
        while ($stmt->fetch()) {
        // Bail if code already redeemed
        if ($redeemed_id > 0) {
            sendResponse(403, 'Code already used');
            return false;
        // Add tracking of redemption
        $stmt = $this->db->prepare("INSERT INTO rw_promo_code_redeemed (rw_promo_code_id, device_id) VALUES (?, ?)");
        $stmt->bind_param("is", $id, $device_id);
        // Decrement use of code
        $this->db->query("UPDATE rw_promo_code SET uses_remaining=uses_remaining-1 WHERE id=$id");
        // Return unlock code, encoded with JSON
        $result = array(
            "unlock_code" => $unlock_code,
        sendResponse(200, json_encode($result));
        return true;
    sendResponse(400, 'Invalid request');
    return false;

You should be able to understand the general idea of how things work here by looking at the comments inline with the code, and if you’re more curious check out the Mysqli reference. Also, here’s a few things I’d like to point out:

  • isset is a handy function in PHP you can use to tell if a particular variable has been set. We use it here to make sure all the required POST parameters are passed in.
  • Note that instead of appending the passed in variables to the SQL statement ourselves, we use the bind_param method to do that. This is a much safer way, otherwise you make yourself vulnerable to SQL injection attack.
  • Note that the unlock_code is returned encoded with JSON. It’s true we could have passed this as a raw string or such since we’re only returning one thing, but by using JSON it makes it easier to extend later.

And that’s it – your web service is ready to roll! You can test it with the following curl command:

curl -F "rw_app_id=1" -F "code=test" -F "device_id=test"

Note that if you are trying this on my web service, if you get a “code already used” error, you should change your device_id (since each one can only be used once!)

You may also wish to go into your database and check that there’s an entry in your rw_promo_code_redeemed table, that the uses_remaining has decremented, etc. Play around with it a bit!

Where To Go From Here?

Here is the source code for the web service we’ve developed in the above tutorial.

Stay tuned for the next tutorial in the series, where we’ll show how to create an iPhone app that integrates with this web service, and also discuss my experience with using this promo code system in Wild Fables!

If you have any questions about this tutorial, or tips for others making web services for iPhone apps, please join the forum discussion below!

Ray Wenderlich

Ray is an indie software developer currently focusing on iPhone and iPad development, and the administrator of this site. He’s the founder of a small iPhone development studio called Razeware, and is passionate both about making apps and teaching others the techniques to make them.

When Ray’s not programming, he’s probably playing video games, role playing games, or board games.

User Comments


[ 1 , 2 , 3 , 4 , 5 ]
  • I'd really appreciate help if anyone has time. I put in a try/catch block to get some more information.

    and I get:
    Parse error: syntax error, unexpected T_VARIABLE, expecting T_FUNCTION in /data/23/2/69/87/2884250/user/3179445/htdocs/promos/index.php on line 36
    which is my execute statement

    Does this mean that there is a variable mismatch between MySQL and PHP? I'm at a loss to understand where.


    function redeem() {
    // Print all codes in database
    $stmt = $this->db->prepare('SELECT id, code, unlock_code, uses_remaining FROM rw_promo_code');
    $stmt->bind_result($id, $code, $unlock_code, $uses_remaining);
    while ($stmt->fetch()) {
    echo "$code has $uses_remaining uses remaining!";
    catch(Exception $e) {
    echo 'Caught exception: ', $e->getMessage(), "\n";
  • A few weeks ago I implemented this tutorial using MySQL and PHP on my NameCheap web host. Since then, I began using Parse to send push notifications to my app and realized that implementing the Promo Code with the Parse 'Cloud Code' and database they maintain makes this an almost trivial exercise. No need to learn PHP and no need to muck with a MySQL instance. Also, a web host is not needed, just sign up for Parse and let them handle the backend.

    Here is the Javascript code I use:
    Code: Select all

    request - The request object contains information about the request. The following fields are set:
    params - The parameters object sent to the function by the client.
    user - The Parse.User that is making the request. This will not be set if there was no logged-in user.

    response - The response object contains two functions:
    success - This function takes an optional parameter which is the data to send back to the client. This object can be any JSON object/array and can contain a Parse.Object.
    error - If called, signals that there was an error. It takes an optional parameter which will be passed to the client to provide a helpful error message.

    Parse.Cloud.define("promoCode", function (request, response)
        var PromoCode = Parse.Object.extend("promoCode");
        var pc = new Parse.Query(PromoCode);

                success: function(promo) {
                    if (promo == null) {
                        response.success("Unknown Promo Code");
                    else {
                        var count = promo.get("count");

                        if (count <= 0) {
                            response.success("Code Expired");
                        else {
                            // Check if already used by this device
                            var PromoCodeUsers = Parse.Object.extend("promoCodeUsers");
                            var codeUser = new Parse.Query(PromoCodeUsers);
                            var did = request.params.device_id;
                            var name =;

                            codeUser.equalTo("device_id", did);
                            codeUser.equalTo("name", name);

                                  success: function(user) {
                                      if (user != null) {
                                        response.success("Code already used");
                                      else {
                                        // code valid, record it
                                        count -= 1;
                                        promo.set("count", count);

                                        var saveMe = new PromoCodeUsers();
                                        saveMe.set("device_id", request.params.device_id);

                                        response.success({ "unlock_code" : "com.whatever.unlock.premium"});
                                  error: function (user)
                                      response.error("Error 2");
                error: function(error) {
                    response.error("Error 1");

    And in iOS, use this to invoke the remote method:
    Code: Select all

        [PFCloud callFunctionInBackground:@"promoCode" withParameters:[NSDictionary
                                                                       dictionaryWithObjectsAndKeys:code, @"name", uniqueIdentifier, @"device_id", nil]
                                    block:^(id response, NSError *error){

                                        [MBProgressHUD hideHUDForView:self.view animated:YES];
                                        if ([response isKindOfClass:[NSDictionary class]])
                                            NSDictionary *results = (NSDictionary *)response;
                                            NSString *unlockCode = [results objectForKey:@"unlock_code"];

                                            if ([unlockCode rangeOfString:@"com.whatever.unlock.premium"].location != NSNotFound)
                                                self.textView.text = @"Premium Unlocked!";
                                                [[InAppHelper sharedInstance] authorizePremium];
                                                [self.purchaseTable reload];
                                                self.textView.text = [NSString stringWithFormat:@"Unlock code: %@", unlockCode];
                                            self.textView.text = response;

    Thanks for the great forum and tutorials. I hope my contribution helps someone out!
  • Is there a way to do these commands:

    INSERT INTO rw_app VALUES(1, 'com.razeware.test');
    INSERT INTO rw_promo_code VALUES(1, 1, 'test', 'com.razeware.test.unlock.cake', 10000);

    in a method called inside an app or is the only way by entering those commands into the computer?

  • Hello,

    There are a lot of REST tutorials out there for various Servers.

    I chose RW because of the economy of PHP/MySql and his great articles.

    However I have been unable to find the Part 2 of the series mentioned in "How To Write A Simple PHP/MySQL Web Service for an iOS App"

    I'm especially interested in this as this is my first complete iOS 7 App I'm writing, although I tested some things over the Summer in the beta.

    Thank you!
  • Richard Caseyrcasey
  • Hi Ray, Great tutorial, but I'm getting 'invalid request' when opening up the website. Any ideas?
  • In the above post it appears there is a type:

    // Check to see if this device already redeemed
    $stmt = $this->db->prepare('SELECT id FROM rw_promo_code_redeemed WHERE device_id=? AND rw_promo_code_id=?');
    $stmt->bind_param("si", $device_id, $id);
    while ($stmt->fetch()) {

    Should certainly have instead the line:
    >bind_param("is", $device_id, $id);
  • Thanks Ray Wenderlich for this great tutorial. I am curious whether an already existing website can share its MySQL database with such a web service for iOS App ? i.e. Will the website conflict with the web service for the iOS App because they share the same MySQL database? I think there will be no problem for their business rule.I am just have concern from the technical perspective. thanks!
  • I Ray!
    Also for me when I complete the third stage had a "Invalid request" when test index.php page.

    On the first two stages also everything is ok but in the last I have this problem. I ever tried with your own code that I downloaded from your source.

    Can you help me ?

  • I fix my "Invalid request" error.
    In my case, when I typing "curl -F "rw_app_id=1" -F "code=test" -F "device_id=test" http://localhost/~marconori/promos/"did receive an "Invalid request" response.
    To solve that I'm using this other string "curl --data "rw_app_id=1&code=test&device_id=test" http://localhost/~marconori/promos/" and everything is ok!
    The problem was how I've passed the POST variables to my url.
  • Where is part two? I don't know if I am wrong that you should add a link to the rest series of this tutorial at the end of this page.
  • Hello

    I have problems with this topic.
    In you're code you have ..
    // Return unlock code, encoded with JSON
    $result = array("unlock_code" => $unlock_code,

    This only put one record into array($result)..... I need to store many records, how can I do that?
  • Thanks a lot for that excellent tuto.
    I gower have a problem with saving the modified file, I get an error :
    This operation couldn't be completed, because an error occurred.
    This authenticated save for this file failed (application error code: 20004)

    1. Open your favorite text editor. Mine happens to be TextMate, but any good text editor will do.

    2. Navigate to /etc/apache2. Its a hidden directory, so to get there, you will need to use the Command Shift G shortcut, either in the Finder, or in your text editors Open File window. This invokes the Go To Location option, allowing you to enter the exact path you want to navigate to. Once in the /etc/apache2 folder, look for the httpd.conf file. Open it.

    3. Do a search for php. You should see the following line:

    #LoadModule php5_module libexec/apache2/
    Remove the #, which uncomments that line, enabling the php5 module in Apache.

    4. Save the file. You may need to authenticate upon saving.

    thanks in advance for help
  • Hi,

    Thanks to this tutorial, I set up my server and mechanisms to unlock the in-app purchases.
    But my app has just been rejected :

    11.1: Apps that unlock or enable additional features or functionality with mechanisms other than the App Store will be rejected

    We found your app inappropriately unlocks or enables additional functionality with mechanisms other than the App Store, which is not in compliance with the App Store Review Guidelines.

    Specifically, your app includes a Promo Code option. Please remove the Promo Code feature from your app.

    Please refer to the attached screenshot.

    Have you succeeded in publish an application using this feature ?
    What is the alternative ?

[ 1 , 2 , 3 , 4 , 5 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!

Coming up in May: Procedural Level Generation in Games with Kim Pedersen.

Sign Up - May

Coming up in June: WWDC Keynote - Podcasters React! with the podcasting team.

Sign Up - June

Vote For Our Next Book!

Help us choose the topic for our next book we write! (Choose up to three topics.)

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

  • Jean-Pierre Distler
  • Sam Davies
  • Kirill Muzykov

... 55 total!

Editorial Team

  • John Clem

... 21 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

  • Myeong Hoon
  • David Xie

... 38 total!

Subject Matter Experts

  • Richard Casey

... 4 total!