One-Step Payment

Overview

The One-Step Payment (OSP) is the simplest method for purchases using AppCoins. To implement it, the developer must create a URL with the payment information (see picture below).
Visit the easy integrations' page for more information.

One Step Payment flowOne Step Payment flow

One Step Payment flow

Process

  1. Create the URL to start the purchase flow. The URL should follow this structure;
  2. Use the URL to make a system call;
  3. AppCoins wallet will communicate with our services to process the payment;
  4. Once the purchase is concluded, our server will notify the app’s services with the details.
  5. The app’s server should deliver the purchased item.

There are 2 ways to implement OSP in your app:

Implementation with server-side validation is much safer since every validation is performed server-side and if the URL is tampered with, our services will reject it.

Implementation without server-side validation is simpler but not as safe. The process is similar to google billing without server-side check, since the entire validation is in the APK, meaning that the value of the purchases can be manipulated by people with reverse engineering skills.

For now, to prevent fraud, implementations without server-side validation are limited to 500 completed transactions every 30 days.

Implementing OSP with server-side validation (backend)

1. Create URL

The first step is to add the functionality to your app to generate the URL as specified in OSP URL Structure.
The domain must be the package name and it must be certified in catappult even for testing purposes.

For extra security of the URL and to validate if its content has not been tampered with, the developer should include the signature parameter in the URL, this parameter is necessary for the backend implementation.

The signature parameter is built as shown below, where the URL is signed using an HMAC function with the use of the SHA256 algorithm.

The required secret key should be available only at the server level and should be shared between the developer and provider. This secret key may be obtained by contacting the catappult dev support team.

Then to have the URL accessible to the app make an endpoint that receives the obligatory and desired parameters and returns the URL with its content, instead of creating the URL in the app, make a request to the designed endpoint with any HTTP library.

🚧

From early 2021 the signature was made obligatory for backend implementations of OSP, apps that were already implemented with OSP without a signature were added to a list so they can continue to work. If a developer has an app that is in that list but is implementing a new one when the secret key is generated every app from that developer will be taken from that list making the signature necessary for old apps.

The following examples show how the endpoint can be implemented to create a URL that contains value, currency, product, domain, data, callback_url, and order_reference.

<?php

$DOMAIN = 'com.mygamestudio.game';
$CALLBACK_URL = 'https%3A%2F%2Fwww.mygamestudio.com%2Fappcoins%3F';
$BASE_URL = 'https://apichain.catappult.io/transaction/inapp';
$SECRET = 'foobar'; 

function generate_url($value, $currency, $product, $data, $order_reference): string
{
    global $DOMAIN, $CALLBACK_URL, $BASE_URL, $SECRET;

    $url = $BASE_URL . 
        '?value=' . $value . 
        '&currency=' . $currency . 
        '&product=' . $product .
        '&domain=' . $DOMAIN .
        '&data=' . $data .
        '&callback_url=' . $CALLBACK_URL .
        '&order_reference=' . $order_reference;
   
    $signature = hash_hmac('sha256', $url, $SECRET, false);
   
    return $url . '&signature=' . $signature;
}
 
echo generate_url($_POST['value'], $_POST['currency'], $_POST['product'], $_POST['data'], $_POST['order_reference']);
 
?>
from flask import Flask
import json
import hmac
import hashlib

app = Flask(__name__)

DOMAIN = "com.mygamestudio.game"
CALLBACK_URL = "https%3A%2F%2Fwww.mygamestudio.com%2Fappcoins%3F"
BASE_URL = "https://apichain.catappult.io/transaction/inapp"
SECRET = b"foobar"

@app.route("/generate_url", methods=['POST'])
def generate_url():
    global DOMAIN, CALLBACK_URL, BASE_URL, SECRET

    content = json.loads(request.json())

    url = "{}/?value={}&currency={}&product={}&domain={}&data={}&callback_url={}&order_reference={}".format(
        content['value'], content['currency'], content['product'], DOMAIN, content['data'], CALLBACK_URL, content['order_reference']
    )

    signature = hmac.new(SECRET, url.encode("utf-8"), hashlib.sha256).hexdigest()
    return f"{url}&signature={signature}"
