Subscription

Subscription is a recurring billing payment solution of Checkout API. It allows you to bill your customer based on a specific schedule automatically. Once a user makes his first payment via subscription, our system signs him up for recurring billing.

The user will be charged based on the subscription schedule. E.g. if you specify the product length as 1 month, we will charge the user every month. Next payment will happen the same day of the next month. If a payment happens on 30th of January, the next payment will be made in first days of March.

Integrating it only requires 3 steps:

Before starting, make sure you have product selection step finished in your application. The deduction period for each products needs to be clearly defined and the total period length of the subscription shall not exceed 6 months.


Payment methods for subscription

Subscirption supports certain payment methods:

Payment method Payment system short code Notes
Credit cards cc Requires credit card activation flow.
FasterPay fasterpay  
PayPal paypal Requires spiderpipe for PayPal to be activated for your Paymentwall account.
MercadoPago mercadopago  
Mobiamo mobilegateway Mobiamo has a special coverage of price points, see Mobiamo coverage.
SEPA sepadirectdebit  
Alipay+ alipayplus  
Credit Card Korea creditcardkr  
Tosspay tosspay Confirm on billingKey

Build payment page

Payment link can be built by widget object. The configuration difference between single payment methods and all payment methods is the value of ps parameter.

See parameter definition for the requirement and meaning of each parameters while using subscription.

<?php
require_once('lib/paymentwall.php');
Paymentwall_Config::getInstance()->set(array(
    'api_type' => Paymentwall_Config::API_GOODS,
    'public_key' => 'YOUR_PROJECT_KEY',
    'private_key' => 'YOUR_SECRET_KEY'
));

$widget = new Paymentwall_Widget(
    'user40012', // uid
    'p1_1', // widget
    array(
        new Paymentwall_Product(
            'product301', // ag_external_id
            9.99, // amount
            'USD', // currencyCode
            'Gold Membership', // ag_name
            Paymentwall_Product::TYPE_SUBSCRIPTION, // ag_type
            1, // ag_period_length
            Paymentwall_Product::PERIOD_TYPE_MONTH, // ag_period_type
            true // ag_recurring
        ) 
    ),
    array(
        'email' => 'user@hostname.com', 
        'history[registration_date]' => 'registered_date_of_user',
        'ps' => 'all', // Replace it with specific payment system short code for single payment methods
        'addtional_param_name' => 'addtional_param_value' 
    )
);

echo $widget->getUrl();
?>
var Paymentwall = require('paymentwall');
Paymentwall.Configure(
    Paymentwall.Base.API_GOODS,
    'YOUR_APPLICATION_KEY',
    'YOUR_SECRET_KEY'
);

var widget = new Paymentwall.Widget(
    'user40012', // uid
    'p1_1', // widget 
    [
        new Paymentwall.Product(
            'product301', // ag_external_id
            9.99, // amount
            'USD', // currencyCode
            'Gold Membership', // ag_name
            Paymentwall.Product.TYPE_SUBSCRIPTION, // ag_type
            1, // ag_period_length
            Paymentwall.Product.PERIOD_TYPE_MONTH, // ag_period_type
            true // ag_recurring
        )
    ],
    {
    	'email': 'user@hostname.com',
    	'history[registration_date]': 'registered_date_of_user',
    	'ps': 'all', // Replace it with specific payment system short code for single payment methods
    	'additional_param_name': 'additional_param_value'
    }
);
widget.getUrl();
Config.getInstance().setLocalApiType(Config.API_GOODS);
Config.getInstance().setPublicKey("YOUR_APPLICATION_KEY");
Config.getInstance().setPrivateKey("YOUR_SECRET_KEY");

WidgetBuilder widgetBuilder = new WidgetBuilder("USER_ID", "p1_1");

widgetBuilder.setProduct( 
    new ProductBuilder("YOUR_PRODUCT_ID") {
    {
        setAmount(0.99);
        setCurrencyCode("USD");
        setName("YOUR_PRODUCT_NAME");
        setProductType(Product.TYPE_SUBSCRIPTION);
        setPeriodType("month");
        setPeriodLength(3);
    }
}.build());

