Click Purchases API

Introduction

API available through the Ubuntu Store.

Staging: https://myapps.developer.staging.ubuntu.com/

Production: https://myapps.developer.ubuntu.com/

Endpoints require clients to send POST parameters encoded as JSON, setting the respective header (Content-type: application/json), as shown in the examples.

Use cases

Get payment methods

GET /api/2.0/click/paymentmethods/
Query Parameters:
 
  • currency – (optional) used to filter methods available for the given currency

Note

valid currencies are USD, EUR and GBP; if not specified, all available payment methods will be returned.

OAuth signed request with user credentials. It will return available payment methods for the user as a json response.

Warning

As this is an OAuth signed request, it’s important that the URL actually ends with a slash. Using a different URL, even if slightly (eg, missing trailing slash) will mean the OAuth signature validation will fail, resulting in a 401 Unauthorized response.

Request:

GET /api/2.0/click/paymentmethods/ HTTP/1.1

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

[
  {
    "description": "PayPal",
    "id": "paypal",
    "preferred": false,
    "choices": [
      {
        "currencies": [
          "USD",
          "GBP",
          "EUR"
        ],
        "id": 532,
        "requires_interaction": false,
        "preferred": true,
        "description": "PayPal Preapproved Payment (exp. 2014-04-12)"
      }
    ]
  },
  {
    "description": "Credit or Debit Card",
    "id": "credit_card",
    "preferred": true,
    "choices": [
      {
        "currencies": [
          "USD"
        ],
        "id": 1767,
        "requires_interaction": false,
        "preferred": true,
        "description": "**** **** **** 1111 (Visa, exp. 02/2015)"
      },
      {
        "currencies": [
          "USD"
        ],
        "id": 1726,
        "requires_interaction": false,
        "preferred": false,
        "description": "**** **** **** 1111 (Visa, exp. 03/2015)"
      }
    ]
  }
]

Optionally you can pass a currency to filter available methods by.

Request:

GET /api/2.0/click/paymentmethods/?currency=EUR HTTP/1.1

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

[
  {
    "description": "PayPal",
    "id": "paypal",
    "preferred": false,
    "choices": [
      {
        "currencies": [
          "USD",
          "GBP",
          "EUR"
        ],
        "id": 532,
        "requires_interaction": false,
        "preferred": true,
        "description": "PayPal Preapproved Payment (exp. 2014-04-12)"
      }
    ]
  }
]

Add payment method

GET /api/2.0/click/paymentmethods/add/
Query Parameters:
 
  • currency – used to specify the preferred currency

Note

valid currencies are USD, EUR and GBP; if not specified, USD will be assumed.

An OAuth signed GET to this endpoint (if going through a web view), will log in the user and redirect to the ‘Add credit card’ web page.

Warning

As this is an OAuth signed request, it’s important that the URL actually ends with a slash. Using a different URL, even if slightly (eg, missing trailing slash) will mean the OAuth signature validation will fail, resulting in a 401 Unauthorized response.

You can specify the preferred currency via a query parameter:

Request:

GET /api/2.0/click/paymentmethods/add/?currency=EUR HTTP/1.1

Disable payment method

DELETE /api/2.0/click/paymentmethods/(backend_id)/(method_id)/
Parameters:
  • backend_idid of one of the payment backends returned by the paymentmethods endpoint
  • method_idid of one of the choices available for the selected backend_id

An OAuth signed DELETE to this endpoint, will (request to) disable the specified payment method. In the case of credit cards, the disabling is immediate; for PayPal preapprovals this could take a bit, until PayPal processes the request and notifies us back (async).

Warning

As this is an OAuth signed request, it’s important that the URL actually ends with a slash. Using a different URL, even if slightly (eg, missing trailing slash) will mean the OAuth signature validation will fail, resulting in a 401 Unauthorized response.

Request:

DELETE /api/2.0/click/paymentmethods/credit_card/1726/ HTTP/1.1

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"success": true}

Request a purchase

POST /api/2.0/click/purchases/
Request Headers:
 
  • X-Partner-Id – (optional) Partner Id to indicate purchases coming from a partner
  • X-Device-Id – (optional) id of the device requesting the purchase