// crypto-js needs to be istalled with npm
import hmacSHA256 from 'crypto-js/hmac-sha256';
const express = require('express')
const app = express()
const port = 3000

const DOMAIN = "com.mygamestudio.game";
const CALLBACK_URL = "https%3A%2F%2Fwww.mygamestudio.com%2Fappcoins%3F";
const BASE_URL = "https://apichain.catappult.io/transaction/inapp";
const SECRET = "foobar";

app.post('/create_url', (req, res) => {
  const content = req.json()

  let url = BASE_URL + 
      '?value=' + content['value'] + 
      '&currency=' + content['currency'] + 
      '&product=' + content['product'] +
      '&domain=' + DOMAIN +
      '&data=' + content['data'] +
      '&callback_url=' + CALLBACK_URL +
      '&order_reference=' + content['order_reference'];

  const signature = hmacSHA256(url, SECRET);

  res.send(url + '&signature=' + signature)
})

await app.listen(port)

📘

To prevent value tampering one option is to store the value of each SKU server-side, this way the value field is generated based on the chosen product and not on the value sent by the app.

2. Call the URL

Inside the app/game, an intent with the URL should be opened either using the web browser (whenever the AppCoins Wallet is not installed), or via the AppCoins Wallet.

In some cases, the user chooses to open the URL with the browser when the AppCoins Wallet is already installed or even chooses the option to always open the URL with the browser. To avoid this, we suggested adding the following sample to trigger the One Step billing flow.

private static int RC_ONE_STEP = 10003

Intent intent = buildTargetIntent(url);
try {
    startActivityForResult(intent, RC_ONE_STEP);
} catch (Exception e) {
    e.printStackTrace();
}
/**
* This method generates the intent with the provided One Step URL to target the
* AppCoins Wallet.
* @param url The url that generated by following the One Step payment rules
* 
* @return The intent used to call the wallet 
*/
private Intent buildTargetIntent(String url) {
  Intent intent = new Intent(Intent.ACTION_VIEW);
  intent.setData(Uri.parse(url));

  // Check if there is an application that can process the AppCoins Billing
  // flow
  PackageManager packageManager = getApplicationContext().getPackageManager();
  List<ResolveInfo> appsList = packageManager
            .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
  for (ResolveInfo app : appsList) {
    if (app.activityInfo.packageName.equals("cm.aptoide.pt")) {
      // If there's aptoide installed always choose Aptoide as default to open 
      // url
      intent.setPackage(app.activityInfo.packageName);
      break;
    } else if (app.activityInfo.packageName.equals("com.appcoins.wallet")) {
      // If Aptoide is not installed and wallet is installed then choose Wallet
      // as default to open url
      intent.setPackage(app.activityInfo.packageName);
    }
  }
  return intent;
}
val RC_ONE_STEP = 10003

var intent = buildTargetIntent(url);
try {
    startActivityForResult(intent, RC_ONE_STEP);
} catch (e: Exception) {
    e.printStackTrace();
}

/**
* This method generates the intent with the provided One Step URL to target the
* AppCoins Wallet.
* @param url The url that generated by following the One Step payment rules
* 
* @return The intent used to call the wallet 
*/
private fun buildTargetIntent(url: String): Intent {
    var intent = Intent(Intent.ACTION_VIEW)
    intent.setData(Uri.parse(url))

    // Check if there is an application that can process the AppCoins Billing
    // flow
    var packageManager = getApplicationContext().getPackageManager()
    var appsList: List<ResolveInfo> = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
    for (app in appsList) {
        if (app.activityInfo.packageName == "cm.aptoide.pt") {
            // If there's aptoide installed always choose Aptoide as default to open 
            // url
            intent.setPackage(app.activityInfo.packageName)
            break  
        } else if (app.activityInfo.packageName == "com.appcoins.wallet") {
            // If Aptoide is not installed and wallet is installed then choose Wallet
            // as default to open url
            intent.setPackage(app.activityInfo.packageName)
        }
    }
    return intent
}

Target SDK 30 and over

When the app targets SDK 30 or above you need to add the intent to AndroidManifest as such:

<manifest>
...
    <queries>
        <intent>
            <action android:name="android.intent.action.VIEW"/>
        </intent>
    ...
    </queries>