widgetBuilder.setExtraParams(new LinkedHashMap<String, String>(){
{
    put("email", "YOUR_CUSTOMER_EMAIL");
    put("history[registration_date]","REGISTRATION_DATE");
    put("ps","all"); // Replace it with specific payment system short code for single payment methods
}
});

Widget widget = widgetBuilder.build();

return widget.getUrl();
require 'paymentwall' # alternatively, require_relative '/path/to/paymentwall-ruby/lib/paymentwall.rb'
Paymentwall::Base::setApiType(Paymentwall::Base::API_GOODS)
Paymentwall::Base::setAppKey('YOUR_APPLICATION_KEY')
Paymentwall::Base::setSecretKey('YOUR_SECRET_KEY')

widget = Paymentwall::Widget.new(
    'user40012',
    'p1',
    [
        Paymentwall::Product.new(
            'product301',
            9.99,
            'USD',
            'Gold Membership',
            Paymentwall::Product::TYPE_SUBSCRIPTION,
            1,
            Paymentwall::Product::PERIOD_TYPE_MONTH,
            true
        )
    ],
    {
        'email' => 'user@hostname.com',
        'history[registration_date]' => 'registered_date_of_user',
        'ps' => 'all', # Replace it with specific payment system short code for single payment methods
        'additional_param_name' => 'additional_param_value'
    }
)
puts widget.getUrl()
from paymentwall import *
Paymentwall.set_api_type(Paymentwall.API_GOODS)
Paymentwall.set_app_key('YOUR_PROJECT_KEY')
Paymentwall.set_secret_key('YOUR_SECRET_KEY')

product = Product(
    'product301',              # id of the product in your system 
    9.99,                     # price
    'USD',                     # currency code
    'Gold Membership',                    # product name
    Product.TYPE_SUBSCRIPTION, # this is a time-based product
    1,                         # duration is 1
    Product.PERIOD_TYPE_WEEK,  #               week
    True                       # recurring
)

widget = Widget(
    'user40012', # uid
    'p1_1', # widget
    [product],
    {
        'email' : 'user@hostname.com',
        'history[registration_date]' : 'registered_date_of_user',
        'ps' : 'all', # Replace it with specific payment system short code for single payment methods
        'additional_param_name' : 'additional_param_value'
    }
)
print(widget.get_url())
using Paymentwall;

Paymentwall_Base.setApiType(Paymentwall_Base.API_GOODS);
Paymentwall_Base.setAppKey("YOUR_PROJECT_KEY"); 
Paymentwall_Base.setSecretKey("YOUR_SECRET_KEY");

List<Paymentwall_Product> productList = new List<Paymentwall_Product>();
Paymentwall_Product product = new Paymentwall_Product(
    "product301", // ag_external_id
    9.99, // amount
    "USD", // currencyCode
    "Gold Membership", // ag_name
    Paymentwall_Product.TYPE_SUBSCRIPTION, // ag_type
    1, // ag_period_length
    Paymentwall_Product.PERIOD_TYPE_YEAR, // ag_period_type
    true // ag_recurring
);
productList.Add(product);
Paymentwall_Widget widget = new Paymentwall_Widget(
    "user40012", 
    "p1_1", 
    productList,
    new Dictionary<string, string>() {
        {
            'email' => 'user@hostname.com',
            'history[registration_date]' => 'registered_date_of_user',
            'ps' => 'all', // Replace it with specific payment system short code for single payment methods
            'additional_param_name' => 'additional_param_value'
        }
    }
);
Response.Write(widget.getUrl());

Handle pingback

Every time a user is billed, we will send a new pingback to inform you the customer has make a payment for his subscription. On your server side, put the following code as an online server interface to interact with our Pingback:

