Express Checkout
To start accepting payments, you'll need to create a checkout session on our platform & render the Express payment page for the customer to complete the payment. We can break down the implementation into three broad steps:
1. Get the checkout details
To create a checkout request, you'll need a JSON checkout payload with the following fields.
Required fields
customer_first_name | Customer's first name |
customer_last_name | Customer's last name |
msisdn | Customer's Phone number is formatted as given in the E.164 phone numbering |
account_number | Account number for the customer on the merchant system. This is the reference the customer will pay to. |
request_amount | The amount of money you wish to collect. |
merchant_transaction_id | Unique merchant reference for the request raised for express checkout |
service_code | The service code assigned to the merchant on the Tingg platform |
country_code | 3 digit ISO code of the country you wish to collect payment for. |
currency_code | 3 digit ISO code of the currency the merchant is invoicing for. |
callback_url | The endpoint where we send the IPN / webhook request to. |
success_redirect_url | Where we will redirect the customer to after a successful payment is made. |
fail_redirect_url | Where we will redirect the customer to when the payment time passed above expires. |
See the sample JSON below:
Ensure your the implementation of the JSON payload is to a JSON string
{
// transaction details
customer_first_name: "John",
customer_last_name: "Doe",
msisdn: "254700000000",
account_number: "ref-qwerty",
request_amount: "100",
merchant_transaction_id: "1234567890",
// checkout configurations
service_code: "JOHNDOEONLINE",
country_code: "KEN",
currency_code: "KES",
// webhooks configurations
callback_url: "https://webhook.site/...",
fail_redirect_url: "https://webhook.site/...",
success_redirect_url: "https://webhook.site/.."
}
Optional fields
We also support additional features through the following fields.
The fields below are not mandatory thus not required in the typical use cases and can be omitted
Field | Type | Description |
---|---|---|
due_date | String | A future date and time the payment should be completed, before the request expires. A date with the format YYYY-MM-DD HH:mm:ss in UTC Defaults to 12 hours |
customer_email | String | Customer's email address. |
request_description | String | Shows the description of the item being purchased. |
invoice_number | String | |
prefill_msisdn | Boolean | When set to false the phone number provided above will not be pre-filled in the payment form. Defaults to true |
payment_option_code | String | Payment option code of the payment options the merchant wishes to collect for. |
language_code | String | Language code you wish to display instructions for payment for: fr - French, en - English, ar - Arabic , pt - Portuguese |
charge_beneficiaries | JSONArray | Extra charge client we should settle to |
charge_beneficiaries:
Field | Type | Description |
---|---|---|
charge_beneficiary_code | String | This is the client who we are to settle to on behalf of the customer |
amount | Double | Amount we should settle the service provider above |
{
"due_date":"2021-11-18 16:15:30",
"customer_email":"[email protected]",
"request_description":"Dummy merchant transaction",
"invoice_number":"",
"prefill_msisdn": true,
"payment_option_code":"SAFKE",
"language_code":"en",
"pending_redirect_url":"https://webhook.site/6c933f61-d6da-4f8e-8a44-bf0323eb8ad6",
"charge_beneficiaries":[
{
"charge_beneficiary_code":"KRA",
"amount":30
},
{
"charge_beneficiary_code":"GLOVO",
"amount":70
}
]
}
2. Create an Express URL
We provide SDK's to assist with the integration process but they only cover a limited number of programming languages.
However, you can still create a checkout URL given a valid checkout request payload where the customer will be redirected to complete the payment.
To create the checkout URL, you'll need three things:
Component | Type | Description |
---|---|---|
payload | JSON | The checkout payload with all required fields |
apikey | String | A key that uniquely identifies the client app on the Tingg platform |
client_id | String | A public identifier for your Tingg platform account. Think of it as an API username |
client_secret | String | A secret known only to the application and the Tingg platform account authorisation server, and is the password to your API username |
Step 1 - Get the access token
access token
The first step is to get an access_token token using your apikey
, client_id
& client_secret
by sending an API request.
See some examples below
Securely store your
apikey
,client_id
andclient_secret
keysDO NOT hard code your credentials or use the on the UI / browser code
Store your credentials in .env and remember to NOT commit your env files
Store your credentials in a secure environment doppler, vault, akeyless ...
curl --location 'https://api-approval.tingg.africa/v1/oauth/token/request' \
--header 'apiKey: <API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"client_id":"<CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>",
"grant_type": "client_credentials"
}'
<?php
// using the Guzzle client
$client = new GuzzleHttp\Client();
$headers = [
'apiKey' => '<API_KEY>',
'Content-Type' => 'application/json'
];
$body = '{
"client_id": "<CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>",
"grant_type": "client_credentials"
}';
$request = new Request('POST', 'https://api-test.tingg.africa/v1/oauth/token/request', $headers, $body);
$res = $client->sendAsync($request)->wait();
echo $res->getBody();
const headers = new Headers();
headers.append("apiKey", "<API_KEY>");
headers.append("Content-Type", "application/json");
const raw = JSON.stringify({
"client_id": "<CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>",
"grant_type": "client_credentials",
});
const options = {method: "POST", headers: headers, body: raw, redirect: "follow"};
fetch("https://api-test.tingg.africa/v1/oauth/token/request", options)
.then((response) => response.text())
.then((result) => console.log(result))
.catch((error) => console.error(error));
import requests
import json
url = "https://api-test.tingg.africa/v1/oauth/token/request"
payload = json.dumps({
"client_id": "<CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>",
"grant_type": "client_credentials"
})
headers = {
'apiKey': '<API_KEY>',
'Content-Type': 'application/json'
}
response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api-test.tingg.africa/v1/oauth/token/request"
method := "POST"
payload := strings.NewReader(`{
"client_id":"<CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>",
"grant_type": "client_credentials"
}`)
client := &http.Client {
}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("apiKey", "<API_KEY>")
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
If the request is successful, you get a JSON body and a 200 OK status response. Which you then parse to get the JWT access_token
to send the next and final request.
Find sample responses below:
{
"expires_in": 3600,
"token_type": "bearer",
"access_token": "<ACCESS TOKEN>",
"refresh_token": "<REFRESH TOKEN>"
}
{
"errors": [
{
"detail": "Failed to resolve API Key variable request.header.apikey"
}
]
}
{
"error": "invalid_credentials",
"error_description": "Invalid credentials"
}
Step 2 - Get the checkout URLs
The second and final step is to use the access_token from the request above to send a JSON request to get your checkout URLs / links.
curl --location 'https://online.sandbox.tingg.africa/approval/request-service/checkout-request/express-request' \
--header 'Authorization: Bearer <ACCESS TOKEN>' \
--header 'Content-Type: application/json' \
--data-raw '{
"customer_first_name": "John",
"customer_last_name": "Doe",
"msisdn": "254700000000",
"account_number": "ref-qwerty",
"request_amount": "100",
"merchant_transaction_id": "1234567890",
"service_code": "JOHNDOEONLINE",
"country_code": "KEN",
"currency_code": "KES",
"callback_url": "https://webhook.site/...",
"fail_redirect_url": "https://webhook.site/...",
"success_redirect_url": "https://webhook.site/.."
}'
<?php
// using the Guzzle client
$client = new GuzzleHttp\Client();
$headers = [
'Authorization' => 'Bearer <ACCESS TOKEN>',
'Content-Type' => 'application/json'
];
// use a complete checkout request payload
$body = '{
"customer_first_name": "John",
"customer_last_name": "Doe",
"msisdn": "254700000000",
"account_number": "ref-qwerty",
"request_amount": "100",
"merchant_transaction_id": "1234567890",
"service_code": "JOHNDOEONLINE",
"country_code": "KEN",
"currency_code": "KES",
"callback_url": "https://webhook.site/...",
"fail_redirect_url": "https://webhook.site/...",
"success_redirect_url": "https://webhook.site/.."
}';
$request = new Request('POST', 'https://online.uat.tingg.africa/testing/request-service/checkout-request/express-request', $headers, $body);
$res = $client->sendAsync($request)->wait();
echo $res->getBody();
const headers = new Headers();
headers.append("Authorization", "Bearer <ACCESS TOKEN>");
headers.append("Content-Type", "application/json");
const body = JSON.stringify({
"customer_first_name": "John",
"customer_last_name": "Doe",
"msisdn": "254700000000",
"account_number": "ref-qwerty",
"request_amount": "100",
"merchant_transaction_id": "1234567890",
"service_code": "JOHNDOEONLINE",
"country_code": "KEN",
"currency_code": "KES",
"callback_url": "https://webhook.site/...",
"fail_redirect_url": "https://webhook.site/...",
"success_redirect_url": "https://webhook.site/.."
});
const requestOptions = {method: "POST", body: body, headers: headers, redirect: "follow"};
const requestURl = "https://online.uat.tingg.africa/testing/request-service/checkout-request/express-request";
fetch(requestURL, requestOptions)
.then((response) => response.text())
.then((result) => console.log(result))
.catch((error) => console.error(error));
import requests
import json
url = "https://online.uat.tingg.africa/testing/request-service/checkout-request/express-request"
payload = json.dumps({
"customer_first_name": "John",
"customer_last_name": "Doe",
"msisdn": "254700000000",
"account_number": "ref-qwerty",
"request_amount": "100",
"merchant_transaction_id": "1234567890",
"service_code": "JOHNDOEONLINE",
"country_code": "KEN",
"currency_code": "KES",
"callback_url": "https://webhook.site/...",
"fail_redirect_url": "https://webhook.site/...",
"success_redirect_url": "https://webhook.site/.."
})
headers = {
'Authorization': 'Bearer <ACCESS TOKEN>',
'Content-Type': 'application/json'
}
response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
"encoding/json"
)
func main() {
url := "https://online.uat.tingg.africa/testing/request-service/checkout-request/express-request"
method := "POST"
payloadData := map[string]interface{}{
"customer_first_name": "John",
"customer_last_name": "Doe",
"msisdn": "254700000000",
"account_number": "ref-qwerty",
"request_amount": "100",
"merchant_transaction_id": "1234567890",
"service_code": "JOHNDOEONLINE",
"country_code": "KEN",
"currency_code": "KES",
"callback_url": "https://webhook.site/...",
"fail_redirect_url": "https://webhook.site/...",
"success_redirect_url": "https://webhook.site/..",
}
payloadJSON, err := json.Marshal(payloadData)
client := &http.Client {
}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Authorization", "Bearer <ACCESS TOKEN>")
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
Should the request be successful, you'll you get a JSON body and a 200 OK status response. The response body will contain the checkout URLs
{
"status": {
"status_code": 200,
"status_description": "success"
},
"results": {
"short_url": "https://../checkout/<UNIQUE CHECKOUT URL SLUG>",
"long_url": "https://.../checkout?access_key=...&encrypted_payload=...",
}
}
We advocate the use of the
short_url
instead of thelong_url
When sharing or passing around the long checkout URL some of the last parts might be ignored or omitted on communication channels i.e SMS, WhatsApp etc.
To make thing easier, we provide SDKs for some common languages.
They are completely open-source and publicly accessible. In case you encounter and issue, feel free to issues on the respective repository or seek support on our discussion forum.
3. Setup Payment Notifications
We send out IPNs on the callback_url
provided on the checkout payload. The payment notifications are in the form of HTTP request from our platform.
We send a HTTP POST request to the merchant's callback URL. With the fields below:
HTTP POST Request Body
Parameter Name | Type | Description |
---|---|---|
checkout_request_id | Double | A unique identifier on Cellulant’s end. |
merchant_transaction_id | String | Unique ID the merchant raised for the request |
request_amount | Double | The converted amount for the request raised by the merchant |
original_request_amount | Double | The original request amount raised by the merchant in the invoice currency |
request_currency_code | String | Converted currency for the request the customer made the payment in |
original_request_currency_code | String | ISO Code of the currency code the merchant raised the request in |
account_number | Double | Merchant reference the customer was paying for |
currency_code | String | ISO Currency code of the payment made |
amount_paid | Double | Amount the customer paid for the request |
service_charge_amount | Double | Charges added to the service for the request initiated |
request_date | Date | Date when the request was raised in |
service_code | String | Unique service code identifying the service the payment request was raised for. |
request_status_code | String | Overall request code indicating the status of the service 177 - partially paid requests 178 - indicating the request was fully paid for 179 - indicating the request was partially paid for but expired 129 - Request expired without payments 102 - Insufficient funds 101 - Invalid pin/canceled 99 - Generic Failed Payment Status |
request_status_description | String | Description of the status given back on the webhook request |
MSISDN | String | The mobile number the person came with from the merchant site |
payments | JSON Array | An array of successful payments made to the request |
failed_payments | JSON Array | An array of any payments initiated but not successfully authorized |
extra_data | String | metadata |
country_abbrv | String | Abbreviation of the country |
Payments array for both failed and successful payments array.
Parameter Name | Type | Description |
---|---|---|
customer_name | string | Customer name of the person who made the payment |
account_number | string | Merchant reference the customer was paying for. |
cpg_transaction_id | String | Unique Cellulant identity |
currency_code | string | ISO Currency code of the payment made |
payer_client_code | string | Payment option customer paid with e.g. Airtel |
payer_client_name | String | Payment option customer name |
amount_paid | double | Amount customer paid for |
service_code | String | Code of service paid to |
date_payment_received | Date | When the payment was made and received |
MSISDN | string | The mobile number the customer is paying for |
payer_transaction_id | string | Unique ID the MNO or bank generated for the transaction |
hub_overall_status | string | The overall status of the payment made is described on the status code table below |
payer_narration | String | Payment description is given by MNO, bank, or card acquirer. |
Here is a sample JSON request body that you'll receive in the IPN:
{
"request_status_code": 178,
"account_number": "11800",
"merchant_transaction_id": "56679792",
"amount_paid": 15,
"service_charge_amount": 0,
"request_amount": "10",
"payments": [
{
"account_number": "11800",
"payer_client_name": "Mula Checkout",
"amount_paid": 5,
"payer_narration": "The service request is processed successfully.",
"date_payment_received": "2021-11-26 14:47:45.0",
"currency_code": "KES",
"payer_transaction_id": "PKQ082LB08",
"cpg_transaction_id": "1195072932",
"payer_client_code": "MULACHECKOUT_KEN",
"hub_overall_status": 139,
"service_code": "MULACHECKOUTONLINE",
"customer_name": "Customer",
"msisdn": 254700000000
},
{
"account_number": "11800",
"payer_client_name": "Mula Checkout",
"amount_paid": 5,
"payer_narration": "The service request is processed successfully.",
"date_payment_received": "2021-11-26 14:47:45.0",
"currency_code": "KES",
"payer_transaction_id": "PKQ082LB08",
"cpg_transaction_id": "1195072932",
"payer_client_code": "MULACHECKOUT_KEN",
"hub_overall_status": 139,
"service_code": "MULACHECKOUTONLINE",
"customer_name": "Customer",
"msisdn": 254700000000
},
],
"original_request_amount": 10,
"checkout_request_id": 6,
"currency_code": "KES",
"failed_payments": [],
"request_currency_code": "KES",
"request_date": "Fri Nov 26 11:47:04 GMT 2021",
"service_code": "MULACHECKOUTONLINE",
"request_status_description": "Success payment",
"original_request_currency_code": "KES",
"msisdn": "254700000000",
"extra_data": "abcd",
"country_abbrv": "KEN"
}
HTTP response
After receiving the HTTP POST request on the callback_url
specified on the checkout payload, you have to respond with the following JSON payloads to either accept or reject the payment according to your business processes.
Parameter Name | Type | Description |
---|---|---|
checkout_request_id | String | The unique Cellulant ID on the database |
merchant_transaction_id | String | Unique ID the merchant raised for the request |
status_code | String | Indicate if a request is received successfully, failed, or accepted 183 - Successful 180 - Payment rejected 188 - Payment received and will be acknowledged later |
status_description | String | A narration of the status code meaning |
receipt_number | String | Unique identifier of the acknowledgment response given back |
Sample Response for payments processed
{
"status_code": "183",
"checkout_request_id": 4826296,
"receipt_number": "r77az121236884",
"merchant_transaction_id": "abcd-efg-hijklm",
"status_description": "Successfully"
}
{
"status_code": "180",
"checkout_request_id": 4826296,
"receipt_number": "r77az121236884",
"merchant_transaction_id": "abcd-efg-hijklm",
"status_description": "Payment rejected"
}
Updated 6 months ago