...
</manifest>

3. Set the callback URL

This is the first step to get a notification when the purchase has been processed and validated, for more information see Payment Notification.

The callback URL can comprise, as its query string, any parameter relevant to the transaction that will ensure its well functioning on the developer's side, this callback URL must be added to the creation of the URL. As an example, the given URL contains a transaction ID relevant to the developer.

https://www.mygamestudio.com/v1/appcoins_ipn.php?out_trade_no=2082389608326064

4. Prepare your server

If the developer provides a callback URL and the signature, our purchase service triggers a notification whenever there is a valid purchase. In order to use this service, you must prepare your server to receive our purchase service’s request and validate data integrity.

Our service will call the provided callback URL using the structure specified in Request to callback URL. The following snippets exemplify how the request can be handled.

📘

For a complete example of a server written in Python/Flask that handles this step and the following 2 see appcoins-server-validator

<?php
require_once 'vendor/autoload.php';
use GuzzleHttp\Client;
# File exemplified in following step
require '/validate_data.php';
# Mock file to be implmented which stores the verified transactions
require 'VerifiedTransactions.php';

$json = file_get_contents('php://input');
$notification_transaction = json_decode(json_decode($json)->transaction);

$client = new Client();
$webservice_transaction = json_decode($client->get(
    'https://api.blockchainds.com/broker/8.20200101/transactions/'.$notification_transaction->uid
)->getBody());

if (is_valid($notification_transaction, $webservice_transaction)) {
    VerifiedTransactions.add($notification_transaction->uid);
}
echo 200
?>
  //This next file can be used as an endpoint to check if transaction was valid
 <?php
# Mock file to be implmented which stores the verified transactions
require_once('VerifiedTransactions.php');

$uid = $_GET['uid'];

$response->valid = VerifiedTransactions.contains($uid);

echo json_encode($response);
?>
import json
import requests
from flask import Flask

app = Flask(__name__)

WEBSERVICE = 'https://api.blockchainds.com/broker/8.20200101/transactions/{}'
verified_transactions = []

# Example for callback_url = 'https://www.mygamestudio.com/v1/appcoins_ipn'
@app.route("/appcoins_ipn", methods=['POST'])
def appcoins_ipn():
  global WEBSERVICE, verified_transactions

  if len(verified_transactions) > 100:
    del verified_transactions[:]

  notification_transaction = json.loads(json.loads(request.json())['transaction'])
  webservice_response = requests.get(
    WEBSERVICE.format(notification_transaction['uid'])
  ).json()

  if is_valid(notification_transaction, webservice_response):
    verified_transactions.insert(0, notification_transaction['uid'])

  return 200

@app.route('/verify/<string:uid>')
def verify(uid):
  global verified_transactions

  response = {'verified': uid in verified_transactions}
  return response
const express = require('express')
const app = express()
const port = 3000

WEBSERVICE = 'https://api.blockchainds.com/broker/8.20200101/transactions/'
verified_transactions = []

app.post('/appcoins_ipn', async (req, res) => {
    if (verified_transactions.length > 100) {
        verified_transactions = [];
    }

    notification_transaction = JSON.parse(req.json['transaction']);
    webservice_transaction = await (await fetch(WEBSERVICE + notification_transaction['uid'])).json()

    if (isValid(notification_transaction, webservice_transaction)) {
        verified_transactions.unshift(notification_transaction['uid'])
    }

    res.sendStatus(200)
})

app.get('/verify/:uid', (req, res) => {
    const uid = req.params['uid'];
    const response = {'verified': verified_transactions.contains(uid)};
    res.send(response);
})

await app.listen(port)

5. Verify data integrity

To verify if the data sent in the transaction parameter was generated by Catappult, you can use our transaction's API where you pass the transaction UID and the returned data must be equal to the transaction data the callback URL received.

The following code examples show the function is_valid/isValid used in the step above.