<?php
require_once('/path/to/paymentwall-php/lib/paymentwall.php');
Paymentwall_Config::getInstance()->set(array(
    'api_type' => Paymentwall_Config::API_GOODS,
    'public_key' => 'YOUR_APPLICATION_KEY', // available in your Paymentwall merchant area
    'private_key' => 'YOUR_SECRET_KEY', // available in your Paymentwall merchant area
));

$pingback = new Paymentwall_Pingback($_GET, $_SERVER['REMOTE_ADDR']);
if ($pingback->validate(true)) {
    $productId = $pingback->getProduct()->getId();
    if ($pingback->isDeliverable()) {
        // deliver the product
    } else if ($pingback->isCancelable()) {
        // withdraw the product
    } else if ($pingback->isUnderReview()) {
        // set "pending" as order status
    }
    echo 'OK'; // Paymentwall expects response to be OK, otherwise the pingback will be resent
} else {
    echo $pingback->getErrorSummary();
}
?>
var Paymentwall = require('paymentwall');
Paymentwall.Configure(
    Paymentwall.Base.API_GOODS,
    'YOUR_PROJECT_KEY',
    'YOUR_SECRET_KEY'
);

var pingback = new Paymentwall.Pingback("query data in pingback request", "ip address of pingback");
if (pingback.validate(true)) {
    var productId = pingback.getProduct().getId();
    if (pingback.isDeliverable()) {
        // deliver the product
    } else if (pingback.isCancelable()) {
        // withdraw the product
    } 
    console.log('OK'); // Paymentwall expects the string OK in response, otherwise the pingback will be resent
} else {
    console.log(pingback.getErrorSummary());
}
import com.paymentwall.java.*;

Config.getInstance().setLocalApiType(Config.API_GOODS);
Config.getInstance().setPublicKey("YOUR_PROJECT_KEY");
Config.getInstance().setPrivateKey("YOUR_SECRET_KEY");

Pingback pingback = new Pingback(request.getParameterMap(), request.getRemoteAddr());
if (pingback.validate(true)) {
    String goods = pingback.getProductId();
    String userId = pingback.getUserId();
    if (pingback.isDeliverable()) {
        // deliver Product to user with userId
    } else if (pingback.isCancelable()) {
        // withdraw Product from user with userId
    }
    return "OK";
} else {
    return pingback.getErrorSummary();
}
require 'paymentwall' # alternatively, require_relative '/path/to/paymentwall-ruby/lib/paymentwall.rb'
Paymentwall::Base::setApiType(Paymentwall::Base::API_GOODS)
Paymentwall::Base::setAppKey('YOUR_PROJECT_KEY') # available in your Paymentwall merchant area
Paymentwall::Base::setSecretKey('YOUR_SECRET_KEY') # available in your Paymentwall merchant area

pingback = Paymentwall::Pingback.new(request_get_params, request_ip_address)
if pingback.validate(true)
    productId = pingback.getProduct().getId()
    if pingback.isDeliverable()
        # deliver the product
    elsif pingback.isCancelable()
        # withdraw the product
    end 
    puts 'OK' # Paymentwall expects response to be OK, otherwise the pingback will be resent
else
    puts pingback.getErrorSummary()
end
from paymentwall import *
Paymentwall.set_api_type(Paymentwall.API_GOODS)
Paymentwall.set_app_key('YOUR_PROJECT_KEY') # available in your merchant area
Paymentwall.set_secret_key('YOUR_SECRET_KEY') # available in your merchant area

pingback = Pingback({x:y for x, y in request.args.iteritems()}, request.remote_addr)

if pingback.validate(True):
    product_id = pingback.get_product().get_id()
    if pingback.is_deliverable():
        # deliver the product
        pass
    elif pingback.is_cancelable():
        # withdraw the product
        pass

    print('OK') # Paymentwall expects response to be OK, otherwise the pingback will be resent

else:
    print(pingback.get_error_summary())
using Paymentwall;