Form Parameters:
 
  • device_id – (optional) id of the device requesting the purchase
  • namepackage_name of the application a purchase is being requested for
  • item_skusku of the item being purchased for the given package
  • amount – the total price of the purchase
  • currency – the currency used to express the price
  • backend_idid of one of the payment backends returned by the paymentmethods endpoint
  • method_idid of one of the choices available for the selected backend_id
Status Codes:

Note

The Partner ID is case sensitive; invalid IDs will be ignored. The only valid partner ID a this time is bq.

Note

The device id SHOULD be specified using the X-Device-Id header. Sending the device_id parameter in the body is still supported for backwards compatibility.

OAuth signed request with user credentials. Token freshness will be checked against SSO (age < 15min). If signing token is not fresh enough (although valid), a 401 Unauthorized response will be returned, specifying the error and the expected threshold (in seconds).

Warning

As this is an OAuth signed request, it’s important that the URL actually ends with a slash. Using a different URL, even if slightly (eg, missing trailing slash) will mean the OAuth signature validation will fail, resulting in a 401 Unauthorized response.

If the user already purchased the application, the subscription details will be returned without taking a new payment.

Example: Signing token not fresh enough

Request:

 POST /api/2.0/click/purchases/ HTTP/1.1
 Content-Type: application/json
 X-Device-Id: 1234567890abcdef

{
  "name": "com.ubuntu.developer.dev.appname",
  "backend_id": "credit_card",
  "method_id": 1726
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "error": "TOKEN_NEEDS_REFRESH",
  "threshold": 900
}

Example: Purchase completed (from BQ device)

Request:

POST /api/2.0/click/purchases/ HTTP/1.1
Content-Type: application/json
X-Partner-Id: bq
X-Device-Id: 1234567890abcdef

{
  "name": "com.ubuntu.developer.dev.appname",
  "backend_id": "credit_card",
  "method_id": 1726,
  "currency": "GBP"
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "open_id": "https://login.staging.ubuntu.com/+id/open_id",
  "package_name": "com.ubuntu.developer.dev.appname",
  "refundable_until": "2015-07-15 18:46:21",
  "state": "Complete"
}

The refundable_until field is a UTC timestamp value indicating the date/time limit allowing the user to request a self-refund.

Once the limit expires, the returned value will be null, that is:

{
  "open_id": "https://login.staging.ubuntu.com/+id/open_id",
  "package_name": "com.ubuntu.developer.dev.appname",
  "refundable_until": null,
  "state": "Complete"
}

Example: Purchasing an in-app item

Request:

POST /api/2.0/click/purchases/ HTTP/1.1
Content-Type: application/json
X-Device-Id: 1234567890abcdef

{
  "name": "com.ubuntu.developer.dev.appname",
  "item_sku": "item_sku",
  "currency": "USD",
  "backend_id": "credit_card",
  "method_id": 1726
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "open_id": "https://login.staging.ubuntu.com/+id/open_id",
  "package_name": "com.ubuntu.developer.dev.appname",
  "item_sku": "item_sku",
  "purchase_id": "1",
  "refundable_until": null,
  "state": "Complete"
}

Note

In-app item purchases are not automatically refundable.

If backend_id and method_id are not specified, the user’s default payment method will be used (if available).

If currency is not specified, USD will be assumed. This defines the currency (and then, the amount) the user desires to be billed into; if the specified application does not provide an amount for the given currency, the USD amount will be used.

If the payment method to be used requires user interaction a URL will be returned in the response; to complete the purchase the user should follow that URL (previously OAuth signing it).

A possible response in that case could be:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "state": "InProgress",
  "redirect_to": "https://some-url"
}

If a partner ID was provided, the header (X-Partner-Id) should also be added to this new request (to the URL where the user is redirected to).

Get subscription details

GET /api/2.0/click/purchases/(package_name)/
Parameters:
  • package_name – used to filter purchases for this package
Query Parameters:
 
  • include_item_purchases – (optional) if present and has a value of true, return a list of all purchases for this package, including in-app item purchases
  • purchase_id – (optional) if present only the purchase matching the given purchase_id will be returned