//Same file as step 4
function is_valid($notification_transaction, $webservice_transaction) {
    return $notification_transaction->uid === $webservice_transaction->uid &&
        $notification_transaction->domain === $webservice_transaction->domain &&
        $notification_transaction->product === $webservice_transaction->product &&
        $notification_transaction->status === $webservice_transaction->status &&
        $notification_transaction->reference === $webservice_transaction->reference &&
        $notification_transaction->price->currency === $webservice_transaction->price->currency &&
        $notification_transaction->price->value === $webservice_transaction->price->value &&
        $notification_transaction->price->appc === $webservice_transaction->price->appc;
}
#Same file as step 4
def is_valid(notification_transaction, webservice_transaction):
  return notification_transaction['uid'] == webservice_transaction['uid'] and \
          notification_transaction['domain'] == webservice_transaction['domain'] and \
          notification_transaction['product'] == webservice_transaction['product'] and \
          notification_transaction['status'] == webservice_transaction['status'] and \
          notification_transaction['reference'] == webservice_transaction['reference'] and \
          notification_transaction['price'] == webservice_transaction['price'] and \
          notification_transaction['price']['currency'] == webservice_transaction['price']['currency'] and \
          notification_transaction['price']['value'] == webservice_transaction['price']['value'] and \
          notification_transaction['price']['appc'] == webservice_transaction['price']['appc']
//Same file as step 4
async function isValid(notification_transaction, webservice_transaction) {
    return notification_transaction['uid'] == webservice_transaction['uid'] &&
    notification_transaction['domain'] == webservice_transaction['domain'] &&
    notification_transaction['product'] == webservice_transaction['product'] &&
    notification_transaction['status'] == webservice_transaction['status'] &&
    notification_transaction['reference'] == webservice_transaction['reference'] &&
    notification_transaction['price'] == webservice_transaction['price'] &&
    notification_transaction['price']['currency'] == webservice_transaction['price']['currency'] &&
    notification_transaction['price']['value'] == webservice_transaction['price']['value'] &&
    notification_transaction['price']['appc'] == webservice_transaction['price']['appc'];
}

6. Verify the transaction from the app

After the purchase is processed, use your server to notify and give the item to the user since this is the safest way.

To do this, the server needs to know the user id, this can be done in 2 ways: make the order reference contain the user id, Ex. user_id.timestamp; or add the user ID as a parameter in the callback URL.

Implementin OSP without server-side validation (no-backend)

1. Create URL

The first step is to add the functionality to your app to generate the URL as specified in OSP URL Structure.
The domain must be the package name and it must be certified in catappult even for testing purposes.

To next example shows how to create the URL:

String DOMAIN = "com.appcoins.trivialdrivesample";

public String createURL(int value, String currency, String product) {
    String url = "https://apichain.catappult.io/transaction/inapp"
                + "?value=" + value
                + "&currency=" + currency
                + "&domain=" + DOMAIN
                + "&product=" + product;
    return url; 
}
val DOMAIN = "com.appcoins.trivialdrivesample"

public fun createURL(value: Int, currency: String, product: String) {
    val url = ("https://apichain.catappult.io/transaction/inapp"
            + "?value=" + value
            + "&currency=" + currency
            + "&domain=" + DOMAIN
            + "&product=" + product)
    return url
}

2. Call the URL

Inside the app/game, an intent with the URL should be opened either using the web browser (whenever the AppCoins Wallet is not installed), or via the AppCoins Wallet.

In some cases, the user chooses to open the URL with the browser when the AppCoins Wallet is already installed or even chooses the option to always open the URL with the browser. In order to avoid this, we suggested adding the following sample to trigger the One Step billing flow.

private static int RC_ONE_STEP = 10003

Intent intent = buildTargetIntent(url);
try {
    startActivityForResult(intent, RC_ONE_STEP);
} catch (Exception e) {
    e.printStackTrace();
}
/**
* This method generates the intent with the provided One Step URL to target the
* AppCoins Wallet.
* @param url The url that generated by following the One Step payment rules
* 
* @return The intent used to call the wallet 
*/
private Intent buildTargetIntent(String url) {
  Intent intent = new Intent(Intent.ACTION_VIEW);
  intent.setData(Uri.parse(url));

  // Check if there is an application that can process the AppCoins Billing
  // flow
  PackageManager packageManager = getApplicationContext().getPackageManager();
  List<ResolveInfo> appsList = packageManager
            .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
  for (ResolveInfo app : appsList) {
    if (app.activityInfo.packageName.equals("cm.aptoide.pt")) {
      // If there's aptoide installed always choose Aptoide as default to open 
      // url
      intent.setPackage(app.activityInfo.packageName);
      break;
    } else if (app.activityInfo.packageName.equals("com.appcoins.wallet")) {
      // If Aptoide is not installed and wallet is installed then choose Wallet
      // as default to open url
      intent.setPackage(app.activityInfo.packageName);
    }
  }
  return intent;
}
val RC_ONE_STEP = 10003

