Create Payment Token (3DS)

The Create Payment Token (3DS) flow allows merchants to authenticate customers and tokenize their payment details. It is designed to facilitate strong customer authentication without disrupting the customer's checkout experience.

This guide provides step-by-step instructions and relevant code examples to help you integrate this payment flow using the CCBill Advanced Widget.

Requirements
  • The CCBill RESTful API supports TLS 1.2 only.
  • A CCBill Account that includes a client account number and two subaccounts used to generate payment tokens for 3DS and non-3DS transactions.
  • API credentials provided by CCBill.
  • A whitelisted domain (contact CCBill Support for setup).
  • Experience with RESTful Web Services and JSON formats.

The Payment Flow

To perform 3DS authentication based on the customer's payment details, create a payment token and then use the token to charge the customer:

  1. Include the Widget on your page.
  2. Provide payment details.
  3. Generate a frontend OAuth Bearer Token.
  4. Check whether 3DS authentication is required based on customer data (or a pre-existing payment token).
  5. Authenticate the customer.
  6. Utilize the payment details and frontend bearer token to create a payment token.
  7. Use the payment token, authentication results, and backend OAuth bearer token to process the transaction securely.

The following sequence diagram describes the flow for creating and charging payment tokens with 3DS authentication.

The sequence diagram for creating and charging payment tokens with 3DS verification.

The following steps explain how to set up the CCBill Advanced Widget and create payment tokens with 3DS authentication.

1. Include the Widget in Your Page

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.

2. Collect Customer and Payment Data

The Advanced Widget automatically extracts values from form fields. Depending on your integration, the required fields can be provided in three ways:

  • (Recommended) Use data-ccbill HTML data attributes.
  • Use default _ccbillId_FieldName ID attributes.
  • Use custom ID attributes (requires additional mapping).

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>

All Form Fields

NAMEREQUIREDDESCRIPTION
amountYesTransaction total. Should be a value greater than 0.
currencyCodeYesA three-digit currency code (ISO 4217 standard) for the currency used in the transaction.
firstNameYesCustomer's first name.
lastNameYesCustomer's last name.
address1NoCustomer's billing address. If provided, it should be between 1 and 50 characters long.
address2NoCustomer's address (line 2). If provided, it should be between 1 and 50 characters long.
address3NoCustomer's address (line 3). If provided, it should be between 1 and 50 characters long.
postalCodeYesCustomer's billing zip code. It should be a valid zip code between 1 and 16 characters long.
cityNoCustomer's billing city. If provided, it should be between 1 and 50 characters long.
stateNoCustomer's billing state. If provided, it should be between 1 and 3 characters long.
countryYesCustomer's billing country. Should be a two-letter country code as defined in ISO 3166-1.
emailYesCustomer's email. Should be a well-formed email address, max 254 characters long.
phoneNumberNoCustomer's phone number. If provided, it should be a well-formed phone number.
ipAddressNoCustomer's IP address.
browserHttpUserAgentNoBrowser User-Agent header value.
browserHttpAcceptNoBrowser Accept header value.
browserHttpAcceptEncodingNoBrowser Accept Encoding header value.
browserHttpAcceptLanguateNoBrowser Accept Language header value.
cardNumberYesA valid credit card number.
expMonthYesCredit card expiration month in mm format. Should be a value between 1 and 12.
expYearYesCredit card expiration year in yyyy format. Should be a value between current year and 2100.
cvv2YesCard security code. Should be a 3-4 digit value.
nameOnCardYesName displayed on the credit card. Should be between 2 and 45 characters long.

3. Generate OAuth Bearer Token

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

  • Never expose API credentials on the front end. Always store your Merchant Application ID and Secret Key securely in server-side environment variables.
  • This request must be sent from your backend. OAuth token requests cannot be made from a web browser for security reasons.
  • OAuth access tokens are temporary. Each token remains valid for a single request or until it expires.
  • Reduce API token attack surface. Execute calls to create an Oauth token and a payment token in quick succession to minimize the risk of the access token being exposed to attackers.
  • Use CSRF tokens for your front-end payment forms. Protect your front-end forms with CSRF tokens to prevent unauthorized form submissions.

4. Check if SCA is Required

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, and currency code.