Status Codes:

Also, a GET request will return the subscription details, if available (or a 404 response if not):

Warning

As this is an OAuth signed request, it’s important that the URL actually ends with a slash. Using a different URL, even if slightly (eg, missing trailing slash) will mean the OAuth signature validation will fail, resulting in a 401 Unauthorized response.

Request:

GET /api/2.0/click/purchases/com.ubuntu.developer.dev.appname/ HTTP/1.1
Accept: application/json

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "open_id": "https://login.staging.ubuntu.com/+id/open_id",
  "package_name": "com.ubuntu.developer.dev.appname",
  "refundable_until": "2015-07-15 18:46:21",
  "state": "Complete"
}

If include_item_purchases was specified in the url, then a list of subscription details will be returned which might include:

  • a subscription for the purchase of the package
  • a subscription for the purchase of an in-app item

Note

If the package is gratis the list might not include a subscription for the package itself.

Request:

GET /api/2.0/click/purchases/com.ubuntu.developer.dev.appname/?include_item_purchases=true HTTP/1.1
Accept: application/json

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

[
  {
    "open_id": "https://login.staging.ubuntu.com/+id/open_id",
    "package_name": "com.ubuntu.developer.dev.appname",
    "refundable_until": "2015-07-15 18:46:21",
    "state": "Complete"
  },
  {
    "open_id": "https://login.staging.ubuntu.com/+id/open_id",
    "package_name": "com.ubuntu.developer.dev.appname",
    "item_sku": "item-1-sku",
    "purchase_id": "1",
    "refundable_until": null,
    "state": "Complete"
  }
]

If purchase_id was specified in the url, then only the item subscription matching the purchase_id will be returned.

Request:

GET /api/2.0/click/purchases/com.ubuntu.developer.dev.appname/?purchase_id=1 HTTP/1.1
Accept: application/json

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "open_id": "https://login.staging.ubuntu.com/+id/open_id",
  "package_name": "com.ubuntu.com.developer.dev.appname",
  "item_sku": "item-1-sku",
  "purchase_id": "1",
  "refundable_until": null,
  "state": "Complete"
}

List completed subscriptions

GET /api/2.0/click/purchases/

Finally, a GET request on /purchases/ will return a list of the current completed subscriptions for the user, which might include:

  • a subscription for the purchase of the package
  • a subscription for the purchase of an in-app item

Note

If the package is gratis the list might not include a subscription for the package itself.

Warning

As this is an OAuth signed request, it’s important that the URL actually ends with a slash. Using a different URL, even if slightly (eg, missing trailing slash) will mean the OAuth signature validation will fail, resulting in a 401 Unauthorized response.

Request:

GET /api/2.0/click/purchases/ HTTP/1.1
Accept: application/json

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

[
  {
    "open_id": "https://login.staging.ubuntu.com/+id/open_id",
    "package_name": "com.ubuntu.developer.dev.appname",
    "refundable_until": "2015-07-15 18:46:21",
    "state": "Complete"
  },
  {
    "open_id": "https://login.staging.ubuntu.com/+id/open_id",
    "package_name": "com.ubuntu.developer.dev.appname",
    "item_sku": "item-1-sku",
    "purchase_id": "1",
    "refundable_until": null,
    "state": "Complete"
  },
  {
    "open_id": "https://login.staging.ubuntu.com/+id/open_id",
    "package_name": "com.ubuntu.developer.dev.otherapp",
    "refundable_until": "2015-07-17 11:33:29",
    "state": "Complete"
  }
]

Confirm download

POST /api/2.0/click/confirm-download/
Form Parameters:
 
  • namepackage_name of the package the download is being confirmed for

Expected to be called once an application download is completed. This will trigger a payment capture 15 minutes later, if the application was paid, giving then a 15-minute window to allow a user request a self-refund.

If download is not confirmed, the payment will be captured (this is, completed) 1 hour after the purchase was completed.

An OAuth signed POST request with the package name is expected. In case the subscription does not exist, or is not completed, a failure response will be returned.

Warning

As this is an OAuth signed request, it’s important that the URL actually ends with a slash. Using a different URL, even if slightly (eg, missing trailing slash) will mean the OAuth signature validation will fail, resulting in a 401 Unauthorized response.

