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_nameCustomer's first name
customer_last_nameCustomer's last name
msisdnCustomer's Phone number is formatted as given in the E.164 phone numbering
account_numberThis is a unique reference for the customer on the merchant system. It is the reference the customer will pay to. The value must not exceed 15 characters, and special characters are not allowed, except for underscores.
request_amountThe amount of money you wish to collect.
merchant_transaction_idUnique merchant reference for the request raised for express checkout. (Has a limit of 100 characters)
service_codeThe service code assigned to the merchant on the Tingg platform
country_code3 digit ISO code of the country you wish to collect payment for.
currency_code3 digit ISO code of the currency the merchant is invoicing for.
callback_urlThe endpoint where we send the IPN / webhook request to.
success_redirect_urlWhere we will redirect the customer to after a successful payment is made.
fail_redirect_urlWhere 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

FieldTypeDescription
due_dateStringA 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_emailStringCustomer's email address.
request_descriptionStringShows the description of the item being purchased. (Has a limit of 100 characters)
invoice_numberString
prefill_msisdnBooleanWhen set to false the phone number provided above will not be pre-filled in the payment form. Defaults to true
payment_option_codeString

Payment option code of the payment options the merchant wishes to collect for.
Note: Here if you wish to only display a specific payment option without switching to other options you would need to pass the string with square brackets i.e [ ] e.g [SAFKE,AIRTELKE] or [SAFKE]
Without the square brackets the customer will be redirected to the payment option however they will be able to switch to a different payment option

Payment option list

language_codeStringLanguage code you wish to display instructions for payment for:
fr - French, en - English, ar - Arabic , pt - Portuguese
charge_beneficiariesJSONArrayExtra charge client we should settle to

charge_beneficiaries:

FieldTypeDescription
charge_beneficiary_codeStringThis is the client who we are to settle to on behalf of the customer
amountDoubleAmount 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:

ComponentTypeDescription
payloadJSONThe checkout payload with all required fields
apikeyStringA key that uniquely identifies the client app on the Tingg platform
client_idStringA public identifier for your Tingg platform account. Think of it as an API username
client_secretStringA 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

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 and client_secret keys

DO 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://api-approval.tingg.africa/v3/checkout-api/checkout-request/express-request' \
--header 'Authorization: Bearer <ACCESS TOKEN>' \
--header 'apiKey: <API_KEY>' \
--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',
	'apiKey' => '<API_KEY>'
];

// 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://api-approval.tingg.africa/v3/checkout-api/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");
headers.append("apiKey", "<API_KEY>");

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://api-approval.tingg.africa/v3/checkout-api/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://api-approval.tingg.africa/v3/checkout-api/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',
  'apiKey': '<API_KEY>'
}

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://api-approval.tingg.africa/v3/checkout-api/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")
  req.Header.Add("apiKey", "<API_KEY>")

  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

Note the production endpoint is:- https://api.tingg.africa/v3/checkout-api/checkout-request/express-request

{
  "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 the long_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 NameTypeDescription
checkout_request_idDoubleA unique identifier on Cellulant’s end.
merchant_transaction_idStringUnique ID the merchant raised for the request
request_amountDoubleThe converted amount for the request raised by the merchant
original_request_amountDoubleThe original request amount raised by the merchant in the invoice currency
request_currency_codeStringConverted currency for the request the customer made the payment in
original_request_currency_codeStringISO Code of the currency code the merchant raised the request in
account_numberDoubleMerchant reference the customer was paying for
currency_codeStringISO Currency code of the payment made
amount_paidDoubleAmount the customer paid for the request
service_charge_amountDoubleCharges added to the service for the request initiated
request_dateDateDate when the request was raised in
service_codeStringUnique service code identifying the service the payment request was raised for.
request_status_codeStringOverall 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_descriptionStringDescription of the status given back on the webhook request
MSISDNStringThe mobile number the person came with from the merchant site
paymentsJSON ArrayAn array of successful payments made to the request
failed_paymentsJSON ArrayAn array of any payments initiated but not successfully authorized
extra_dataStringmetadata
country_abbrvStringAbbreviation of the country

Payments array for both failed and successful payments array.

Parameter NameTypeDescription
customer_namestringCustomer name of the person who made the payment
account_numberstringMerchant reference the customer was paying for.
cpg_transaction_idStringUnique Cellulant identity
currency_codestringISO Currency code of the payment made
payer_client_codestringPayment option customer paid with e.g. Airtel
payer_client_nameStringPayment option customer name
amount_paiddoubleAmount customer paid for
service_codeStringCode of service paid to
date_payment_receivedDateWhen the payment was made and received
MSISDNstringThe mobile number the customer is paying for
payer_transaction_idstringUnique ID the MNO or bank generated for the transaction
hub_overall_statusstringThe overall status of the payment made is described on the status code table below
payer_narrationStringPayment description is given by MNO, bank, or card acquirer.
payment_statusStringThis has the specific standard code on why the payment failed from the MNO.

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"
}
{
  "request_status_code": 99,
  "account_number": "11800",
  "merchant_transaction_id": "56679792",
  "amount_paid": 15,
  "service_charge_amount": 0,
  "request_amount": "10",
  "payments": [],
  "original_request_amount": 10,
  "checkout_request_id": 6,
  "currency_code": "KES",
  "failed_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": 138,
      "service_code": "MULACHECKOUTONLINE",
      "customer_name": "Customer",
      "msisdn": 254700000000,
      "payment_status": "CANCELLED",
    },
    {
      "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": 138,
      "service_code": "MULACHECKOUTONLINE",
      "customer_name": "Customer",
      "msisdn": 254700000000,
      "payment_status": "TIMEOUT",
    },
  ],
  "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"
}

Failed Payment Status (Work in Progress)

Kindly note on callback under the failed_payments array we will send the field payment_status with the exact status code of why the request failed on the MNO's end. Here overall_status for the request will be 99 or 101 or 102 however the individual failed payment will have the correct status code with the reason for failure. Note the new field showing the actual failure reason will be under failed_payments[].payment_status

Status CodeDescription
FAILEDGeneric failure reason given by the payment provider.
TIMEOUTThere was a timeout when MNO was sending request to end users handset.
INVALID_PINCustomer entered incorrect pin.
BLOCKEDCustomers mobile number was blocked by the MNO.
INSUFFICIENT_BALANCEThis indicates customer does not have enough money in their wallet to complete these transactions.
CANCELLEDRequest was cancelled by the customer.
LIMIT_EXCEEDEDRequest amount is outside the required limit for the account.
SUCCESSPayment was debited successfully.
ERRORInternal error or network when trying to reach the MNO.
NOT_ALLOWEDCustomer is not allowed to make such a transaction either due to decimals or their account is on hold.
ENGAGEDHandset has a similar request they are processing hence cannot proceed.
INVALIDFor the request sent one of the parameters are not correct and has a problem.
ACCOUNT_NOT_FOUNDMSISDN or wallet is not available on the acquirers end.

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 NameTypeDescription
checkout_request_idStringThe unique Cellulant ID on the database
merchant_transaction_idStringUnique ID the merchant raised for the request
status_codeStringIndicate if a request is received successfully, failed, or accepted
183 - Successful
180 - Payment rejected
188 - Payment received and will be acknowledged later
status_descriptionStringA narration of the status code meaning
receipt_numberStringUnique 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"
}