var intent = buildTargetIntent(url);
try {
    startActivityForResult(intent, RC_ONE_STEP);
} catch (e: Exception) {
    e.printStackTrace();
}

/**
* This method generates the intent with the provided One Step URL to target the
* AppCoins Wallet.
* @param url The url that generated by following the One Step payment rules
* 
* @return The intent used to call the wallet 
*/
private fun buildTargetIntent(url: String): Intent {
    var intent = Intent(Intent.ACTION_VIEW)
    intent.setData(Uri.parse(url))

    // Check if there is an application that can process the AppCoins Billing
    // flow
    var packageManager = getApplicationContext().getPackageManager()
    var appsList: List<ResolveInfo> = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
    for (app in appsList) {
        if (app.activityInfo.packageName == "cm.aptoide.pt") {
            // If there's aptoide installed always choose Aptoide as default to open 
            // url
            intent.setPackage(app.activityInfo.packageName)
            break  
        } else if (app.activityInfo.packageName == "com.appcoins.wallet") {
            // If Aptoide is not installed and wallet is installed then choose Wallet
            // as default to open url
            intent.setPackage(app.activityInfo.packageName)
        }
    }
    return intent
}

Target SDK 30 and over

When the app targets SDK 30 or above you need to add the intent to AndroidManifest as such:

<manifest>
...
    <queries>
        <intent>
            <action android:name="android.intent.action.VIEW">
        </intent>
    ...
    </queries>
...
</manifest>

3. Verify the transaction from the app

To validate the purchase from the app use the transaction_hash received from the wallet, to check if the transaction was processed call the catapult transactions API, but instead of calling transactions/{uid}, use transactions/?hash=transaction_hash to receive and object To extract the order ID from the data Intent you can use the following code:

// data being the parameter Intent data in onActivityResult(int requestCode, int resultCode, Intent data)
String transactionHash = data.getStringExtra("transaction_hash");
// getTransactionData returns the response of calling the transactionAPI
String response = getTransactionData(transactionHash);
JSONObject mPurchaseData = new JSONObject(response);
// Object with the same structure as exemplified in transactionsAPI
JSONObject mTransactionData = mPurchaseData.getJSONArray("items").getJSONObject(0);
// data being the parameter data: Intent in onActivityResult(requestCode: int, resultCode: int, data: Intent)
val transactionHash = data.getStringExtra("transaction_hash")
// getTransactionData returns the response of calling the transactionAPI
val response = getTransactionData(transactionHash)
val purchase = JSONObject(transaction)
// Object with the same structure as exemplified in transactionsAPI
val transactionDetails = purchase.getJSONArray("items").getJSONObject(0)

OSP URL Structure

Scheme: https
Host: apichain.catappult.io
Path: /transaction/inapp
Query string arguments: value, currency, to, product, domain, data, callback_url, reference, signature
Example:

https://apichain.catappult.io/transaction/inapp?value=11&currency=usd&domain=com.appcoins.trivialdrivesample&product=sword.001

🚧

Warning!

Previously there was a "to" argument where the destination wallet address would be specified. This argument is no longer needed and was removed from this example.

Query String Arguments details

Parameter

Type

Description_of_field

Static

Optional

Example

value*

Double

The value of the chosen product (if no currency is set it is considered APPC)

N

N

11.5

currency

String

The currency in which the value is sent, if no currency is sent it is considered APPC (AppCoins).

N

Y

USD or EUR

product*

String

The id of the item being bought.

Y

N

sword.001

domain*

String

The application id, also known as package name.

Y

N

com.appcoins.trivialdrivesample

data

String

Additional information to be sent if needed.

N

Y

