This flow allows you to authenticate customers and tokenize their payment details in a single step. Use this option to reduce the overall number of API calls to CCBill's RESTful API and minimize system overhead.
The following guide provides step-by-step instructions and relevant code examples to help you integrate this payment flow using the CCBill Advanced Widget.
To simultaneously perform a 3DS check on customer payment details and generate a payment token:
The following sequence diagram describes the flow for creating and charging payment tokens with 3DS authentication.
The following steps explain how to set up the CCBill Advanced Widget and create payment tokens with 3DS authentication.
To use the CCBill Advanced Widget, add the following preload link and script elements to your HTML page:
<link rel="preload" href="https://js.ccbill.com/v1.13.1/ccbill-advanced-widget.js" as="script"/>
<script type="text/javascript" src="https://js.ccbill.com/v1.13.1/ccbill-advanced-widget.js"></script>
The API version in this URI example is v1.13.1. Pay special attention to the version in the URI path, as the version number may be subject to change.
The Advanced Widget automatically extracts values from form fields. Depending on your integration, the required fields can be provided in three ways:
Using data-ccbill
data attributes is non-intrusive and provides more flexibility. It allows you to map form inputs directly without modifying existing id
attributes.
<form id="payment-form">
<input data-ccbill="firstName" />
<input data-ccbill="lastName" />
<input data-ccbill="postalCode" />
<input data-ccbill="amount" />
<input data-ccbill="country" />
<input data-ccbill="email" />
<input data-ccbill="cardNumber" />
<input data-ccbill="currencyCode" />
<input data-ccbill="expYear" />
<input data-ccbill="expMonth" />
<input data-ccbill="nameOnCard" />
<input data-ccbill="cvv2" />
</form>
If you cannot modify your HTML to include data-ccbill
attributes, use the default _ccbillId_
attributes instead. This method is less flexible because the field names must match CCBill's predefined format.
<form id="payment-form">
<input id="_ccbillId_firstName" />
<input id="_ccbillId_lastName" />
<input id="_ccbillId_postalCode" />
<input id="_ccbillId_amount" />
<input id="_ccbillId_country" />
<input id="_ccbillId_email" />
<input id="_ccbillId_cardNumber" />
<input id="_ccbillId_expYear" />
<input id="_ccbillId_currencyCode" />
<input id="_ccbillId_expMonth" />
<input id="_ccbillId_nameOnCard" />
<input id="_ccbillId_cvv2" />
</form>
If you prefer custom IDs, map them to corresponding input fields using the customIds
parameter in the Widget constructor.
<form id="payment-form">
<input id="custom_firstName_id" />
<input id="custom_lastName_id" />
<input id="custom_postalCode_id" />
<input id="custom_amount_id" />
<input id="custom_country_id" />
<input id="custom_email_id" />
<input id="custom_cardNumber_id" />
<input id="custom_currencyCode_id" />
<input id="custom_expYear_id" />
<input id="custom_expMonth_id" />
<input id="custom_nameOnCard_id" />
<input id="custom_cvv2_id" />
</form>
<script>
// map custom ids to relevant fields
const customIds = {
firstName: "custom_firstName_id",
lastName: "custom_lastName_id",
postalCode: "custom_postalCode_id",
amount: "custom_amount_id",
country: "custom_country_id",
email: "custom_email_id",
currencyCode: "custom_currencyCode_id",
cardNumber: "custom_cardNumber_id",
expYear: "custom_expYear_id",
expMonth: "custom_expMonth_id",
nameOnCard: "custom_nameOnCard_id",
cvv2: "custom_cvv2_id"
};
// pass custom ids to Widget constructor
const widget = new ccbill.CCBillAdvancedWidget("application_id", customIds);
// call the desired Widget method
</script>
NAME | REQUIRED | DESCRIPTION |
---|---|---|
firstName | Yes | Customer's first name. |
lastName | Yes | Customer's last name. |
currencyCode | Yes | A three-digit currency code (ISO 4217 standard) for the currency used in the transaction. |
amount | Yes | Transaction total. Should be value greater than 0. |
address1 | No | Customer's billing address. If provided, it should be between 1 and 50 characters long. |
address2 | No | Customer's address (line 2). If provided, it should be between 1 and 50 characters long. |
postalCode | Yes | Customer's billing zip code. It should be a valid zip code between 1 and 16 characters long. |
city | No | Customer's billing city. If provided, it should be between 1 and 50 characters long. |
state | No | Customer's billing state. If provided, it should be between 1 and 3 characters long. |
country | Yes | Customer's billing country. Should be a two-letter country code as defined in ISO 3166-1. |
Yes | Customer's email. Should be a well-formed email address, max 254 characters long. | |
phoneNumber | No | Customer's phone number. If provided, it should be a well-formed phone number. |
ipAddress | No | Customer's IP address. |
browserHttpUserAgent | No | Browser User-Agent header value. |
browserHttpAccept | No | Browser Accept header value. |
browserHttpAcceptEncoding | No | Browser Accept Encoding header value. |
browserHttpAcceptLanguate | No | Browser Accept Language header value. |
cardNumber | Yes | A valid credit card number. |
expMonth | Yes | Credit card expiration month in mm format. Should be a value between 1 and 12. |
expYear | Yes | Credit card expiration year in yyyy format. Should be a value between current year and 2100. |
cvv2 | Yes | Card security code. Should be a 3-4 digit value. |
nameOnCard | Yes | Name displayed on the credit card. Should be between 2 and 45 characters long. |
The CCBill RESTful API uses OAuth-based authentication and authorization. Use the frontend credentials (Merchant Application ID
and Secret Key
that are Base64 encoded) you received from Merchant Support to generate a frontend bearer token.
You must include this token in the Authorization header of API requests when creating payment tokens. Use the following example and adjust the necessary parameters to obtain a frontend bearer token:
curl -X POST 'https://api.ccbill.com/ccbill-auth/oauth/token' \
-u '[Frontend_Merchant_Application_ID]:[Frontend_Secret_Key]' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=client_credentials'
String getOAuthToken() {
String credentials = Base64.getEncoder()
.encodeToString(("[Frontend_Merchant_Application_ID]" + ":" + "[Frontend_Secret_Key]")
.getBytes(StandardCharsets.UTF_8));
String requestBody = "grant_type=client_credentials";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.ccbill.com/ccbill-auth/oauth/token"))
.header("Authorization", "Basic " + credentials)
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.build();
try {
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
return extractAccessToken(response.body());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
return null;
}
}
<?php
function getOAuthToken() {
$url = "https://api.ccbill.com/ccbill-auth/oauth/token";
$merchantAppId = "[Frontend_Merchant_Application_ID]";
$secretKey = "[Frontend_Secret_Key]";
$data = http_build_query(["grant_type" => "client_credentials"]);
try {
$httpRequest = new HttpRequest();
$httpRequest->setUrl($url);
$httpRequest->setMethod(HTTP_METH_POST);
$httpRequest->setHeaders([
"Authorization" => "Basic " . base64_encode("$merchantAppId:$secretKey"),
"Content-Type" => "application/x-www-form-urlencoded"
]);
$httpRequest->setBody($data);
$httpClient = new HttpClient();
$response = $httpClient->send($httpRequest);
$responseData = json_decode($response->getBody(), true);
return $responseData['access_token'] ?? die("Error: Invalid OAuth response.");
} catch (HttpException $ex) {
die("Error fetching OAuth token: " . $ex->getMessage());
}
}
?>
Important Notes
The isScaRequired() function determines whether strong customer authentication is required before generating a Payment Token. The system checks the provided credit card number, merchant account number, subaccount number, and currency code.
This method allows you to dynamically apply 3DS authentication only in instances when they are required.
Code Example
async function checkIfScaRequired() {
const widget = new ccbill.CCBillAdvancedWidget('your-application-id');
const scaRequiredResponse = await widget.isScaRequired(
"[Frondent_Access_Token]",
[Your_Client_Account_Number],
[Your_3DS_Client_Subaccount_Number]);
return await scaRequiredResponse.json();
}
The isScaRequiredForPaymentToken() function determines whether strong customer authentication(3DS) is required for a pre-existing Payment Token.
Merchants who have already stored payment information as a token (Payment Token ID) can use it to determine if SCA is required before processing a charge.
Code Example
async function checkIfScaRequired() {
const widget = new ccbill.CCBillAdvancedWidget('your-application-id');
const scaRequiredResponse = await widget.isScaRequiredForPaymentToken(
"[Frondent_Access_Token]",
"[payment_token_id]");
return await scaRequiredResponse.json();
}
Response Handling
The function automatically checks the transaction parameters to determine if strong customer authentication (SCA) is required:
The authenticateCustomerAndCreatePaymentToken() function combines 3DS authentication and payment token creation in a single call. This integration simplifies the workflow by:
Merchants can use this flow for card-present transactions where the charge amount is known upfront, such as in-session web payments.
Code Example
async function authenticateCustomerAndCreatePaymentToken() {
const widget = new ccbill.CCBillAdvancedWidget('your-application-id');
return await widget.authenticateCustomerAndCreatePaymentToken(
"[Frondent_Access_Token]",
[Your_Client_Account_Number],
[Your_3DS_Client_Subaccount_Number]);
}
Response Handling
The function automatically handles 3DS authentication and Payment Token generation:
After you receive a payment token ID, generate a new backend bearer token using your Base64 encoded backend credentials. Then, use both tokens to charge the customer's credit card. This step finalizes the transaction.
Code Examples
curl -X POST 'https://api.ccbill.com/transactions/payment-tokens/threeds/[payment_token_id]' \
-H 'Accept: application/vnd.mcn.transaction-service.api.v.2+json' \
-H 'Authorization: Bearer [Backend_Access_Token]' \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/json' \
-d '{
"clientAccnum": [Your_Client_Account_Number],
"clientSubacc": [Your_Client_Subaccount_Number],
"initialPrice": 9.99,
"initialPeriod": 30,
"currencyCode": 978,
"threedsEci": "05",
"threedsStatus": "Y",
"threedsSuccess": true,
"threedsVersion": "2.2.0",
"threedsAmount": 9.99,
"threedsClientTransactionId": "id-wl9r6duc5zj",
"threedsCurrency": "840",
"threedsSdkTransId": "d535b6d1-19f9-11f0-92b9-0242ac110005",
"threedsAcsTransId": "ca5f9649-b865-47ce-be6f-54422a0fce47",
"threedsDsTransId": "e3693b86-8217-48c6-9628-2e8852dc60d4",
"threedsAuthenticationType": "",
"threedsAuthenticationValue": "Pes4aJnpT+1mjhUoBynC92iQbeg="
}'
public ResponseEntity<String> processPurchase3ds() {
String requestBody = """
{
"clientAccnum": [Your_Client_Account_Number],
"clientSubacc": [Your_Client_Subaccount_Number],
"initialPrice": 9.99,
"initialPeriod": 30,
"currencyCode": 978,
"threedsEci": "05",
"threedsStatus": "Y",
"threedsSuccess": true,
"threedsVersion": "2.2.0",
"threedsAmount": 9.99,
"threedsClientTransactionId": "id-wl9r6duc5zj",
"threedsCurrency": "840",
"threedsSdkTransId": "d535b6d1-19f9-11f0-92b9-0242ac110005",
"threedsAcsTransId": "ca5f9649-b865-47ce-be6f-54422a0fce47",
"threedsDsTransId": "e3693b86-8217-48c6-9628-2e8852dc60d4",
"threedsAuthenticationType": "",
"threedsAuthenticationValue": "Pes4aJnpT+1mjhUoBynC92iQbeg="
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.ccbill.com/transactions/payment-tokens/threeds/[payment_token_id]"))
.header("Accept", "application/vnd.mcn.transaction-service.api.v.2+json")
.header("Authorization", "Bearer [Backend_Access_Token]")
.header("Cache-Control", "no-cache")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return ResponseEntity.ok(response.body());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
return ResponseEntity.status(500).body("Error processing payment");
}
}
<?php
function processPurchase3ds() {
$url = "https://api.ccbill.com/transactions/payment-tokens/threeds/[payment_token_id]";
$paymentData = json_encode([
"clientAccnum" => [Your_Client_Account_Number],
"clientSubacc" => [Your_Client_Subaccount_Number],
"initialPrice" => 9.99,
"initialPeriod" => 30,
"threedsEci" => "05",
"threedsStatus" => "Y",
"threedsSuccess" => true,
"threedsVersion" => "2.2.0",
"threedsAmount" => 9.99,
"threedsClientTransactionId" => "id-wl9r6duc5zj",
"threedsCurrency" => "840",
"threedsSdkTransId" => "d535b6d1-19f9-11f0-92b9-0242ac110005",
"threedsAcsTransId" => "ca5f9649-b865-47ce-be6f-54422a0fce47",
"threedsDsTransId" => "e3693b86-8217-48c6-9628-2e8852dc60d4",
"threedsAuthenticationType" => "",
"threedsAuthenticationValue" => "Pes4aJnpT+1mjhUoBynC92iQbeg="
]);
try {
$httpRequest = new HttpRequest();
$httpRequest->setUrl($url);
$httpRequest->setMethod(HTTP_METH_POST);
$httpRequest->setHeaders([
"Accept" => "application/vnd.mcn.transaction-service.api.v.2+json",
"Authorization" => "Bearer [Backend_Access_Token]",
"Cache-Control" => "no-cache",
"Content-Type" => "application/json"
]);
$httpRequest->setBody($paymentData);
$httpClient = new HttpClient();
$response = $httpClient->send($httpRequest);
return $response->getBody();
} catch (HttpException $ex) {
die("Error charging payment token: " . $ex->getMessage();
}
}
?>
Response Handling
The server returns a standard response with the transaction status:
To simplify the 3DS transaction flow, the example below shows how to authenticate a customer and create a Payment Token using the above steps. The example uses:
All placeholder values should be replaced with the actual client account number, subaccount number, bearer token, and 3DS credentials.
async function fetchOAuthToken() {
return (await (await fetch('https://your-website.com/api/auth-token')).json()).token;
}
async function checkIfScaRequired(widget, authToken, clientAccnum, clientSubacc) {
const scaRequiredResponse = await widget.isScaRequired(authToken, clientAccnum, clientSubacc);
return await scaRequiredResponse.json();
}
async function authenticateCustomerAndCreatePaymentToken(widget, authToken, clientAccnum, clientSubacc) {
return await widget.authenticateCustomerAndCreatePaymentToken(authToken, clientAccnum, clientSubacc);
}
async function createPaymentToken(widget, authToken, clientAccnum, clientSubacc) {
const paymentTokenResponse = await widget.createPaymentToken(
authToken,
clientAccnum,
clientSubacc
);
return await paymentTokenResponse.json();
}
async function chargePaymentToken(paymentToken) {
return await (await (fetch('https://your-website.com/api/purchase', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
paymentToken,
amount: 9.99,
currency: 840
})
}))).json();
}
async function chargePaymentToken3ds(paymentToken, threedsInformation) {
return await (await (fetch('https://your-website.com/api/purchase-3ds', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
paymentToken,
threedsInformation,
amount: 9.99,
currency: 840
})
}))).json();
}
async function authenticateAndPurchase() {
const widget = new ccbill.CCBillAdvancedWidget('your-application-id');
const clientAccnum = [Your_Client_Account_Number];
const clientSubacc = [Your_3DS_Client_Subaccount_Number];
try {
// retrieval of the auth token from merchant provided endpoint
// this should be done as late in the submission process as possible to avoid potential exploit.
const authToken = await fetchOAuthToken();
let chargeCallResponse;
// check if 3DS is required and process the 3DS flow with the client if necessary
const scaRequired = await checkIfScaRequired(widget, authToken, clientAccnum, clientSubacc);
if (scaRequired) {
// go through 3DS flow and create payment token in a single API call.
// The resulting object will hold both payment token and SCA results,
// which should be submitted to merchant owned endpoint and charged
/// via /transactions/payment-tokens/threeds/{paymentTokenId}.
const response = await authenticateCustomerAndCreatePaymentToken(widget,
authToken, clientAccnum, clientSubacc);
// submit the payment token and 3DS information to the back-end endpoint implementing
// charging of the token
chargeCallResponse = await chargePaymentToken3ds(response.paymentToken, response.threedsInformation);
} else {
// create the payment token to be submitted to the merchant owned endpoint
// and charged via /transactions/payment-tokens/{paymentTokenId}.
const paymentToken = await createPaymentToken(widget, authToken, clientAccnum, [Your_Client_Subaccount_Number]);
// submit the payment token to be charged to an endpoint implementing backend charging of the token
chargeCallResponse = await chargePaymentToken(paymentToken);
}
return Promise.resolve(chargeCallResponse);
} catch (error) {
// react to any errors that may occur during the process
return Promise.reject({error});
}
}
let result = await authenticateAndPurchase();
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@RestController
@RequestMapping("/api")
public class ApiController {
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
@PostMapping("/auth-token")
public ResponseEntity<AuthTokenResponse> getAuthToken() {
String accessToken = fetchOAuthToken("[Frontend_Merchant_Application_ID]", "[Frontend_Secret_Key]");
if (accessToken != null) {
return ResponseEntity.ok(new AuthTokenResponse(accessToken));
} else {
return ResponseEntity.status(500).body(new AuthTokenResponse(""));
}
}
@PostMapping("/purchase")
public ResponseEntity<String> processPurchase(@RequestBody PurchaseRequest purchaseRequest) {
String requestBody = String.format(
"""
{
"clientAccnum": %d,
"clientSubacc": %d,
"initialPrice": %.2f,
"initialPeriod": 30,
"currencyCode": %d
}
""",
purchaseRequest.paymentToken().clientAccnum(),
purchaseRequest.paymentToken().clientSubacc(),
purchaseRequest.amount(),
purchaseRequest.currency()
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.ccbill.com/transactions/payment-tokens/"
+ purchaseRequest.paymentToken().paymentTokenId()))
.header("Accept", "application/vnd.mcn.transaction-service.api.v.2+json")
.header("Authorization", "Bearer "
+ fetchOAuthToken("[Backend_Merchant_Application_ID]", "[Backend_Secret_Key]"))
.header("Cache-Control", "no-cache")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.build();
try {
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
return ResponseEntity.ok(response.body());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
return ResponseEntity.status(500).body("Error processing payment");
}
}
@PostMapping("/purchase-3ds")
public ResponseEntity<String> processPurchase3ds(@RequestBody PurchaseRequest3ds purchaseRequest3ds) {
String requestBody = String.format(
"""
{
"clientAccnum": %d,
"clientSubacc": %d,
"initialPrice": %.2f,
"initialPeriod": 10,
"currencyCode": "%s",
"threedsEci": "%s",
"threedsStatus": "%s",
"threedsSuccess": %b,
"threedsVersion": "%s",
"threedsAmount": %.2f,
"threedsClientTransactionId": "%s",
"threedsCurrency": "%s",
"threedsSdkTransId": "%s",
"threedsAcsTransId": "%s",
"threedsDsTransId": "%s",
"threedsAuthenticationType": "%s",
"threedsAuthenticationValue": "%s"
}
""",
purchaseRequest3ds.paymentToken().clientAccnum(),
purchaseRequest3ds.paymentToken().clientSubacc(),
purchaseRequest3ds.amount(),
purchaseRequest3ds.currency(),
purchaseRequest3ds.threedsInformation().eci(),
purchaseRequest3ds.threedsInformation().status(),
purchaseRequest3ds.threedsInformation().success(),
purchaseRequest3ds.threedsInformation().protocolVersion(),
purchaseRequest3ds.threedsInformation().amount(),
purchaseRequest3ds.threedsInformation().clientTransactionId(),
purchaseRequest3ds.threedsInformation().currency(),
purchaseRequest3ds.threedsInformation().sdkTransId(),
purchaseRequest3ds.threedsInformation().acsTransId(),
purchaseRequest3ds.threedsInformation().dsTransId(),
purchaseRequest3ds.threedsInformation().authenticationType(),
purchaseRequest3ds.threedsInformation().authenticationValue()
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.ccbill.com/transactions/payment-tokens/threeds/"
+ purchaseRequest.paymentToken().paymentTokenId()))
.header("Accept", "application/vnd.mcn.transaction-service.api.v.2+json")
.header("Authorization", "Bearer "
+ fetchOAuthToken("[Backend_Merchant_Application_ID]", "[Backend_Secret_Key]"))
.header("Cache-Control", "no-cache")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.build();
try {
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
return ResponseEntity.ok(response.body());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
return ResponseEntity.status(500).body("Error processing payment");
}
}
private static String fetchOAuthToken(String merchantAppId, String sercretKey) {
String credentials = Base64.getEncoder()
.encodeToString((merchantAppId + ":" + sercretKey).getBytes(StandardCharsets.UTF_8));
String requestBody = "grant_type=client_credentials";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.ccbill.com/ccbill-auth/oauth/token"))
.header("Authorization", "Basic " + credentials)
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.build();
try {
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
return extractAccessToken(response.body());
} catch (IOException | InterruptedException e) {
e.printStackTrace();
return null;
}
}
private static String extractAccessToken(String responseBody) {
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(responseBody);
return jsonNode.has("access_token") ? jsonNode.get("access_token").asText() : null;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private record AuthTokenResponse(String token) {}
private record PurchaseRequest(double amount, String currency, PaymentToken paymentToken) {}
private record PurchaseRequest3ds(double amount, String currency, PaymentToken paymentToken,
ThreedsInformation threedsInformation) {}
private record PaymentToken(String paymentTokenId, Integer clientAccnum, Integer clientSubacc) {}
private record ThreedsInformation(String eci, String status, boolean success, String protocolVersion,
double amount, String clientTransactionId, String currency, String sdkTransId, String acsTransId,
String dsTransId, String authenticationType, String authenticationValue) {}
}
Additional Documentation