diff --git a/addons/iap/__init__.py b/addons/iap/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..dc5e6b693d19dcacd224b7ab27b26f75e66cb7b2
--- /dev/null
+++ b/addons/iap/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
diff --git a/addons/iap/__manifest__.py b/addons/iap/__manifest__.py
new file mode 100644
index 0000000000000000000000000000000000000000..35124df76ebcb421b3161f3349523852c5b7e43c
--- /dev/null
+++ b/addons/iap/__manifest__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+
+{
+    'name': 'In-App Purchases',
+    'category': 'Tools',
+    'summary': 'Basic models and helpers to support In-App purchases.',
+    'description': """
+This module provides standard tools (account model, context manager and helpers) to support In-App purchases inside Odoo.
+""",
+    'depends': ['web'],
+    'data': [
+        'security/ir.model.access.csv',
+        'security/ir_rule.xml',
+        'views/assets.xml',
+        'views/iap_views.xml',
+    ],
+    'qweb': [
+        'static/src/xml/iap_templates.xml',
+    ],
+    'auto_install': True,
+}
diff --git a/addons/iap/models/__init__.py b/addons/iap/models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4af61c83a0d780e9dadbc5a223d3307328c0d9c3
--- /dev/null
+++ b/addons/iap/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import iap
diff --git a/addons/iap/models/iap.py b/addons/iap/models/iap.py
new file mode 100644
index 0000000000000000000000000000000000000000..87394fd053b12ade58d5090ae3d0dda3f16a32dd
--- /dev/null
+++ b/addons/iap/models/iap.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+import contextlib
+import logging
+import json
+import uuid
+
+import werkzeug.urls
+import requests
+from requests.packages import urllib3
+
+from odoo import api, fields, models, exceptions
+
+_logger = logging.getLogger(__name__)
+
+DEFAULT_ENDPOINT = 'https://iap.odoo.com'
+
+
+#----------------------------------------------------------
+# Helpers for both clients and proxy
+#----------------------------------------------------------
+def get_endpoint(env):
+    url = env['ir.config_parameter'].sudo().get_param('iap.endpoint', DEFAULT_ENDPOINT)
+    return url
+
+
+#----------------------------------------------------------
+# Helpers for clients
+#----------------------------------------------------------
+class InsufficientCreditError(Exception):
+    pass
+
+
+class AuthenticationError(Exception):
+    pass
+
+
+def jsonrpc(url, method='call', params=None):
+    """
+    Calls the provided JSON-RPC endpoint, unwraps the result and
+    returns JSON-RPC errors as exceptions.
+    """
+    payload = {
+        'jsonrpc': '2.0',
+        'method': method,
+        'params': params,
+        'id': uuid.uuid4().hex,
+    }
+
+
+    _logger.info('iap jsonrpc %s', url)
+    try:
+        req = requests.post(url, json=payload)
+        response = req.json()
+        if 'error' in response:
+            name = response['error']['data'].get('name').rpartition('.')[-1]
+            message = response['error']['data'].get('message')
+            if name == 'InsufficientCreditError':
+                e_class = InsufficientCreditError
+            elif name == 'AccessError':
+                e_class = exceptions.AccessError
+            else:
+                e_class = exceptions.UserError
+            e = e_class(message)
+            e.data = response['error']['data']
+            raise e
+        return response.get('result')
+    except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, urllib3.exceptions.MaxRetryError) as e:
+        raise exceptions.AccessError('The url that this service requested returned an error. Please contact the author the app. The url it tried to contact was ' + url)
+
+#----------------------------------------------------------
+# Helpers for proxy
+#----------------------------------------------------------
+@contextlib.contextmanager
+def charge(env, key, account_token, credit, description=None, credit_template=None):
+    """
+    Account charge context manager: takes a hold for ``credit``
+    amount before executing the body, then captures it if there
+    is no error, or cancels it if the body generates an exception.
+
+    :param str key: service identifier
+    :param str account_token: user identifier
+    :param int credit: cost of the body's operation
+    :param str description:
+    """
+    end_point = get_endpoint(env)
+    params = {
+        'account_token': account_token,
+        'credit': credit,
+        'key': key,
+        'description': description,
+    }
+    try:
+        transaction_token = jsonrpc(endpoint + '/iap/1/authorize', params=params)
+    except InsufficientCreditError as e:
+        if credit_template:
+            arguments = json.loads(e.args[0])
+            arguments['body'] = env['ir.qweb'].render(credit_template)
+            e.args = (json.dumps(arguments),)
+
+    try:
+        yield
+    except Exception as e:
+        params = {
+            'token': transaction_token,
+            'key': key,
+        }
+        r = jsonrpc(end_point + '/iap/1/cancel', params=params)
+        raise e
+    else:
+        params = {
+            'token': transaction_token,
+            'key': key,
+        }
+        r = jsonrpc(end_point + '/iap/1/capture', params=params) # noqa
+
+
+#----------------------------------------------------------
+# Models for client
+#----------------------------------------------------------
+class IapAccount(models.Model):
+    _name = 'iap.account'
+    _rec_name = 'service_name'
+
+    service_name = fields.Char()
+    account_token = fields.Char(default=lambda s: uuid.uuid4().hex)
+    company_id = fields.Many2one('res.company', default=lambda self: self.env.user.company_id)
+
+    @api.model
+    def get(self, service_name):
+        account = self.search([('service_name', '=', service_name), ('company_id', 'in', [self.env.user.company_id.id, False])])
+        if not account:
+            account = self.create({'service_name': service_name})
+            # Since the account did not exist yet, we will encounter a NoCreditError,
+            # which is going to rollback the database and undo the account creation,
+            # preventing the process to continue any further.
+            self.env.cr.commit()
+        return account
+
+    @api.model
+    def get_credits_url(self, base_url, service_name, credit):
+        dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
+        account_token = self.get(service_name).account_token
+        d = {
+            'dbuuid': dbuuid,
+            'service_name': service_name,
+            'account_token': account_token,
+            'credit': credit,
+        }
+        return '%s?%s' % (base_url, werkzeug.urls.url_encode(d))
+
+    @api.model
+    def get_account_url(self):
+        route = '/iap/services'
+        endpoint = get_endpoint(self.env)
+        d = {'dbuuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid')}
+
+        return '%s?%s' % (endpoint + route, werkzeug.urls.url_encode(d))
diff --git a/addons/iap/security/ir.model.access.csv b/addons/iap/security/ir.model.access.csv
new file mode 100644
index 0000000000000000000000000000000000000000..0eeb34c40cd59cebcd2af7d9a4ad3be4f47768b2
--- /dev/null
+++ b/addons/iap/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_client_iap_account_manager,iap.account.manager,model_iap_account,base.group_system,1,1,1,0
+access_client_iap_account_user,iap.account.user,model_iap_account,base.group_user,1,0,0,0
\ No newline at end of file
diff --git a/addons/iap/security/ir_rule.xml b/addons/iap/security/ir_rule.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2935ce2b7461322df1443129814d4091aed33234
--- /dev/null
+++ b/addons/iap/security/ir_rule.xml
@@ -0,0 +1,13 @@
+<odoo>
+   <data>
+    <record id="user_iap_account" model="ir.rule">
+      <field name="name">User IAP Account</field>
+      <field name="model_id" ref="model_iap_account"/>
+      <field name="groups" eval="[(4, ref('base.group_user'))]"/>
+      <!-- partners can CUD services linked to themselves -->
+      <field name="domain_force">[
+        ('company_id', 'in', [user.company_id.id, False]),
+      ]</field>
+    </record>
+  </data>
+</odoo>
\ No newline at end of file
diff --git a/addons/iap/static/src/js/crash_manager.js b/addons/iap/static/src/js/crash_manager.js
new file mode 100644
index 0000000000000000000000000000000000000000..3c8d330013eadbc04768d3df4227e568a48d9122
--- /dev/null
+++ b/addons/iap/static/src/js/crash_manager.js
@@ -0,0 +1,49 @@
+odoo.define('iap.CrashManager', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var CrashManager = require('web.CrashManager');
+var Dialog = require('web.Dialog');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+CrashManager.include({
+    /**
+     * @override
+     */
+    rpc_error: function (error) {
+        if (error.data.name === "odoo.addons.iap.models.iap.InsufficientCreditError") {
+            var error_data = JSON.parse(error.data.message);
+            ajax.jsonRpc('/web/dataset/call_kw', 'call', {
+                model:  'iap.account',
+                method: 'get_credits_url',
+                args: [],
+                kwargs: {
+                    base_url: error_data.base_url,
+                    service_name: error_data.service_name,
+                    credit: error_data.credit,
+                }
+            }).then(function (url) {
+                new Dialog(this, {
+                    size: 'large',
+                    title: error_data.title || _t("Insufficient Balance"),
+                    $content: $(QWeb.render('iap.redirect_to_odoo_credit', {
+                        data: error_data,
+                    })).css('padding', 0),
+                    buttons: [
+                        {text: 'Buy credits at Odoo', classes : "btn-primary", click: function() {
+                            window.open(url, '_blank');
+                        }, close:true},
+                        {text: _t("Cancel"), close: true}
+                    ],
+                }).open();
+            });
+        } else {
+            this._super.apply(this, arguments);
+        }
+    },
+});
+
+});
diff --git a/addons/iap/static/src/js/dashboard.js b/addons/iap/static/src/js/dashboard.js
new file mode 100644
index 0000000000000000000000000000000000000000..b3490da2eda2d26ad3fabb3f22725cad9ce898b9
--- /dev/null
+++ b/addons/iap/static/src/js/dashboard.js
@@ -0,0 +1,29 @@
+odoo.define('iap.Dashboard', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var Dashboard = require('web_settings_dashboard');
+
+var _t = core._t;
+var QWeb = core.qweb;
+
+Dashboard.Dashboard.include({
+    /**
+     * @override
+     */
+    load_apps: function (data) {
+        var _super = this._super.bind(this);
+        return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
+            model:  'iap.account',
+            method: 'get_account_url',
+            args: [],
+            kwargs: {},
+        }).then(function (url) {
+            data.apps.url = url;
+            return _super(data);
+        });
+    },
+});
+
+});
diff --git a/addons/iap/static/src/js/iap_credit.js b/addons/iap/static/src/js/iap_credit.js
new file mode 100644
index 0000000000000000000000000000000000000000..37cddd72aa8ca60c4fd678cf4365c42ba510fa24
--- /dev/null
+++ b/addons/iap/static/src/js/iap_credit.js
@@ -0,0 +1,28 @@
+odoo.define('iap.redirect_odoo_credit_widget', function(require) {
+"use strict";
+
+var core = require('web.core');
+var framework = require('web.framework');
+var Widget = require('web.Widget');
+var QWeb = core.qweb;
+
+
+var IapOdooCreditRedirect = Widget.extend({
+    template: 'iap.redirect_to_odoo_credit',
+    events : {
+        "click .redirect_confirm" : "odoo_redirect",
+    },
+    init: function (parent, action) {
+        this._super(parent, action);
+        this.url = action.params.url;
+    },
+
+    odoo_redirect: function () {
+        window.open(this.url, '_blank');
+        this.do_action({type: 'ir.actions.act_window_close'});
+        // framework.redirect(this.url);
+    },
+
+});
+core.action_registry.add('iap_odoo_credit_redirect', IapOdooCreditRedirect);
+});
diff --git a/addons/iap/static/src/xml/iap_templates.xml b/addons/iap/static/src/xml/iap_templates.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9b52a4ec445b5cc7c008c653ce6a1a37938550f1
--- /dev/null
+++ b/addons/iap/static/src/xml/iap_templates.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<template id="template" xml:space="preserve">
+
+    <!-- LAYOUT TEMPLATES -->
+    <div t-name="iap.redirect_to_odoo_credit">
+        <t t-if="data.body">
+            <div t-raw="data.body"/>
+        </t>
+        <t t-if="!data.body">
+            <t t-if="data.message">
+                <span t-esc="data.message"/>
+            </t>
+            <t t-if="!data.message">
+                <span>Insufficient credit to perform this service.</span>
+            </t>
+        </t>
+    </div>
+
+    <t t-extend="DashboardApps">
+        <t t-jquery=".o_web_settings_dashboard_pills" t-operation="append">
+            <a class="pull-right" t-att-href="widget.data.url" target="_blank">
+            <i class="fa fa-money fa-2x text-muted"/> In-App Purchases</a>
+        </t>
+    </t>
+
+</template>
diff --git a/addons/iap/views/assets.xml b/addons/iap/views/assets.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0e7294ca5867da870a3f116749aebe34d251e834
--- /dev/null
+++ b/addons/iap/views/assets.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <template id="assets_backend" name="iap assets" inherit_id="web.assets_backend">
+        <xpath expr="." position="inside">
+            <script type="text/javascript" src="/iap/static/src/js/iap_credit.js"></script>
+            <script type="text/javascript" src="/iap/static/src/js/crash_manager.js"></script>
+            <script type="text/javascript" src="/iap/static/src/js/dashboard.js"></script>
+        </xpath>
+    </template>
+</odoo>
diff --git a/addons/iap/views/iap_views.xml b/addons/iap/views/iap_views.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ebd64fedee8f5fb84c992658e45b6b6402b20c8c
--- /dev/null
+++ b/addons/iap/views/iap_views.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+    <!-- iap Client Account Views -->
+    <record id="iap_account_view_form" model="ir.ui.view">
+        <field name="name">iap.account.form</field>
+        <field name="model">iap.account</field>
+        <field name="arch" type="xml">
+            <form string="IAP Account">
+                <sheet>
+                    <group name="account" string="Account Information">
+                        <field name="service_name"/>
+                        <field name="company_id"/>
+                        <field name="account_token"/>
+                    </group>
+                </sheet>
+            </form>
+        </field>
+    </record>
+    <record id="iap_account_view_tree" model="ir.ui.view">
+        <field name="name">iap.account.tree</field>
+        <field name="model">iap.account</field>
+        <field name="arch" type="xml">
+            <tree string="IAP Accounts">
+                <field name="service_name"/>
+                <field name="company_id"/>
+                <field name="account_token" readonly="1"/>
+            </tree>
+        </field>
+    </record>
+    <!-- Actions -->
+    <record id="iap_account_action" model="ir.actions.act_window">
+        <field name="name">IAP Account</field>
+        <field name="res_model">iap.account</field>
+        <field name='view_type'>form</field>
+        <field name='view_mode'>tree,form</field>
+    </record>
+
+    <!-- Menus -->
+    <menuitem
+        id="iap_root_menu"
+        name="IAP"
+        parent="base.menu_custom"
+        sequence="4"/>
+
+    <menuitem
+        id="iap_account_menu"
+        name="IAP Accounts"
+        parent="iap_root_menu"
+        action="iap_account_action"
+        sequence="10"/>
+
+</odoo>
diff --git a/addons/web_settings_dashboard/static/src/js/dashboard.js b/addons/web_settings_dashboard/static/src/js/dashboard.js
index 060f7dca61275faca7be21f98497ce7ea9cc0e56..7848b4934fd1b4c6b0d5c1da1df14204c1245fed 100644
--- a/addons/web_settings_dashboard/static/src/js/dashboard.js
+++ b/addons/web_settings_dashboard/static/src/js/dashboard.js
@@ -345,6 +345,7 @@ core.action_registry.add('web_settings_dashboard.main', Dashboard);
 
 return {
     Dashboard: Dashboard,
+    DashboardApps: DashboardApps,
     DashboardInvitations: DashboardInvitations,
     DashboardPlanner: DashboardPlanner,
     DashboardShare: DashboardShare,
diff --git a/doc/_extensions/odoo_ext/static/layout.less b/doc/_extensions/odoo_ext/static/layout.less
index c350defa2a1b52faef0d078e1187c94d78ce164a..4c8a5c643869c9e8640ca49f36ef88dbc2fc1653 100644
--- a/doc/_extensions/odoo_ext/static/layout.less
+++ b/doc/_extensions/odoo_ext/static/layout.less
@@ -103,7 +103,7 @@ main.has_code_col{
     > *{
       max-width: 100%;
     }
-    section {
+    > section {
       position: relative;
       display:block;
       float: left;
@@ -116,7 +116,7 @@ main.has_code_col{
       &:before {
         .code-col();
       }
-      section {
+      > section {
         > * {
           width: 54.633333%;
           max-width: 600px;
diff --git a/doc/_extensions/odoo_ext/static/style.css b/doc/_extensions/odoo_ext/static/style.css
index ead78f9101dd2f625e541f22d2e5d4304a050bf1..1442d4bdd6756fb2b4b7a4f770653610c2809599 100644
--- a/doc/_extensions/odoo_ext/static/style.css
+++ b/doc/_extensions/odoo_ext/static/style.css
@@ -9708,7 +9708,7 @@ main.has_code_col article.doc-body {
 main.has_code_col article.doc-body > * {
   max-width: 100%;
 }
-main.has_code_col article.doc-body section {
+main.has_code_col article.doc-body > section {
   position: relative;
   display: block;
   float: left;
@@ -9730,36 +9730,36 @@ main.has_code_col article.doc-body section {
     top: 0;
     right: 0;
   }
-  main.has_code_col article.doc-body section > * {
+  main.has_code_col article.doc-body > section > * {
     width: 54.633333%;
     max-width: 600px;
     float: left;
     clear: left;
   }
-  main.has_code_col article.doc-body section > h1,
-  main.has_code_col article.doc-body section > h2,
-  main.has_code_col article.doc-body section > h3,
-  main.has_code_col article.doc-body section > h4,
-  main.has_code_col article.doc-body section > h5,
-  main.has_code_col article.doc-body section > h6 {
+  main.has_code_col article.doc-body > section > h1,
+  main.has_code_col article.doc-body > section > h2,
+  main.has_code_col article.doc-body > section > h3,
+  main.has_code_col article.doc-body > section > h4,
+  main.has_code_col article.doc-body > section > h5,
+  main.has_code_col article.doc-body > section > h6 {
     width: 100%;
     float: none;
     clear: none;
   }
-  main.has_code_col article.doc-body section .doc-aside {
+  main.has_code_col article.doc-body > section .doc-aside {
     width: 41%;
     float: none;
     clear: none;
     margin-right: 15px;
     margin-left: 57%;
   }
-  main.has_code_col article.doc-body section .doc-aside .content-switcher {
+  main.has_code_col article.doc-body > section .doc-aside .content-switcher {
     margin-top: 0;
   }
-  main.has_code_col article.doc-body section .doc-aside .content-switcher > ul {
+  main.has_code_col article.doc-body > section .doc-aside .content-switcher > ul {
     margin-bottom: 0;
   }
-  main.has_code_col article.doc-body section .doc-aside .content-switcher > ul > li {
+  main.has_code_col article.doc-body > section .doc-aside .content-switcher > ul > li {
     color: #dcddde;
   }
 }
diff --git a/doc/_extensions/odoo_ext/translator.py b/doc/_extensions/odoo_ext/translator.py
index 443458f33c7755e94e7437b0687c6f3e09ab854a..c2349bca315b919d9b0b57d58c09771d43c90081 100644
--- a/doc/_extensions/odoo_ext/translator.py
+++ b/doc/_extensions/odoo_ext/translator.py
@@ -142,6 +142,11 @@ class BootstrapTranslator(nodes.NodeVisitor, object):
         if not self.section_level:
             self.body.append(u'</section>')
 
+    def visit_topic(self, node):
+        self.body.append(self.starttag(node, 'nav'))
+    def depart_topic(self, node):
+        self.body.append(u'</nav>')
+
     def is_compact_paragraph(self, node):
         parent = node.parent
         if isinstance(parent, (nodes.document, nodes.compound,
diff --git a/doc/index.rst b/doc/index.rst
index 21231b7ce3fadedc00eb62fd13eb478ba8852def..8035da789f9ea03364c90d4e0bef14bd7c1a48d9 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -14,7 +14,7 @@ Index
     :maxdepth: 2
 
     tutorials
-    api_integration
+    webservices
     setup
     reference
 
diff --git a/doc/reference.rst b/doc/reference.rst
index d83ef3bb4768bdd6107e1506fecddfa6c60d93b2..1b41f4cdbd24a0eb85bc27f7ddc36ec53d43b862 100644
--- a/doc/reference.rst
+++ b/doc/reference.rst
@@ -21,5 +21,4 @@ Reference
     reference/reports
     reference/mixins
     reference/guidelines
-    reference/upgrade_api
     reference/mobile
diff --git a/doc/webservices.rst b/doc/webservices.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3dad86448ba797ec7b4fb1bd12873284fe01f17b
--- /dev/null
+++ b/doc/webservices.rst
@@ -0,0 +1,13 @@
+:banner: banners/web_service_api.jpg
+:types: api
+
+============
+Web Services
+============
+
+.. toctree::
+    :titlesonly:
+
+    webservices/odoo
+    webservices/iap
+    webservices/upgrade
diff --git a/doc/webservices/flow.png b/doc/webservices/flow.png
new file mode 100644
index 0000000000000000000000000000000000000000..df4bf7cfbbbc24ecda679650eb5c13a467c93659
Binary files /dev/null and b/doc/webservices/flow.png differ
diff --git a/doc/webservices/iap.rst b/doc/webservices/iap.rst
new file mode 100644
index 0000000000000000000000000000000000000000..18af76900fb8bc09ebe1396cedda1f96b38a1493
--- /dev/null
+++ b/doc/webservices/iap.rst
@@ -0,0 +1,235 @@
+:types: api
+
+
+:code-column:
+
+.. _webservices/iap:
+
+================
+In-App Purchases
+================
+
+IAP allow providers of ongoing services through Odoo apps to be compensated
+for ongoing service use rather than — and possibly instead of — a sole initial
+purchase.
+
+In that context, Odoo acts mostly as a *broker* between the service user
+(client) and the service provider (Odoo App developer):
+
+* users purchase service tokens from Odoo
+* service providers draw tokens from the user's Odoo account
+
+.. attention::
+
+    This document is intended for *service providers* and presents the latter,
+    which can be done either via direct JSON-RPC2_ or if you are using Odoo
+    using the convenience helpers it provides.
+
+.. image:: flow.png
+    :align: center
+
+.. contents::
+    :local:
+
+JSON-RPC2_ Transaction API
+==========================
+
+* The IAP transaction API does not require using Odoo when implementing your
+  server gateway, calls are standard JSON-RPC2_.
+* Calls use different *endpoints* but the same *method* on all endpoints
+  (``call``).
+* Exceptions are returned as JSON-RPC2_ errors, the formal exception name is
+  available on ``data.name`` for programmatic manipulation.
+
+.. class:: ServiceKey
+
+    Identifier generated for the provider's service. Each key (and service)
+    matches a token of a fixed value, as generated by the service provide.
+
+    Multiple types of tokens correspond to multiple services e.g. SMS and MMS
+    could either be the same service (with an MMS being "worth" multiple SMS)
+    or could be separate services at separate price points.
+
+    .. danger:: your service key *is a secret*, leaking your service key
+                allows other application developers to draw credits bought for
+                your service(s)
+
+.. class:: UserToken
+
+    Identifier for a user account.
+
+.. class:: TransactionToken
+
+    Transaction identifier, returned by the authorization process and consumed
+    by either capturing or cancelling the transaction
+
+.. exception:: odoo.addons.iap.models.iap.NoCreditError
+
+    Raised during transaction authorization if the credits requested are not
+    currently available on the account (either not enough credits or too many
+    pending transactions/existing holds).
+
+.. exception:: odoo.addons.iap.models.iap.BadAuthError
+
+    Raised by any operation to which a service token is required, if the
+    service token is invalid.
+
+Authorize
+---------
+
+.. function:: /iap/1/authorize
+
+    Verifies that the user's account has at least as ``credit`` available
+    *and creates a hold (pending transaction) on that amount*.
+
+    Any amount currently on hold by a pending transaction is considered
+    unavailable to further authorize calls.
+
+    Returns a :class:`TransactionToken` identifying the pending transaction
+    which can be used to capture (confirm) or cancel said transaction.
+
+    :param ServiceKey key:
+    :param UserToken account_token:
+    :param int credit:
+    :param str description: optional, helps users identify the reason for
+                            charges on their accounts.
+    :returns: :class:`TransactionToken` if the authorization succeeded.
+    :raises: :class:`~odoo.addons.iap.models.iap.BadAuthError` if the service token is invalid
+    :raises: :class:`~odoo.addons.iap.models.iap.NoCreditError` if the account does
+    :raises: ``TypeError`` if the ``credit`` value is not an integer
+
+.. rst-class:: doc-aside
+
+.. code-block:: python
+
+    r = requests.post(ODOO + '/iap/1/authorize', json={
+        'jsonrpc': '2.0',
+        'id': None,
+        'method': 'call',
+        'params': {
+            'account_token': user_account,
+            'key': SERVICE_KEY,
+            'credit': 25,
+            'description': "Why this is being charged",
+        }
+    }).json()
+    if 'error' in r:
+        # handle authorize error
+    tx = r['result']
+
+    # provide your service here
+
+Capture
+-------
+
+.. function:: /iap/1/capture
+
+    Confirms the specified transaction, transferring the reserved credits from
+    the user's account to the service provider's.
+
+    Capture calls are idempotent: performing capture calls on an already
+    captured transaction has no further effect.
+
+    :param TransactionToken token:
+    :param ServiceKey key:
+    :raises: :class:`~odoo.addons.iap.models.iap.BadAuthError`
+
+.. rst-class:: doc-aside
+
+.. code-block:: python
+
+    r2 = requests.post(ODOO + '/iap/1/capture', json={
+        'jsonrpc': '2.0',
+        'id': None,
+        'method': 'call',
+        'params': {
+            'token': tx,
+            'key': SERVICE_KEY,
+        }
+    }).json()
+    if 'error' in r:
+        # handle capture error
+    # otherwise transaction is captured
+
+Cancel
+------
+
+.. function:: /iap/1/cancel
+
+    Cancels the specified transaction, releasing the hold on the user's
+    credits.
+
+    Cancel calls are idempotent: performing capture calls on an already
+    cancelled transaction has no further effect.
+
+    :param TransactionToken token:
+    :param ServiceKey key:
+    :raises: :class:`~odoo.addons.iap.models.iap.BadAuthError`
+
+.. rst-class:: doc-aside
+
+.. code-block:: python
+
+    r2 = requests.post(ODOO + '/iap/1/cancel', json={
+        'jsonrpc': '2.0',
+        'id': None,
+        'method': 'call',
+        'params': {
+            'token': tx,
+            'key': SERVICE_KEY,
+        }
+    }).json()
+    if 'error' in r:
+        # handle cancel error
+    # otherwise transaction is cancelled
+
+Odoo Helpers
+============
+
+For convenience, if you are implementing your service using Odoo the ``iap``
+module provides a few helpers to make IAP flow even simpler:
+
+Charging
+--------
+
+.. class:: odoo.addons.iap.models.iap.charge(env, key, account_token, credit[, description])
+
+    A *context manager* for authorizing and automatically capturing or
+    cancelling transactions for use in the backend/proxy.
+
+    Works much like e.g. a cursor context manager:
+
+    * immediately authorizes a transaction with the specified parameters
+    * executes the ``with`` body
+    * if the body executes in full without error, captures the transaction
+    * otherwise cancels it
+
+    :param odoo.api.Environment env: used to retrieve the ``iap.endpoint``
+                                     configuration key
+    :param ServiceKey key:
+    :param UserToken token:
+    :param int credit:
+    :param str description:
+
+.. rst-class:: doc-aside
+
+.. code-block:: python
+
+    @route('/deathstar/superlaser', type='json')
+    def superlaser(self, user_account,
+                   coordinates, target,
+                   factor=1.0):
+        """
+        :param factor: superlaser power factor,
+                       0.0 is none, 1.0 is full power
+        """
+        credits = int(MAXIMUM_POWER * factor)
+        with charge(request.env, SERVICE_KEY, user_account, credits):
+            # TODO: allow other targets
+            self.env['systems.planets'].search([
+                ('grid', '=', 'M-10'),
+                ('name', '=', 'Alderaan'),
+            ]).unlink()
+
+
+.. _JSON-RPC2: http://www.jsonrpc.org/specification
diff --git a/doc/api_integration.rst b/doc/webservices/odoo.rst
similarity index 99%
rename from doc/api_integration.rst
rename to doc/webservices/odoo.rst
index 612e91ed85422cd43cf952266c2c9abea9c3627e..f68862626a758d5a59f51378d51451bf897798f3 100644
--- a/doc/api_integration.rst
+++ b/doc/webservices/odoo.rst
@@ -4,9 +4,9 @@
 
 :code-column:
 
-===============
-Web Service API
-===============
+===========================
+Odoo Remote Procedure Calls
+===========================
 
 Odoo is usually extended internally via modules, but many of its features and
 all of its data are also available from the outside for external analysis or
diff --git a/doc/reference/upgrade_api.rst b/doc/webservices/upgrade.rst
similarity index 99%
rename from doc/reference/upgrade_api.rst
rename to doc/webservices/upgrade.rst
index 04edf919e83baddbd4359863c1734c6f651a6cf1..fa168935b2385f51a6671657ff2cc81b355de325 100644
--- a/doc/reference/upgrade_api.rst
+++ b/doc/webservices/upgrade.rst
@@ -6,9 +6,9 @@
 
 .. _reference/upgrade-api:
 
-===========
-Upgrade API
-===========
+================
+Database Upgrade
+================
 
 Introduction
 ~~~~~~~~~~~~