Awesome Sword

callback_url

String

The developer's URL to be called after the transaction is completed.

N

Y

https://mygamestudio.co/appcoins?out_trade_no=1234

order_reference

String

Unique identifier of the transaction created by the developer. (Cannot be used for different purchases)

N

Y

XYZ98880032

signature

String

The Hexadecimal string of the signed URL in order to be validated. For more details see section Create URL (backend)

N

Y

49bc6dac9780acfe5419eb16e862cf096994c15f807313b04f5a6ccd7717e78e

* mandatory fields

Payment Notification

If the developer provides a callback URL, our purchase service triggers a notification whenever there is a valid purchase. In order to use this service you must:

After the URL is called and the payment is processed our services make a request to the callback URL that follows the structure specified in Request to callback URL, so the developer needs to prepare the server to handle this request and finally validate if the request did come from catappult.

📘

You can check https://github.com/Catappult/appcoins-server-validator for a fully working sample.

Request to callback URL

Method: Post
URL: The provided URL
Body: signature, transaction
Content type: Json
Response code: 200

❗️

Response code

It is important that your service return 200 as status code so we acknowledge you did receive the notification successfully.

Body example

{
  "signature": "62747e2bd871b38794edf8e7e27fc525f523be4955beeb2467ba8617d10ef1ff3c39eac5c75a52c8ebc0badefec32a84616f26ebc0213fa86604567c218b74931b",
  "transaction": "{\"uid\":\"2DtyvTOSShc1xT9C\",\"domain\":\"com.appcoins.trivialdrivesample\",\"product\":\"gas\",\"reference\":\"XYZ98880032\",\"status\":\"COMPLETED\",\"added\":\"2020-05-04T10:19:44+00:00\",\"modified\":\"2020-05-04T10:19:44+00:00\",\"price\":{\"currency\":\"APPC\",\"value\":\"1\",\"appc\":\"1\"}}"
}

Body structure

Details about parameters present at the root of the json object sent when calling callback_url:

Type

Name

Description

Schema

Example

Body

signature

(Deprecated)
Notification signature based on transaction parameter

string

0x7b87a3c4dd63bee43d4c88

Body

transaction

Transaction resource in json

string

Signature

Deprecated: This argument is no longer used.

Transaction parameter fields

This parameter is a json structured string that should be parsed to get the information about the purchase itself.

Name

Description

Schema

Example

uid

Unique ID for transaction resource

string

2DtyvTOSShc1xT9C

domain

Package name

string

com.appcoins.trivialdrivesample

product

Product name (aka SKU)

string

sword.001

reference

Unique identifier of the transaction created by the developer.

string

XYZ98880032

status

Transaction status

string

COMPLETED or CANCELED or FAILED

added

Transaction added timestmap

string

2020-04-18T06:15:18+00:00

modified

Transaction modified timestmap

string

2020-04-18T07:17:19+00:00

price.appc

Transaction price in AppCoins

string

115

price.currency

Transaction price currency (used by user to perform the purchase)

string

USD, APPC, EUR, etc

price.value

Transaction price value

string

11.5

Transaction API

Method: GET
Scheme: https
HOST: api.blockchainds.com
Path: /broker/8.20200101/transactions/{uid}
Ex:

https://api.blockchainds.com/broker/8.20200101/transactions/2DtyvTOSShc1xT9C

Response:

{
  "uid": "2DtyvTOSShc1xT9C",
  "domain": "com.appcoins.trivialdrivesample",
  "product": "gas",
  "wallet_from": "0xa43748bf498d7070d05d4fde042c51c780ce71b9",
  "country": "PT",
  "type": "INAPP_UNMANAGED",
  "reference": "XYZ98880032",
  "hash": "0x234e3c2407680ffe07d4f1bb7bc5c773085cd4ca723669a0473777ceeaabab95",
  "origin": "BDS",
  "status": "COMPLETED",
  "added": "2020-05-04T10:19:45+00:00",
  "modified": "2020-05-04T10:19:45+00:00",
  "gateway": {
    "name": "appcoins_credits"
  },
  "metadata": null,
  "price": {
    "currency": "APPC",
    "value": "1",
    "appc": "1"
  }
}