Paymentwall_Base.setApiType(Paymentwall_Base.API_GOODS);
Paymentwall_Base.setAppKey("YOUR_PROJECT_KEY"); // available in your Paymentwall merchant area
Paymentwall_Base.setSecretKey("YOUR_SECRET_KEY"); // available in your Paymentwall merchant area


NameValueCollection parameters = Request.QueryString;
Paymentwall_Pingback pingback = new Paymentwall_Pingback(parameters, HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"]);
if (pingback.validate(true))
{
    string productId = pingback.getProduct().getId();
    if (pingback.isDeliverable())
    {
        //deliver the product
    }
    else if (pingback.isCancelable())
    {
        //withdraw the product
    }
    Response.Write("OK"); // Paymentwall expects response to be OK, otherwise the pingback will be resent
}
else {
    Response.Write(pingback.getErrorSummary());
}

A pingback request typically contains all the information for you to do the product delivery. As an addition,Paymentwall provides a series of reversed parameters as custom pingback parameters for specific needs, you can also add your own parameters as custom pingback parameter in order to implement parameter transmission.

Below is a sample with default format:

http://www.yourserver.com/pingback_path?uid=pwuser&goodsid=gold_membership&slength=3&speriod=day&type=0&ref=b1493048890&sign_version=2&sig=d94b23ba8585f29978706dd1b153ead9

After validating the pingback, your server is always expected to be able to proceed the delivery process and respond to it with only OK in the body of response.

Other than successful subscription, there are other cases for pingback:

  • From the moment that the recurring was cancelled, Paymentwall will send a pingback with type=12. A subscription can be cancelled by either cancelled via submitting a ticket by using cancellation API or deactivating in Dashboard.

  • A pingback with type=13 is always generated when the recurring billing period is expired.

  • If a user has insufficient funds or a payment fails for any other reason, Paymentwall will make 2 re-attempts (3 attempts in total) to charge the user. Payment Status API can report active status of the subscription with date_next for the date of next attempt. The subscription will be stopped if all of the attempts fail. And once it is stopped, it will no longer process an attempt to charge the user for the next scheduled payment. Paymentwall will send a pingback with type=14 in this case , means the corresponding recurring billing payment failed.


Implement Delivery Confirmation API

Delivery Confirmation API allows you to notify Paymentwall about successful delivery of the purchased item(s) to the user. This information helps us resolve dispute cases and refund requests in the fastest and the most efficient manner.

This API expects secret key to be sent as custom HTTPS header “X-ApiKey”.


<?php
require_once('path/to/lib/paymentwall.php');
Paymentwall_Config::getInstance()->set(array(
    'private_key' => '[YOUR_SECRET_KEY]'
));

$delivery = new Paymentwall_GenerericApiObject('delivery');

$response = $delivery->post(array(
    'payment_id' => 'b63400368',
    'merchant_reference_id' => 'order_12345',
    'type' => 'digital',
    'status' => 'delivered',
    'estimated_delivery_datetime' => '2015/01/15 15:00:00 +0300',
    'estimated_update_datetime' => '2015/01/15 11:00:00 +0300',
    'refundable' => true,
    'details' => 'Item was delivered to the user account and via email',
    'shipping_address[email]' => 'user@hostname.com',
    'reason' => 'none',
    'attachments[0]' => '@/usr/local/www/content/proof/b63400368/1.png',
    'attachments[1]' => '@/usr/local/www/content/proof/b63400368/2.png',
));
if (isset($response['success'])) {
    // delivery status is successfully saved
} elseif (isset($response['error'])) {
    var_dump($response['error'], $response['notices']);
}
?>
'use strict';
var HttpAction = require('paymentwall/lib/HttpAction'),
    util = require('util'),
    querystring = require('querystring'),
    ApiObject = require('paymentwall/lib/ApiObject'),
    Paymentwall = require('paymentwall');

Paymentwall.Configure(
    Paymentwall.Base.API_GOODS,
    'YOUR_PROJECT_KEY ',
    'YOUR_SECRET_KEY '
);

var api = new ApiObject();
api.createDeliveryRequest = function() {
    var url = this.BRICK_BASE_URL;
    var method = 'POST';
    var post_options = this.createPostOptions(url, '/api/delivery', method);
    return post_options;
};

var post_options = api.createDeliveryRequest();

var post_data = {
    "payment_id" : "b63400368",
    "merchant_reference_id" : "order_12345",
    "type" : "digital",
    "status" : "delivered",
    "estimated_delivery_datetime" : "2018/08/23 15:00:00 +0300",
    "estimated_update_datetime" : "2018/08/23 11:00:00 +0300",
    "refundable" : true,
    "details" : "Item was delivered to the user account and via email",
    "shipping_address[email]" : "user@hostname.com",
    "reason" : "none",
    "attachments" : {}
};

post_data = querystring.stringify(post_data);

HttpAction.runAction(post_options, post_data, true, function(response) {
    response = response.JSON_chunk;
    if(response.success) {
        // delivery status is successfully saved
    } else if(response.error) {
        console.log(response.error);
        console.log(response.notices);
    }
});
private void sendPost() {

    HttpPost post = new HttpPost("https://api.paymentwall.com/api/delivery");
    post.addHeader("X-ApiKey", <PROJECT_SECRET_KEY>);

    // add request parameter, form parameters
    List<NameValuePair> urlParameters = new ArrayList<NameValuePair>();
    urlParameters.add(new BasicNameValuePair("payment_id", "b199488072"));
    urlParameters.add(new BasicNameValuePair("merchant_reference_id", "w199488072"));
    urlParameters.add(new BasicNameValuePair("type", "digital"));
    urlParameters.add(new BasicNameValuePair("status", "order_placed"));
    urlParameters.add(new BasicNameValuePair("estimated_delivery_datetime", "2015/01/15 15:00:00 +0300"));
    urlParameters.add(new BasicNameValuePair("estimated_update_datetime", "2015/01/15 11:00:00 +0300"));
    urlParameters.add(new BasicNameValuePair("refundable", "true"));
    urlParameters.add(new BasicNameValuePair("details", "Item was delivered to the user account and via email"));
    urlParameters.add(new BasicNameValuePair("shipping_address[email]", "test@paymentwall.com"));
    urlParameters.add(new BasicNameValuePair("reason", "none"));
    urlParameters.add(new BasicNameValuePair("attachments[0]", "@/usr/local/www/content/proof/b63400368/1.png"));
    urlParameters.add(new BasicNameValuePair("attachments[1]", "@/usr/local/www/content/proof/b63400368/2.png"));

    try {
        post.setEntity(new UrlEncodedFormEntity(urlParameters));
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = httpClient.execute(post);
        System.out.println(EntityUtils.toString(response.getEntity()));
    } 
    catch(IOException ex) {
        System.out.println(ex.getMessage());
    }

}
curl https://api.paymentwall.com/api/delivery \
-H "X-ApiKey: [YOUR_SECRET_KEY]" \
-d "payment_id=b111260108" \
-d "merchant_reference_id=order_12345" \
-d "type=digital" \
-d "status=started" \
-d "is_test=1" \
-d "estimated_delivery_datetime=2017/01/15 15:04:55 +0500" \
-d "estimated_update_datetime=2017/01/16 15:04:55 +0500" \
-d "refundable=false" \
-d "details=Item was delivered to the user account and via email" \
-d "shipping_address[email]=test@paymentwall.com"
-d "reason=none"

Client-side action

In most cases, a complete payment experience is required wherein the end-users can be redirected back to the original application after making a payment.

There would be a link shown up in the payment successful page which is hosted by Paymentwall or our partners if you have success_url presented as optional parameter while building the link of payment page. Your customers are then able to click it and go back to your applications.

Alternatively, client-side callback is suitable if you have additional requirements about client-side actions once a payment is finished.


Relevant topics