This method allows you to dynamically apply 3DS authentication only in instances when they are truly 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();
}
Alternative: Check If 3DS Is Required Based on Existing Token

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:

  • A successful response returns a Boolean value that indicates whether 3DS is required for the transaction. Use the result to dynamically route customers through a 3DS flow only when required. This ensures a better user experience and compliance with SCA regulations.
  • If validation fails (e.g., invalid credentials), the response will show an error message to describe the issue.

5. Authenticate Customer

The authenticateCustomer() function initiates an SCA flow to obtain 3DS results before executing a 3DS transaction and calling CCBill's RESTful API endpoint.

Code Example

async function authenticate() {
    const widget = new ccbill.CCBillAdvancedWidget('your-application-id');
    return await widget.authenticateCustomer(
        "[Frondent_Access_Token]", 
        [Your_Client_Account_Number], 
        [Your_3DS_Client_Subaccount_Number]);
}
Alternative: Authenticate Customer Based on Exiting Token

The authenticateCustomer() function can also be used to perform strong customer authentication based on a pre-existing payment token.

Merchants who have already stored payment information as a token (paymentTokenID) can use it to authenticate a customer before processing a charge.

Code Example

async function authenticate() {
    const widget = new ccbill.CCBillAdvancedWidget('your-application-id');
    return await widget.authenticateCustomer(
        "[Frondent_Access_Token]", 
        [Your_Client_Account_Number], 
        [Your_Client_Subaccount_Number],
        null, null,
        "[payment_token_id]");
}

Response Handling

The function initiates the 3DS authentication flow and returns:

  • A successful response that includes authentication data, which is required to proceed with a 3DS transaction.
  • A relevant error code and description in case of failure. In this case, prompt the user to address the error and retry the authentication.

6. Generate Payment Token

The createPaymentToken() function is the primary way to generate a payment token using the CCBill Advanced Widget. Invoke the function on the frontend after collecting and validating customer input and generating a frontend bearer token with your frontend credentials.

Code Example

async function createPaymentToken(scaRequired) {
    const widget = new ccbill.CCBillAdvancedWidget('your-application-id');
    const clientSubacc = scaRequired ? [Your_3DS_Client_Subaccount_Number] : [Your_Client_Subaccount_Number];
    const paymentTokenResponse = await widget.createPaymentToken(
        "[Frondent_Access_Token]",
        [Your_Client_Account_Number],
        clientSubacc
    );
    return await paymentTokenResponse.json();
}

Response Handling

The function creates the appropriate payment token based on the SCA requirement check and automatically validates all fields before generating a token:

  • A successful response returns a payment token ID, which is required to continue the payment flow.
  • Validation failures must show an appropriate error message and prompt the user to correct the input before resubmitting.

The response may also have processing-related errors (such as token expiry). Visit the RESTful API Error Codes page for a comprehensive list of error codes.

7. Charge Payment Token (3DS)

Use the Payment Token ID and backend bearer token to charge a customer's credit card through a 3DS-secured payment flow. Generate a new backend bearer token using your Base64 encoded backend credentials.

Ensure the Payment Token passed the required 3DS authentication flow and the required 3DS values are collected.

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 API endpoint handles the transaction:

  • A successful charge returns a response with transaction details.
  • If the charge fails, the response includes an error code and a descriptive message.

Full Integration Example (3DS)

To streamline the 3DS payment flow, we have provided a full working example that shows how to implement a 3DS-compliant transaction using CCBill's Advanced Widget. The example has:

  • A JavaScript frontend that initializes the widget, collects payment data, and triggers the 3DS authentication flow.
  • A backend in Java that handles bearer token generation, receives the Payment Token, and submits a 3DS charge request using the required data.

Replace all placeholder values with actual client account details, bearer tokens, 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 authenticate(widget, authToken, clientAccnum, clientSubacc) {
    return await widget.authenticateCustomer(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 threedsInformation;
        // 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
            threedsInformation = await authenticate(widget, authToken, clientAccnum, clientSubacc);
        }
 
        // create the payment token to be submitted to the merchant owned endpoint
        const paymentToken = await createPaymentToken(widget, authToken, clientAccnum, scaRequired ? clientSubacc : [Your_Client_Subaccount_Number]);
 
        // submit the payment token and 3DS information to the back-end endpoint implementing charging of the token
        const chargeCallResponse = scaRequired ? await chargePaymentToken3ds(paymentToken, threedsInformation)
            : 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

CCBill RESTful API Resources

Error Codes

CCBill Advanced Widget API Reference