In-app checkout page provides you with the flexibility to be able to provide a unique checkout experience for your users. The drawbacks are you need to ensure security. Also if you want to show a order summary , then you would have to do the various calculations (like tax , coupon support) etc. Chargebee's Estimate API allows you to dynamically update the order summary based on user selections without having to hard-code the prices or validate coupons.
Alternative Options
The simplest way to setup your checkout process with Chargebee is by using the hosted payment pages or the Hosted Pages + API integration method. Chargebee’s hosted pages are built using the Bootstrap themes.
Overview
What is Estimate API?
Estimate api allows you to calculate the 'invoice' that would be raised on completion of a operation such as creating a subscription or updating a subscription. Using the returned estimate you could show a order summary. This allows you to dynamically update the order summary based on user selection. You can avoid hard-coding the prices or validating the coupon codes for updating it.
How do I use it?
Whenever a user selects an addon or applies a coupon you just need to call the estimate api to get the line items for the order summary. Additionally if you want allow users to apply coupon codes, you should handle the coupon errors such as wrong code and show the message appropriately.
Honey Comics - Demo Application
'Honey Comics', our demo application, is a fictitious online comic book store providing a subscription service for comics. We send physical comic books every month. In addition to comic books we sell wallpapers of the famous heros and also provide a option for users to opt for a e-book version of the comic to be sent along with comics. The wallpapers and e-book are modeled as add-ons in Chargebee.
Prerequisites
To try out the tutorial yourself, you'll need the following:
- A Chargebee account. Signup for a free trial if you don't have one.
- Create the following configuration in your Chargebee site. (Note: You can setup the plan and addons for the demo using the "Setup Configuration" option in the index page if you have downloaded code and started the tutorials locally).
- A plan with id 'monthly' in your Chargebee Site.
- A 'On Off' addon with id 'e-book' in Chargebee.
- A 'Quantity' type addon with id 'wall-poster' in Chargebee.
- Your Chargebee API key for your test site.
Setup the Chargebee client library
You have to download and import the client library of our choice. Then, configure the client library with your test site and its api key.
For the tutorial, we have configured the site and the credentials in a separate properties file. When the webapp is initialized, the client library gets configured.
/**
* The credentials are stored in a properties file under WEB-INF
* The live site api keys should be stored securely. It should preferably
* be stored only in the production machine(s) and not hard coded
* in code or checked into a version control system by mistake.
*/
Properties credentials = read("WEB-INF/ChargeBeeCredentials.properties");
Environment.configure(credentials.getProperty("site"), credentials.getProperty("api_key"));
For the tutorial, we have configured the site credentials in config/environments/development.rb
ENV["CHARGEBEE_SITE"]="honeycomics-test"
ENV["CHARGEBEE_API_KEY"]="test_5LjFA6K6doB2EKRP7cufTd5TvT32a5BrT"
We setup the client library in config/initializers/chargebee.rb
ChargeBee.configure(:site => ENV["CHARGEBEE_SITE"], :api_key => ENV["CHARGEBEE_API_KEY"])
For the tutorial, we have configured the site credentials in Config.php
require_once(dirname(__FILE__) . "/lib/ChargeBee.php");
/*
* Sets the environment for calling the Chargebee API.
* You need to sign up at ChargeBee app to get this credential.
* It is better if you fetch configuration from the environment
* properties instead of hard coding it in code.
*/
ChargeBee_Environment::configure("honeycomics-test", "test_5LjFA6K6doB2EKRP7cufTd5TvT32a5BrT");
Implementing the dynamic order summary using Estimate api
Requesting updated order summary in client
We will start with the client side implementation. Whenever the user changes e-book option (or updates wallpaper quantity or applies a coupon) we will request the server for the updated order summary. We do this via javascript as shown below
$('#order_summary').on('click', '#apply-coupon', function(e) {
if ($('#coupon').val().trim() == '') {
$('.error_msg').text("invalid coupon code");
$('.error_msg').show();
} else {
sendAjaxRequest();
}
})
$('#order_summary').on('click', '#remove-coupon', function(e) {
$('#coupon').removeAttr("value");
sendAjaxRequest();
})
$('.addons').on('change', '.wallposters-quantity', function(e) {
sendAjaxRequest();
})
$('.addons').on('change', '.ebook', function(e) {
sendAjaxRequest();
})
function sendAjaxRequest() {
var wallpostersQuantity, ebook, coupon;
if ($('.wallposters').is(":checked")) {
wallpostersQuantity = $('.wallposters-quantity').val();
}
if ($('.ebook').is(':checked')) {
ebook = "true";
}
if ($('#coupon').val().trim() != '') {
coupon = $('#coupon').val().trim();
}
parameters = {"wallposters-quantity": wallpostersQuantity,
"ebook": ebook,
"coupon": coupon
}
orderSummaryAjaxHandler(parameters)
}
function orderSummaryAjaxHandler(dataContent) {
$.ajax({
url: "order_summary",
data: dataContent,
beforeSend: function(data, textstatus, jqXHR) {
$('.text-danger').text('');
$('.ajax-loader').show();
},
success: function(data, textstatus, jqXHR) {
$('#order_summary').html(data);
},
error: function(data, textstatus, jqXHR) {
try {
var error = JSON.parse(data.responseText);
$('.error_msg').text(error.error_msg);
} catch (e) {
$('.error_msg').text("Internal Server Error");
}
$('.error_msg').show();
},
complete: function() {
$('.ajax-loader').hide();
}
});
}
Handling order summary requests in server
We will invoke the invoke the estimate api with the various options selected by the user.
/*
* Returns estimate object by applying the addons and coupons set by user.
*/
public static Estimate getOrderSummary(HttpServletRequest request)
throws IOException, Exception {
/*
* Forming create subscription estimate parameters to ChargeBee.
*/
Estimate.CreateSubscriptionRequest estimateReq = Estimate.
createSubscription().subscriptionPlanId("monthly");
/*
* Adding addon1 to the create subscription estimate request,
* if it is set by user.
*/
if (request.getParameter("wallposters-quantity") != null &&
!"".equals(request.getParameter("wallposters-quantity"))){
String quantityParam = request.getParameter("wallposters-quantity");
Integer quantity = Integer.parseInt(quantityParam);
estimateReq.addonId(0, "wall-posters")
.addonQuantity(0, quantity);
}
/*
* Adding addon2 to the create subscription estimate request,
* if it is set by user.
*/
if (request.getParameter("ebook") != null &&
"true".equals(request.getParameter("ebook"))){
estimateReq.addonId(1, "e-book");
}
/*
* Adding coupon to the create subscription estimate request,
* if it is set by user.
*/
if (request.getParameter("coupon") != null &&
!"".equals(request.getParameter("coupon"))){
estimateReq.subscriptionCoupon(request.getParameter("coupon"));
}
/*
* Sending request to the ChargeBee.
*/
Result result = estimateReq.request();
Utils.log("estimate.json", "estimate_response", result);
return result.estimate();
}
# Calls the ChargeBee Estimate API to find the total amount for checkout.
def estimate_result(_params)
# Forming create subscription estimate parameters to ChargeBee.
subscription_params = { :plan_id => "monthly" }
# Adding addon1 to the create subscription estimate request, if it is set by user.
if _params['coupon'] !=nil
subscription_params[:coupon] = _params['coupon']
end
addons = Array.new
# Adding addon1 to the addons array, if it is set by user.
if _params['wallposters-quantity'] != nil
addon1 = { :id => "wall-posters",
:quantity => params['wallposters-quantity']
}
addons.push addon1
end
# Adding ebook to the addons array, if it is set by user.
if _params['ebook'] != nil
ebook = { :id => "e-book" }
addons.push ebook
end
# Sending request to the ChargeBee.
result = ChargeBee::Estimate.create_subscription({
:subscription => subscription_params,
:addons => addons
})
return result.estimate.invoice_estimate
end
/*
* Forming create subscription estimate parameters to ChargeBee.
*/
$subParams = array("planId" => "monthly");
/*
* Adding coupon to the create subscription estimate request, if it is set by user.
*/
if (isset($_GET['coupon']) && $_GET['coupon'] != "") {
$subParams["coupon"] = $_GET['coupon'];
}
$addons = array();
/*
* Adding addon1 to the addons array, if it is set by user.
*/
if (isset($_GET['wallposters-quantity']) &&
$_GET['wallposters-quantity'] != "") {
$wallPosters = array("id" => "wall-posters",
"quantity" => $_GET['wallposters-quantity']);
array_push($addons, $wallPosters);
}
/*
* Adding addon2 to the addons array, if it is set by user.
*/
if (isset($_GET['ebook']) && $_GET['ebook'] != "") {
$ebook = array("id" => "e-book");
array_push($addons, $ebook);
}
/*
* Adding subscription and addons params to the create subscription estimate request
*/
$params = array("subscription" => $subParams);
$params["addons"] = $addons;
$result = ChargeBee_Estimate::createSubscription($params);
$invoiceEstimate = $result->estimate()->invoiceEstimate;
The response(as shown below) contains the estimate based on the options. We use it to create our order summary snippet which we send back to client.
{
"estimate": {
"created_at": 1464359301,
"invoice_estimate": {
"amount_due": 15300,
"amount_paid": 0,
"credits_applied": 0,
"discounts": [
{
"amount": 1700,
"description": "SPIDERMAN25",
"entity_id": "SPIDERMAN25",
"entity_type": "document_level_coupon",
"object": "discount"
}
],
"line_item_taxes": [],
"line_items": [
{
"amount": 1000,
"date_from": 1464359301,
"date_to": 1467037701,
"description": "monthly",
"discount_amount": 100,
"entity_id": "monthly",
"entity_type": "plan",
"id": "li_HtZEwLjPmPeOXR1MDc",
"is_taxed": false,
"item_level_discount_amount": 0,
"object": "line_item",
"quantity": 1,
"tax_amount": 0,
"unit_amount": 1000
},
{
"amount": 15000,
"date_from": 1464359301,
"date_to": 1467037701,
"description": "wall-posters",
"discount_amount": 1500,
"entity_id": "wall-posters",
"entity_type": "addon",
"id": "li_HtZEwLjPmPeOXR1MDd",
"is_taxed": false,
"item_level_discount_amount": 0,
"object": "line_item",
"quantity": 3,
"tax_amount": 0,
"unit_amount": 5000
},
{
"amount": 1000,
"date_from": 1464359301,
"date_to": 1467037701,
"description": "e-book",
"discount_amount": 100,
"entity_id": "e-book",
"entity_type": "addon",
"id": "li_HtZEwLjPmPeOXR1MDe",
"is_taxed": false,
"item_level_discount_amount": 0,
"object": "line_item",
"quantity": 1,
"tax_amount": 0,
"unit_amount": 1000
}
],
"object": "invoice_estimate",
"price_type": "tax_exclusive",
"recurring": true,
"sub_total": 17000,
"taxes": [],
"total": 15300
},
"object": "estimate",
"subscription_estimate": {
"next_billing_at": 1467037701,
"object": "subscription_estimate",
"status": "active"
}
}
}
<% List<InvoiceEstimate.LineItem> lineItems = invoiceEstimate.lineItems(); %>
<div class="row">
<div class="col-xs-12">
<div class="page-header">
<h3>Your Order Summary</h3>
</div>
<ul class="text-right list-unstyled">
<% for (LineItem li : lineItems) { %>
<li class="row">
<span class="col-xs-8">
<%=Utils.esc(li.description())%>
<%=" × " + Utils.esc(li.quantity() + " item(s)")%>
</span>
<span class="col-xs-4">$
<label>
<%= Utils.esc(String.format("%d.%02d", li.amount() / 100, li.amount() % 100))%>
</label>
</span>
</li>
<% } %>
<div class="page-header"><h3>Your Order Summary</h3></div>
<ul class="text-right list-unstyled">
<% formatter = lambda { |no| "#{no/100}.#{no%100}" }
if @invoice_estimate.line_items != nil
@invoice_estimate.line_items.each do |li| %>
<li class="row">
<span class="col-xs-8">
<%= li.description + " x " + li.quantity.to_s + " item(s)"%>
</span>
<span class="col-xs-4">$
<label>
<%= formatter[li.amount()] %>
</label>
</span>
</li>
<% end
end %>
<?php
foreach ($invoiceEstimate->lineItems as $li) {
?>
<li class="row">
<span class="col-xs-8">
<?php echo esc($li->description) . " × " .
esc($li->quantity) . " item(s)" ?>
</span>
<span class="col-xs-4">$
<label>
<?php echo number_format($li->amount / 100, 2, '.', '') ?>
</label>
</span>
</li>
<?php
}
?>
Implementing checkout.
When user subscribes by providing card and other details we first validate the inputs in client for immediate feedback incase of error. We are using Stripe's client validation library for the card related validations.
var validatePaymentDetails = function(form) {
var errorMap = {};
if (!$.payment.validateCardNumber($('#card_no').val())) {
errorMap[$('#card_no').attr('name')] = 'invalid card number';
}
if (!$.payment.validateCardExpiry($('#expiry_month').val(),
$('#expiry_year').val())) {
errorMap[$('#expiry_month').attr('name')] = 'invalid expiry date';
}
if (!$.payment.validateCardCVC($('#cvc').val(),
$.payment.cardType($('#card_no').val()) ) ) {
errorMap[$('#cvc').attr('name')] = 'invalid cvc number';
}
if(jQuery.isEmptyObject(errorMap)){
return true;
}else{
$(form).validate().showErrors(errorMap);
return false;
}
};
On the server side we fill the parameters required for create subscription api.
/*
* Forming create subscription request parameters to ChargeBee.
*/
Subscription.CreateRequest createSubcriptionRequest = Subscription.create()
.planId("monthly")
.customerFirstName(request.getParameter("customer[first_name]"))
.customerLastName(request.getParameter("customer[last_name]"))
.customerEmail(request.getParameter("customer[email]"))
.customerPhone(request.getParameter("customer[phone]"))
.cardNumber(request.getParameter("card_no"))
.cardExpiryMonth(Integer.parseInt(request.getParameter("expiry_month")))
.cardExpiryYear(Integer.parseInt(request.getParameter("expiry_year")))
.cardCvv(request.getParameter("cvc"));
# Forming create subscription request parameters to ChargeBee
# Note : Here customer object received from client side is sent directly
# to ChargeBee.It is possible as the html form's input names are
# in the format customer[<attribute name>] eg: customer[first_name]
# and hence the $_POST["customer"] returns an associative array of the attributes.
create_subscription_params = {:plan_id => "monthly",
:customer => params['customer'],
:card => { :number => params['card_no'],
:expiry_month => params['expiry_month'],
:expiry_year => params['expiry_year'],
:cvv => params['cvc']
}}
/*
* Forming create subscription request parameters to ChargeBee.
* Note : Here customer object received from client side is sent directly
* to ChargeBee.It is possible as the html form's input names are
* in the format customer[<attribute name>] eg: customer[first_name]
* and hence the $_POST["customer"] returns an associative array of the attributes.
*/
$createSubscriptionParam = array(
"planId" => "monthly",
"customer" => $_POST['customer'],
"card" => array("number" => $_POST['card_no'],
"expiryMonth" => $_POST['expiry_month'],
"expiryYear" => $_POST['expiry_year'],
"cvv" => $_POST['cvc']
));
We also add the addons and coupons based on the customer's input
/*
* Adding addon1 to the create subscription request, if it is set by user.
*/
if(request.getParameter("wallposters-quantity") != null &&
!"".equals(request.getParameter("wallposters-quantity"))) {
String quantityParam = request.getParameter("wallposters-quantity");
Integer quantity = Integer.parseInt(quantityParam);
createSubcriptionRequest.addonId(0, "wall-posters")
.addonQuantity(0, quantity);
}
addons = Array.new
# Adding addons to the create subscription request parameters
create_subscription_params[:addons] = addons
# Adding addon1 to the addons array, if it is set by user.
if params['wallposters-quantity'] != nil
addon1 = { :id => "wall-posters",
:quantity => params['wallposters-quantity']
}
addons.push(addon1)
end
$addons = array();
/*
* Adding addons to the create subscription request parameters
*/
$createSubscriptionParam['addons'] = $addons;
/*
* Adding addon1 to the addons array, if it is set by user.
*/
if(isset($_POST['wallposters-quantity']) &&
$_POST['wallposters-quantity'] != "") {
$wallPosters = array("id" => "wall-posters",
"quantity" => $_POST['wallposters-quantity']) ;
array_push($addons, $wallPosters);
}
We then invoke the api. The response(as shown below) contains the subscription details which we could store in our db. We also update the shipping address via a separate api call. At the end we forward the user to the 'thank you' page.
{
"card": {
"card_type": "visa",
"customer_id": "1sjs9ilPmPes9A19xq",
"expiry_month": 1,
"expiry_year": 2020,
"gateway": "chargebee",
"iin": "411111",
"last4": "1111",
"masked_number": "************1111",
"object": "card",
"status": "valid"
},
"customer": {
"allow_direct_debit": false,
"auto_collection": "on",
"card_status": "valid",
"created_at": 1464359415,
"email": "john@acmeinc.com",
"excess_payments": 0,
"first_name": "John",
"id": "1sjs9ilPmPes9A19xq",
"last_name": "Doe",
"object": "customer",
"payment_method": {
"gateway": "chargebee",
"object": "payment_method",
"reference_id": "tok_1sjs9ilPmPes9f19xr",
"status": "valid",
"type": "card"
},
"phone": "1956781276",
"promotional_credits": 0,
"refundable_credits": 0,
"taxability": "taxable"
},
"invoice": {
"adjustment_credit_notes": [],
"amount_adjusted": 0,
"amount_due": 0,
"amount_paid": 15300,
"applied_credits": [],
"billing_address": {
"first_name": "John",
"last_name": "Doe",
"object": "billing_address"
},
"credits_applied": 0,
"currency_code": "USD",
"customer_id": "1sjs9ilPmPes9A19xq",
"date": 1464359415,
"discounts": [{
"amount": 1700,
"description": "SPIDERMAN25",
"entity_id": "SPIDERMAN25",
"entity_type": "document_level_coupon",
"object": "discount"
}],
"first_invoice": true,
"id": "850",
"issued_credit_notes": [],
"line_items": [
{
"amount": 1000,
"date_from": 1464359415,
"date_to": 1467037815,
"description": "monthly",
"discount_amount": 100,
"entity_id": "monthly",
"entity_type": "plan",
"id": "li_1sjs9ilPmPesAa19xt",
"is_taxed": false,
"item_level_discount_amount": 0,
"object": "line_item",
"quantity": 1,
"tax_amount": 0,
"unit_amount": 1000
},
{
"amount": 15000,
"date_from": 1464359415,
"date_to": 1467037815,
"description": "wall-posters",
"discount_amount": 1500,
"entity_id": "wall-posters",
"entity_type": "addon",
"id": "li_1sjs9ilPmPesAc19xu",
"is_taxed": false,
"item_level_discount_amount": 0,
"object": "line_item",
"quantity": 3,
"tax_amount": 0,
"unit_amount": 5000
},
{
"amount": 1000,
"date_from": 1464359415,
"date_to": 1467037815,
"description": "e-book",
"discount_amount": 100,
"entity_id": "e-book",
"entity_type": "addon",
"id": "li_1sjs9ilPmPesAc19xv",
"is_taxed": false,
"item_level_discount_amount": 0,
"object": "line_item",
"quantity": 1,
"tax_amount": 0,
"unit_amount": 1000
}
],
"linked_orders": [],
"linked_payments": [{
"applied_amount": 15300,
"applied_at": 1464359415,
"txn_amount": 15300,
"txn_date": 1464359415,
"txn_id": "txn_1sjs9ilPmPesBh19xx",
"txn_status": "success"
}],
"object": "invoice",
"paid_at": 1464359415,
"price_type": "tax_exclusive",
"recurring": true,
"status": "paid",
"sub_total": 17000,
"subscription_id": "1sjs9ilPmPes9A19xq",
"tax": 0,
"total": 15300,
"write_off_amount": 0
},
"subscription": {
"activated_at": 1464359415,
"addons": [
{
"id": "wall-posters",
"object": "addon",
"quantity": 3
},
{
"id": "e-book",
"object": "addon",
"quantity": 1
}
],
"coupon": "SPIDERMAN25",
"coupons": [{
"applied_count": 1,
"coupon_id": "SPIDERMAN25",
"object": "coupon"
}],
"created_at": 1464359415,
"current_term_end": 1467037815,
"current_term_start": 1464359415,
"customer_id": "1sjs9ilPmPes9A19xq",
"due_invoices_count": 0,
"has_scheduled_changes": false,
"id": "1sjs9ilPmPes9A19xq",
"object": "subscription",
"plan_id": "monthly",
"plan_quantity": 1,
"started_at": 1464359415,
"status": "active"
}
}
/*
* Sending request to the ChargeBee.
*/
Result result = createSubcriptionRequest.request();
/*
* Adds shipping address to the subscription using the subscription Id
* returned during create subscription response.
*/
addShippingAddress(result.subscription().id(), request);
/*
* Forwarding to thank you page.
*/
respJson.put("forward", "thankyou.html");
out.write(respJson.toString());
# Sending request to the ChargeBee.
result = ChargeBee::Subscription.create( create_subscription_params )
# Adds shipping address to the subscription using the subscription Id
# returned during create subscription response.
shipping_address(result, params)
# Forwarding to thank you page.
render json: { :forward => "thankyou.html" }
/*
* Sending request to the ChargeBee.
*/
$result = ChargeBee_Subscription::create($createSubscriptionParam);
/*
* Adds shipping address to the subscription using the subscription Id
* returned during create subscription response.
*/
addShippingAddress($result->subscription()->id, $result->customer());
/*
* Forwarding to thank you page.
*/
$jsonResp = array("forward"=> "thankyou.html");
print(json_encode($jsonResp,true));
Validation and Error Handling
Here's how we validate user inputs and handle API call errors in this demo:
- Client Side Validation: Chargebee uses jQuery form validation plugin to check whether the user’s field inputs(email, zip code and phone number) are valid or not.
Credit Card Validation: We use Stripe's client validation library to validate credit card details.
Server Side Validation: As this is a demo application we have skipped the server side validation of all input parameters. But we recommend you to perform the validation at your end.
Coupon Errors: If a user enters a coupon that is invalid, expired, exhausted or not applicable to a given plan, Chargebee returns an error response(as shown below) that contains the following attributes: api_error_code, param. Using these attributes you will be able to identify different coupon errors and display custom error messages.
{ "api_error_code": "resource_not_found", "error_code": "referenced_resource_not_found", "error_msg": "The SPIDERMAN25 referenced in parameter subscription[coupon] is not present ", "error_param": "subscription[coupon]", "http_status_code": 404, "message": "subscription[coupon] : The SPIDERMAN25 referenced in parameter subscription[coupon] is not present ", "param": "subscription[coupon]", "type": "invalid_request" }
Payment Errors: If a payment fails due to card verification or processing errors, Chargebee returns an error response(as shown below) which is thrown as a payment exception by the client library. We handle the exceptions in the demo application with appropriate error messages.
{ "api_error_code": "payment_method_verification_failed", "error_code": "add_card_error", "error_msg": "Problem while adding the card. Error message : (3009) Do not honour.", "http_status_code": 400, "message": "Problem while adding the card. Error message : (3009) Do not honour.", "type": "payment" }
General API Errors: Chargebee might return error responses due to various reasons such as invalid configuration, bad request etc. To identify specific reasons for all error responses you can check the API documentation. Also take a look at the error handler file to check how these errors can be handled.
Test cards
Now that you're all set, why don't you test your integration with some test transactions. Here are some credit card numbers that you can use to test your application.
Valid Card | 4111 1111 1111 1111 |
Verification Error Card | 4119 8627 6033 8320 |
Transaction Error Card | 4005 5192 0000 0004 |
Reference Links
We're always happy to help you with any questions you might have!
support@chargebee.com