Request:

POST /api/2.0/click/confirm-download/ HTTP/1.1
Content-Type: application/json

{
  "name": "com.ubuntu.developer.dev.appname"
}

Response (success):

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"success": true}

Response (failure):

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"success": false}

Request a refund

POST /api/2.0/click/refunds/
Form Parameters:
 
  • namepackage_name of the package the refund is being requested for

Between purchase completed/confirmed download and final payment capture there is time window in which a request to this API will cancel the payment without any other interaction (and without costs to the user or us).

self-refund window = MIN ((confirmed download + 15 minutes), (purchase completed + 1h))

An OAuth signed POST request with the package name is expected. In case the subscription does not exist, or refund time window expired, a {“success”: false} response will be returned.

Warning

As this is an OAuth signed request, it’s important that the URL actually ends with a slash. Using a different URL, even if slightly (eg, missing trailing slash) will mean the OAuth signature validation will fail, resulting in a 401 Unauthorized response.

Request:

POST /api/2.0/click/refunds/ HTTP/1.1
Content-Type: application/json

{
  "name": "com.ubuntu.developer.dev.appname"
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"success": true}

Python sample API interaction (staging)

import json
from urlparse import urljoin

import requests
from requests_oauthlib import OAuth1Session


# staging base URLs
PAY_URL = 'https://pay.staging.ubuntu.com/'
SCA_URL = 'https://myapps.developer.staging.ubuntu.com/'
SSO_URL = 'https://login.staging.ubuntu.com/'

# SSO existing user credentials
USER_EMAIL = 'some-user@example.com'
USER_PASSWORD = 'thepassword'

# staging click app with a purchase price set
CLICK_APP = 'com.ubuntu.developer.diegosarmentero.TabuGame.tabugame'

# get credential from SSO
# use a different token name? (if not, any refresh would work to allow purchases)
creds = requests.post(
    urljoin(SSO_URL, '/api/v2/tokens/oauth'),
    data=json.dumps(dict(email=USER_EMAIL,
    password=USER_PASSWORD, token_name='clickbuy')),
    headers={'content-type': 'application/json', 'accept': 'application/json'})

credentials = creds.json()

# create a session to work with
u1 = OAuth1Session(
    credentials.get('consumer_key'), credentials.get('consumer_secret'),
    credentials.get('token_key'), credentials.get('token_secret'))


# retrieve current available payment methods for this user
response = u1.get(
    urljoin(SCA_URL, '/api/2.0/click/paymentmethods/'),
    headers={'Accept': 'application/json'})


# adding a new payment method redirects
response = u1.get(urljoin(SCA_URL, '/api/2.0/click/paymentmethods/add/'))

response.history
## (<Response [302]>,)
response.url
## u'https://myapps.developer.staging.ubuntu.com/click/payment-method/add/'


# check if user already purchased the app
response = u1.get(
    urljoin(SCA_URL, '/api/2.0/click/purchases/%s/' % CLICK_APP),
    headers={'Accept': 'application/json'})

response.status_code
## 404


# retrieve current purchases information for this user
response = u1.get(
    urljoin(SCA_URL, '/api/2.0/click/purchases/'),
    headers={'Accept': 'application/json'})

response.status_code
## 200

response.json()
## []

# try to purchase the app
# (without refreshing the token, more than 15 minutes later)
response = u1.post(
    urljoin(SCA_URL, '/api/2.0/click/purchases/'),
    data=json.dumps({'name': CLICK_APP}),
    headers={'Content-Type': 'application/json', 'Accept': 'application/json'})

response.status_code
## 401

response.json()
## {u'threshold': 900, u'error': u'TOKEN_NEEDS_REFRESH'}


# refresh token and retry purchase

# refresh token
response = requests.post(
    urljoin(SSO_URL, '/api/v2/tokens/oauth'),
    data=json.dumps(dict(email=USER_EMAIL,
    password=USER_PASSWORD, token_name='clickbuy')),
    headers={'content-type': 'application/json', 'accept': 'application/json'})

# purchase
response = u1.post(
    urljoin(SCA_URL, '/api/2.0/click/purchases/'),
    data=json.dumps({'name': CLICK_APP}),
    headers={'Content-Type': 'application/json', 'Accept': 'application/json'})

response.status_code
## 201 (or 200 if user already purchased)

response.json()
## [{u'open_id': u'https://login.staging.ubuntu.com/+id/PDTAH8r',
##   u'package_name': u'com.ubuntu.developer.diegosarmentero.TabuGame.tabugame',
##   u'refundable_until': u'2015-07-15 18:46:21',
##   u'state': u'Complete'}]


# retrieve current purchases information for this user
# (after the self-refund period expired)
response = u1.get(
    urljoin(SCA_URL, '/api/2.0/click/purchases/'),
    headers={'Accept': 'application/json'})

response.status_code
## 200

response.json()
## [{u'open_id': u'https://login.staging.ubuntu.com/+id/PDTAH8r',
##   u'package_name': u'com.ubuntu.developer.diegosarmentero.TabuGame.tabugame',
##   u'refundable_until': null,
##   u'state': u'Complete'}]


# check if user already purchased the app
response = u1.get(
    urljoin(SCA_URL, '/api/2.0/click/purchases/%s/' % CLICK_APP),
    headers={'Accept': 'application/json'})

response.status_code
## 200

response.json()
## {u'open_id': u'https://login.staging.ubuntu.com/+id/PDTAH8r',
##  u'package_name': u'com.ubuntu.developer.diegosarmentero.TabuGame.tabugame',
##  u'state': u'Complete'}


# request self-refund
response = u1.post(
    urljoin(SCA_URL, '/api/2.0/click/refunds/'),
    data=json.dumps({'name': CLICK_APP}),
    headers={'Content-Type': 'application/json', 'Accept': 'application/json'})

response.status_code
## 200

response.json()
## {u'success': true} (or {u'success': false} if refundable window expires)

Adding a credit card to staging via API

# after previous initialization and credentials setup
# valid fake card details
card_data = {
    'open_id': credentials.get('openid'),
    'billing_address': {'street': 'Street',
                        'state': 'State',
                        'country_code': 'AR',
                        'postal_code': '5000'},
    'card_type': 'Visa',
    'card_holder': 'Test',
    'card_number': '4111111111111111',
    'card_ccv': '111',
    'card_expiration_month': 12,
    'card_expiration_year': 2021,
    'currency': 'USD',
    'make_unattended_default': True,
}

response = u1.post(
    urljoin(PAY_URL, '/api/2.0/credit_card/'),
    data=json.dumps(card_data),
    headers={'content-type': 'application/json', 'accept': 'application/json'})

response.json()
## {u'card_type': u'Visa',
## u'expiration': u'2021-12-31',
## u'masked_number': u'1111'}

Disabling a payment method via API

# retrieve current available payment methods for this user
response = u1.get(
    urljoin(SCA_URL, '/api/2.0/click/paymentmethods/'),
    headers={'Accept': 'application/json'})

response.json()
## [{u'choices': [{u'currencies': [u'USD'], u'id': 2547, u'preferred': True,
## u'description': u'**** **** **** 1111 (Visa, exp. 12/2021)'}, {u'currencies': [u'USD'], u'id': 2566,
## u'preferred': False, u'description': u'**** **** **** 1111 (Visa, exp. 03/2016)'}], u'id': u'credit_card',
## u'preferred': True, u'description': u'Credit or Debit Card'}]

response = u1.delete(
    urljoin(SCA_URL, '/api/2.0/click/paymentmethods/credit_card/2566/'),
    headers={'Accept': 'application/json'})

response
## <Response [200]>

response.json()
## {u'success': True}

response = u1.get(
    urljoin(SCA_URL, '/api/2.0/click/paymentmethods/'),
    headers={'Accept': 'application/json'})

response.json()
## [{u'choices': [{u'currencies': [u'USD'], u'id': 2547, u'preferred': True,
## u'description': u'**** **** **** 1111 (Visa, exp. 12/2021)'}], u'id': u'credit_card',
## u'preferred': True, u'description': u'Credit or Debit Card'}]