Skip to content

billdesk-payments/bd-java-client

Repository files navigation

BillDesk Java Client

Overview

The official Java SDK for integrating with BillDesk Payment Gateway REST APIs.

Version: 1.0.0 | Java: 8+ | Build: Maven

The library provides a single, generic invoke(ApiRequest) interface to call any BillDesk API endpoint, handling authentication, encryption, request signing, and response verification automatically.


Index


Overview

The BillDesk Java Client is an official library that provides a simple invoke(ApiRequest) interface to call any BillDesk API endpoint. It handles authentication, encryption, request signing, and response verification automatically.

Supported security schemes:

  • HMAC-SHA256 — Shared secret signing (simplest setup)
  • JOSE (RSA-OAEP-256 + PS256) — Asymmetric encryption with certificates
  • Symmetric JOSE (AES-256-GCM + HS256) — Symmetric key encryption

Features

2.1 Multiple Security Schemes

Scheme Algorithm Payload Encrypted Use Case
HmacScheme JWS/HS256 No (signed only) Shared-secret integration
JoseScheme JWE RSA-OAEP-256 + JWS/PS256 Yes High-security certificate-based integration
SymmetricJoseScheme JWE/DIR + A256GCM, JWS/HS256 Yes Symmetric key integration

2.2 Extension Support

The library is extensible via SPI interfaces:

  • HttpClientExtension — Plug in custom HTTP clients (JDK built-in or Apache HttpClient 5)

2.3 Single Invoke Method API

ApiResponse response = client.invoke(new ApiRequest("/payments/ve1_2/orders/create", payload));

Works for every BillDesk API endpoint. No per-endpoint method calls needed.

2.4 Payload Masking

Sensitive fields are automatically masked in logs:

  • Card numbers: keep last 4 digits (e.g., **** **** **** 1234)
  • CVV, expiry: full mask
  • UPI/VPA: mask prefix (e.g., *****@paytm)
  • Bank account: partial masking with IFSC preserved

Masking is enabled by default. Use .withMaskingEnabled() to enable explicitly or .withMaskingDisabled() to disable (for development/debugging only — do not use in production).

Note: If masking fails (e.g., malformed JSON input), the log entry is suppressed entirely to ensure sensitive data never prevents an API call from completing. A WARN log will indicate the masking failure. For full payload visibility during development, disable masking temporarily with .withMaskingDisabled().

2.5 Exception Hierarchy

Exception
├── BillDeskClientException (com.billdesk.client.extensions)
│   └── BillDeskClientTimeoutException
└── BillDeskSecurityException (com.billdesk.security)
    └── BillDeskSignatureVerificationException
Exception Purpose
BillDeskClientTimeoutException HTTP request exceeded timeout
BillDeskClientException Network errors, validation failures
BillDeskSignatureVerificationException Response signature verification failed
BillDeskSecurityException Encryption/decryption failures

2.7 Logging

The SDK uses SLF4J for all logging. SLF4J is a facade — you need a logging binding (Logback, Log4j 2, or slf4j-simple) on your classpath to see logs.

Binding Dependency
Logback (recommended) ch.qos.logback:logback-classic
Log4j 2 org.apache.logging.log4j:log4j-slf4j2-impl
JCL org.slf4j:jcl-over-slf4j
simple org.slf4j:slf4j-simple

Log levels used:

Class Category Levels
BillDeskClient com.billdesk.client INFO, DEBUG
LegacyJdkHttpClientExt com.billdesk.client INFO, DEBUG, WARN, ERROR
ApacheHttpClientExtension com.billdesk.client.extensions INFO, ERROR
JoseScheme com.billdesk.security INFO, DEBUG, ERROR

Logback example (logback.xml):

<logger name="com.billdesk.client" level="DEBUG"/>
<logger name="com.billdesk.security" level="DEBUG"/>

Set to INFO for production (masks payloads but still logs key lifecycle events). Set to DEBUG for request/response body debugging.

2.8 Auto-generated Headers

BillDesk requires two mandatory headers on every API call:

Header Purpose
BD-Traceid Idempotency key — must be unique per request
BD-Timestamp Request timestamp for replay detection

The SDK auto-generates these headers when not explicitly provided, so you don't need to manage them yourself. However, there are scenarios where you may want to override them (see examples below).

BD-Traceid — Idempotency

BillDesk uses BD-Traceid to detect duplicate requests. The same trace ID with a different payload may be treated as a replay attack, so always generate a unique value per request unless you specifically need idempotent behaviour for the same operation.

BD-Timestamp — Replay Protection

The timestamp must always be in IST (Asia/Kolkata). BillDesk validates this against its server clock to prevent replay attacks. The SDK sets the current IST time automatically when you don't provide one.

Override Examples

import com.billdesk.client.ApiRequest;
import java.time.Instant;

// Auto-generated both headers (simplest)
ApiResponse r1 = client.invoke(new ApiRequest("/path", payload));

// Custom BD-Traceid for idempotency (BD-Timestamp auto-generated)
ApiResponse r2 = client.invoke(ApiRequest.builder("/path", payload)
        .withTraceId("my-unique-trace-001").build());

// Custom BD-Timestamp only (BD-Traceid auto-generated)
ApiResponse r3 = client.invoke(ApiRequest.builder("/path", payload)
        .withTimestamp(Instant.now()).build());

// Override both headers
ApiResponse r4 = client.invoke(ApiRequest.builder("/path", payload)
        .withTraceId("my-unique-trace-002")
        .withTimestamp(Instant.now()).build());

// Access from response
String traceId = r4.getTraceId();    // "my-unique-trace-002"
Instant timestamp = r4.getTimestamp();  // parsed from BD-Timestamp header

Installation

Maven

<!-- Required: core client (pulls in billdesk-security-schemes transitively) -->
<dependency>
    <groupId>com.billdesk.client</groupId>
    <artifactId>billdesk-client</artifactId>
    <version>1.0.0</version>
</dependency>

<!-- Optional: security schemes — also published independently for webhook handling -->
<dependency>
    <groupId>com.billdesk.security</groupId>
    <artifactId>billdesk-security-schemes</artifactId>
    <version>1.0.0</version>
</dependency>

<!-- Optional: Apache HttpClient 5 -->
<dependency>
    <groupId>com.billdesk.client.extensions.apache</groupId>
    <artifactId>billdesk-client-extensions-apache-httpclient</artifactId>
    <version>1.0.0</version>
</dependency>

Gradle

// Required: core client (pulls in billdesk-security-schemes transitively)
implementation 'com.billdesk.client:billdesk-client:1.0.0'

// Optional: security schemes — also published independently for webhook handling
implementation 'com.billdesk.security:billdesk-security-schemes:1.0.0'

// Optional: Apache HttpClient 5
implementation 'com.billdesk.client.extensions.apache:billdesk-client-extensions-apache-httpclient:1.0.0'

Transitive Dependencies

These libraries are resolved automatically by Maven/Gradle but may be useful when managing classpaths manually or excluding dependencies in Spring Boot:

Dependency Required By Min Version
com.jayway.jsonpath:json-path Core masking (DefaultPayloadMasker) 2.9.0
com.nimbusds:nimbus-jose-jwt All crypto (HmacScheme, JoseScheme, SymmetricJoseScheme) 9.37.3
org.bouncycastle:bcprov-jdk18on JoseScheme (RSA-OAEP-256) 1.78.1
org.slf4j:slf4j-api Core logging 1.7.36
org.apache.hc.client5:httpclient5 ApacheHttpClientExtension 5.3

Note on slf4j-api version: The library depends on slf4j-api 1.7.36 for backward compatibility with applications using slf4j 1.x bindings. slf4j 1.x API is forward-compatible with slf4j 2.x implementations — if your application uses slf4j 2.x, it will work without conflict. You only need to ensure a compatible SLF4J binding is on your classpath.

If your project uses slf4j-api 2.x and you want to align all transitive dependencies to the same version, explicitly declare the slf4j-api 2.x dependency in your own pom.xml or build file. This will override the transitive 1.7.36 version pulled by billdesk-client:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.x</version>
</dependency>

Quick Start

Create a client with HMAC scheme and make an API call in under 2 minutes:

import com.billdesk.security.HmacScheme;
import com.billdesk.client.BillDeskClient;
import com.billdesk.client.Environments;
import com.billdesk.client.ApiResponse;
import com.billdesk.client.ApiRequest;

// NOTE: Replace with your actual BillDesk credentials
String clientId = "your-client-id";
String secretKey = "your-secret-key";

HmacScheme hmacScheme = new HmacScheme(clientId, secretKey);

String payload = createOrder(); // See "Create Order Payload" section below

try (BillDeskClient client = BillDeskClient.builder(hmacScheme)
        .withEnvironment(Environments.uat()) // Use Environments.production() for live
        .build()) {
    ApiResponse response = client.invoke(new ApiRequest("/payments/ve1_2/orders/create", payload));
    System.out.println("Status: " + response.getStatusCode());
    System.out.println("Body: " + response.getBody());
}

Client Creation

Minimal Example

import com.billdesk.security.HmacScheme;
import com.billdesk.client.BillDeskClient;

HmacScheme hmacScheme = new HmacScheme("your-client-id", "your-secret-key");

BillDeskClient client = BillDeskClient.builder(hmacScheme).build();
  • Environment defaults to production if not explicitly set.
  • HTTP client extension is automatically resolved by the platform: Java 11+ uses JdkHttpClientExt, Java 8–10 falls back to LegacyJdkHttpClientExt.
  • Payload masking is enabled by default — sensitive fields are automatically masked in logs.
  • BillDeskClient implements AutoCloseable — wrap in try-with-resources or call close() explicitly to release resources.

Builder Example (Recommended)

import com.billdesk.security.HmacScheme;
import com.billdesk.client.BillDeskClient;
import com.billdesk.client.JdkHttpClientExt;
import com.billdesk.client.Environments;

// NOTE: For Java 8–10, use LegacyJdkHttpClientExt instead
BillDeskClient client = BillDeskClient.builder(hmacScheme)
        .withEnvironment(Environments.production())
        .withHttpClientExtension(new JdkHttpClientExt(5000, 30000))
        .withMaskingEnabled()
        .build();

Constructor Example

The BillDeskClient constructor accepts all configuration directly:

import com.billdesk.client.BillDeskClient;
import com.billdesk.client.LegacyJdkHttpClientExt;
import com.billdesk.client.Environments;

// NOTE: Use actual credentials from BillDesk
// For Java 11 or above, prefer JdkHttpClientExt over LegacyJdkHttpClientExt
BillDeskClient client = new BillDeskClient(
        hmacScheme,                // PayloadSecurityScheme (required)
        Environments.production(), // Environment (defaults to production)
        new LegacyJdkHttpClientExt(),   // HttpClientExtension (null = JDK default)
        true                      // maskingEnabled (true by default)
);

Dependency Injection Example (Spring)

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BillDeskConfig {

    @Value("${billdesk.security.hmac.clientId}")
    private String clientId;

    @Value("${billdesk.security.hmac.secretKey}")
    private String secretKey;

    @Bean
    public BillDeskClient billDeskClient() {
        HmacScheme hmacScheme = new HmacScheme(clientId, secretKey);

        return BillDeskClient.builder(hmacScheme)
                .withEnvironment(Environments.production())
                .build();
    }
}

Environments

The Environments class provides factory methods for targeting BillDesk environments:

  • Environments.production() — BillDesk live production environment (cached singleton).
  • Environments.uat() — BillDesk UAT/staging environment (cached singleton).
  • Environments.custom(url) — Custom base URL for testing or mock servers (returns a new instance each call).

Create Order Payload

Use LinkedHashMap with Jackson's ObjectMapper to build the JSON payload — no string concatenation, no third-party JSON libraries required beyond Jackson:

import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;

ObjectMapper mapper = new ObjectMapper();
String orderDate = OffsetDateTime.now()
        .format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"));

Map<String, Object> device = new LinkedHashMap<>();
device.put("init_channel", "internet");
device.put("ip", "0.0.0.0");
device.put("user_agent", "Mozilla/5.0");

Map<String, Object> order = new LinkedHashMap<>();
order.put("mercid", System.getenv("BILLDESK_MERCHANT_ID"));
order.put("orderid", "ORDER-" + System.currentTimeMillis());
order.put("amount", "200.00");
order.put("order_date", orderDate);
order.put("currency", "356");
order.put("itemcode", "DIRECT");
order.put("ru", "https://yourdomain.com/callback");
order.put("device", device);

String payload = mapper.writeValueAsString(order);

Security Schemes

7.1 HMAC Scheme

The simplest security scheme using shared secret keys.

import com.billdesk.security.HmacScheme;

// NOTE: Replace with your actual BillDesk credentials
String clientId = "your-client-id";
String secretKey = "your-secret-key";

HmacScheme hmacScheme = new HmacScheme(clientId, secretKey);

7.2 JOSE Scheme (Asymmetric)

Uses RSA certificates for encryption and signing. Three initialization options:

Option A — Direct constructor with pre-computed thumbprint:

import com.billdesk.security.JoseScheme;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;

// NOTE: Use actual credentials and certificates from BillDesk
JoseScheme joseScheme = new JoseScheme(
        clientId,
        serverEncryptionCert,   // X509Certificate — encrypts requests
        serverSigningCert,      // X509Certificate — verifies responses
        clientPrivateKey,       // RSAPrivateKey — signs requests
        thumbprint              // String — SHA-256 JWK thumbprint (RFC 7638)
);

Option B — Builder with pre-computed thumbprint:

JoseScheme joseScheme = JoseScheme.builder()
        .clientId(clientId)
        .serverEncryptionCert(serverEncryptionCert)  // X509Certificate
        .serverSigningCert(serverSigningCert)        // X509Certificate
        .clientPrivateKey(clientPrivateKey)
        .thumbprint(thumbprint)  // Pre-computed SHA-256 JWK thumbprint (RFC 7638)
        .build();

Option C — Builder with auto-derived thumbprint (RFC 7638):

JoseScheme joseScheme = JoseScheme.builder()
        .clientId(clientId)
        .serverEncryptionCert(serverEncryptionCert)   // X509Certificate
        .serverSigningCert(serverSigningCert)        // X509Certificate
        .clientPrivateKey(clientPrivateKey)
        .clientPublicKey(clientPublicKey)             // X509Certificate — thumbprint derived automatically
        .build();

Note: JoseScheme.builder().build() (and the direct constructor) may throw IllegalArgumentException at construction time if required fields are missing (clientId, serverEncryptionCert, serverSigningCert, clientPrivateKey) or if any certificate is expired. Certificates are validated with zero clock-skew tolerance — ensure system clocks are synchronised.

Loading certificates and keys:

import java.security.KeyFactory;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.nio.file.Files;
import java.nio.file.Paths;

// Load server certificate (PEM .cer file)
X509Certificate serverEncryptionCert;
try (InputStream is = new FileInputStream("server_encryption.cer")) {
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    serverEncryptionCert = (X509Certificate) cf.generateCertificate(is);
}

// Load client private key (PKCS#8 PEM .key file)
RSAPrivateKey clientPrivateKey;
String pem = new String(Files.readAllBytes(Paths.get("client.key")))
        .replaceAll("-----.*-----", "").replaceAll("\\s", "");
byte[] keyBytes = Base64.getDecoder().decode(pem);
clientPrivateKey = (RSAPrivateKey) KeyFactory.getInstance("RSA")
        .generatePrivate(new PKCS8EncodedKeySpec(keyBytes));

7.3 Symmetric JOSE Scheme

Uses shared symmetric keys for both encryption and signing.

import com.billdesk.security.SymmetricJoseScheme;

// NOTE: Use actual credentials from BillDesk
String clientId        = "your client id";
String encryptionKeyId = "encryption key id";
String encryptionKey   = "your encryption key";    // 32 bytes for AES-256
String signingKeyId    = "signing key id";
String signingKey      = "your signing key";      // 32 bytes minimum for HS256

SymmetricJoseScheme symmetricScheme = new SymmetricJoseScheme(
        clientId,
        encryptionKeyId,
        encryptionKey,
        signingKeyId,
        signingKey
);

Exception Handling

Exception List

Exception FQCN Purpose
BillDeskClientTimeoutException com.billdesk.client.extensions.BillDeskClientTimeoutException HTTP request exceeded configured timeout
BillDeskClientException com.billdesk.client.extensions.BillDeskClientException Validation errors, network failures, connection refused
BillDeskSignatureVerificationException com.billdesk.security.BillDeskSignatureVerificationException Response signature verification failed — payload may be tampered
BillDeskSecurityException com.billdesk.security.BillDeskSecurityException Encryption/decryption failures, wrong keys

Exception Hierarchy

Exception
├── BillDeskClientException
│   └── BillDeskClientTimeoutException    (must catch before BillDeskClientException)
└── BillDeskSecurityException
    └── BillDeskSignatureVerificationException  (must catch before BillDeskSecurityException)

Exception Handling Example

import com.billdesk.client.BillDeskClient;
import com.billdesk.client.ApiResponse;
import com.billdesk.client.ApiRequest;
import com.billdesk.client.extensions.BillDeskClientException;
import com.billdesk.client.extensions.BillDeskClientTimeoutException;
import com.billdesk.security.BillDeskSecurityException;
import com.billdesk.security.BillDeskSignatureVerificationException;

try {
    ApiResponse response = client.invoke(new ApiRequest("/payments/ve1_2/orders/create", payload));

    if (response.getStatusCode() == 200) {
        // Success — parse response.getBody()
    } else {
        // Handle HTTP errors (400, 401, 409, etc.)
        System.err.println("BillDesk error: " + response.getBody());
    }

} catch (BillDeskClientTimeoutException e) {
    // Must catch BEFORE BillDeskClientException
    // Request timed out — may or may not have reached BillDesk
    // For payment creation: query order status before retrying
    System.err.println("Request timed out: " + e.getMessage());

} catch (BillDeskClientException e) {
    // Arbitrary network failure
    System.err.println("Client error: " + e.getMessage());

} catch (BillDeskSignatureVerificationException e) {
    // Must catch BEFORE BillDeskSecurityException
    // Response signature invalid — do NOT trust the body
    // Possible MITM attack or environment mismatch
    System.err.println("Signature verification FAILED: " + e.getMessage());

} catch (BillDeskSecurityException e) {
    // Encryption/decryption failed — check key configuration
    System.err.println("Security error: " + e.getMessage());
}

Exception Handling Tips

For timeout on payment creation:

} catch (BillDeskClientTimeoutException e) {
    // Never silently retry — query order status first to avoid double-charge
    ApiResponse statusResponse = client.invoke(
            new ApiRequest("/payments/ve1_2/orders/" + orderId,
            "{\"mercid\":\"" + merchantId + "\"}")
    );
    if (statusResponse.getStatusCode() == 200) {
        // Order already exists — do not retry
    }
}

For signature verification failure:

} catch (BillDeskSignatureVerificationException e) {
    // Do NOT process the response body — it may be tampered
    // Log and alert security team immediately
    // Return appropriate error to user without revealing details
}

HTTP Client Extension

You can use HTTP Client library of your own choice. To integrate your HTTP client library,

  1. Implement the interface (HttpClientExtension)
  2. Pass the implementation via .withHttpClientExtension()

Only one HTTP client extension can be active at a time.


Built-In Client Extensions

Introduction

The SDK ships with three HTTP client extension implementations:

Client Package Min JDK Connection Pooling Connect Timeout Request/Response Timeout
JdkHttpClientExt com.billdesk.client 11 Yes (shared HttpClient instance) Yes Yes
LegacyJdkHttpClientExt com.billdesk.client 8 No Yes Yes
ApacheHttpClientExtension com.billdesk.client.extensions 8 Yes Yes Yes

Constructors at a glance:

Class Constructors
JdkHttpClientExt JdkHttpClientExt(), JdkHttpClientExt(int, int), JdkHttpClientExt(HttpClient)
LegacyJdkHttpClientExt LegacyJdkHttpClientExt(), LegacyJdkHttpClientExt(int, int)
ApacheHttpClientExtension ApacheHttpClientExtension(CloseableHttpClient)

Note on Multi-Release JAR (MR-JAR): JdkHttpClientExt uses the Java 11+ java.net.http.HttpClient and is bundled under src/main/java11/. The JVM loads it automatically on Java 11+ and falls back to LegacyJdkHttpClientExt (Java 8 HttpURLConnection-based) on Java 8–10. You only need to import a class explicitly when calling its constructor with custom settings — otherwise the default is applied automatically.

JDK HTTP Client Extensions

The SDK ships two JDK-based HTTP client extensions, one for each Java generation:

JdkHttpClientExt (Java 11+)

Backed by java.net.http.HttpClient. Available automatically on Java 11+ via the Multi-Release JAR mechanism (no explicit import or configuration needed — it is the default). Use it explicitly when you need custom timeouts.

Three constructors:

import com.billdesk.client.JdkHttpClientExt;

// Constructor (1) — default timeouts: 5s connect, 30s request
new JdkHttpClientExt()

// Constructor (2) — custom timeouts (milliseconds)
new JdkHttpClientExt(10_000, 60_000)  // 10s connect, 60s request

// Constructor (3) — fully pre-configured HttpClient (all settings caller-controlled)
import java.net.http.HttpClient;
HttpClient custom = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .proxy(ProxySelector.of(...))
        .sslContext(mySSLContext)
        .version(HttpClient.Version.HTTP_1_1)  // force HTTP/1.1 if needed
        .build();
new JdkHttpClientExt(custom)  // no additional timeouts applied by SDK

Important: When using Constructor (3), the SDK does not apply a request-level timeout (the requestTimeout field is set to null). Timeouts and all other settings (proxy, SSL, redirect policy, etc.) are entirely your responsibility. If you need request-level timeouts, prefer Constructor (2).

Constructor Connect Timeout Request Timeout Caller-controlled settings
(1) no-arg 5,000 ms 30,000 ms None
(2) int, int First arg (ms) Second arg (ms) None
(3) HttpClient Caller's responsibility Not applied All (java.net.http.HttpClient)
LegacyJdkHttpClientExt (Java 8–10)

Backed by java.net.HttpURLConnection. This is the automatic default on Java 8–10. Use it explicitly when you need custom timeouts on older JVMs.

Two constructors:

import com.billdesk.client.LegacyJdkHttpClientExt;

// Constructor (1) — default timeouts: 5s connect, 30s read
new LegacyJdkHttpClientExt()

// Constructor (2) — custom timeouts (milliseconds)
new LegacyJdkHttpClientExt(10_000, 60_000)  // 10s connect, 60s read

Note on per-request timeout: LegacyJdkHttpClientExt does not support a per-request timeout (a known JDK 8 limitation). HttpURLConnection exposes only setReadTimeout() (socket read) and setConnectTimeout() (TCP connection establishment). Both are applied at connection open time.

Constructor Connect Timeout Read Timeout Per-request (socket) Timeout
(1) no-arg 5,000 ms 30,000 ms No
(2) int, int First arg (ms) Second arg (ms) No
Choosing Among the Three Extensions
LegacyJdkHttpClientExt JdkHttpClientExt ApacheHttpClientExtension
Min JDK 8 11 8
HTTP backend HttpURLConnection java.net.http.HttpClient Apache HTTP Client 5
Connection pooling No Yes (inherent in java.net.http.HttpClient) Yes
Connection timeout Yes Yes Yes
Request/response timeout No (read only) Yes Yes
Connection request timeout No No Yes (pool management)
Pre-configured client No Yes (HttpClient) Yes (CloseableHttpClient)
Bundle size increase Adds Apache transitive deps

Recommendation:

  • Java 11+: Prefer JdkHttpClientExt — it is the automatic default and modern HTTP semantics are better for production. Use new JdkHttpClientExt(connectMs, requestMs) for custom timeouts.
  • Java 8–10: Use LegacyJdkHttpClientExt — the automatic default. Use new LegacyJdkHttpClientExt(connectMs, readMs) for custom timeouts.
  • Need Apache features (connection pooling tuning, granular pool timeouts, proxy authentication): use ApacheHttpClientExtension.
  • Need full control over HTTP client settings on Java 11+: use new JdkHttpClientExt(prebuiltHttpClient).

Examples using explicit constructors with the builder:

// Java 11+ with custom timeouts
import com.billdesk.client.JdkHttpClientExt;

BillDeskClient client = BillDeskClient.builder(scheme)
        .withEnvironment(Environments.production())
        .withHttpClientExtension(new JdkHttpClientExt(10_000, 60_000))
        .build();

// Java 8 with custom timeouts
import com.billdesk.client.LegacyJdkHttpClientExt;

BillDeskClient client = BillDeskClient.builder(scheme)
        .withEnvironment(Environments.production())
        .withHttpClientExtension(new LegacyJdkHttpClientExt(10_000, 60_000))
        .build();

Apache HttpClient Extension

The SDK provides an extension implemented using the Apache HTTP Client 5 library:

import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.util.Timeout;
import com.billdesk.client.extensions.ApacheHttpClientExtension;

RequestConfig requestConfig = RequestConfig.custom()
        .setConnectTimeout(Timeout.ofMilliseconds(5000))
        .setResponseTimeout(Timeout.ofMilliseconds(30000))
        .setConnectionRequestTimeout(Timeout.ofMilliseconds(30000))
        .build();

CloseableHttpClient httpClient = HttpClients.custom()
        .setDefaultRequestConfig(requestConfig)
        .disableAutomaticRetries()
        .build();

BillDeskClient client = BillDeskClient.builder(scheme)
        .withHttpClientExtension(new ApacheHttpClientExtension(httpClient))
        .build();

Timeout Explanation

Apache HttpClient 5 supports three distinct timeouts:

Timeout Apache Method Purpose
connectTimeout setConnectTimeout Time to establish TCP connection to BillDesk server
responseTimeout setResponseTimeout Time to wait for data after request is sent (socket read timeout)
connectionRequestTimeout setConnectionRequestTimeout Time to wait to obtain a connection from the connection pool

Default values: 30 seconds for response and connection request timeout, 5 seconds for connect timeout.


Webhook Handling

Overview

Webhooks (S2S callbacks) can be handled by creating a PayloadSecurityScheme and calling unseal() on the received token.

Token format varies by scheme:

Scheme Token Format
HmacScheme JWS compact serialization (HS256 signed)
JoseScheme Nested JWE + JWS token
SymmetricJoseScheme Nested JWE + JWS token

Platform Example — Servlet (Accepting Form Post)

import javax.servlet.http.*;
import java.io.*;
import java.util.stream.Collectors;
import com.billdesk.security.HmacScheme;
import com.billdesk.security.BillDeskSignatureVerificationException;
import com.billdesk.security.BillDeskSecurityException;

// NOTE: Use actual credentials from BillDesk
HmacScheme hmacScheme = new HmacScheme(
        System.getenv("BILLDESK_CLIENT_ID"),
        System.getenv("BILLDESK_SECRET_KEY")
);

public class BillDeskWebhookServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // Extract token from the request body (S2S webhook sends token as entire body)
        String sealedToken = req.getReader().lines().collect(Collectors.joining());

        resp.setContentType("text/plain");
        resp.setStatus(HttpServletResponse.SC_OK);

        try {
            // Unseal the token — verifies signature and decrypts
            String transactionJson = hmacScheme.unseal(sealedToken);

            // Parse transactionJson and update order status
            // Example fields: orderid, txnid, transaction_status, amount

            resp.getWriter().write("OK");

        } catch (BillDeskSignatureVerificationException e) {
            // Signature verification failed — do NOT trust the payload
            // Still return OK to stop BillDesk from retrying
            resp.getWriter().write("OK");
            // Log and alert — possible security issue

        } catch (BillDeskSecurityException e) {
            // Decryption failed — check key configuration
            resp.getWriter().write("OK");
            // Log and investigate
        }
    }
}

Always Return "OK" with HTTP 200

BillDesk expects your webhook endpoint to return:

  • HTTP Status: 200
  • Body: OK

This confirms receipt and stops BillDesk from retrying delivery. Always return OK — even if unseal() fails — but do not process the payload if signature verification fails.

Signature Verification Failure

When BillDeskSignatureVerificationException is thrown:

  • Do NOT process the payload — it may have been tampered with
  • Return HTTP 200 with "OK" anyway to stop retries
  • Log and alert your security team for investigation

Idempotency — Same Webhook Multiple Times

BillDesk may deliver the same webhook more than once. Your handler must be idempotent:

try {
    String transactionJson = scheme.unseal(sealedToken);

    // Parse and extract orderid/txnid
    // Check if already processed (database lookup)
    // If already processed, return OK without further action

    // Process the transaction
    // Update order status in database

    resp.getWriter().write("OK");

} catch (BillDeskSignatureVerificationException e) {
    resp.getWriter().write("OK");
    // Log and alert for investigation
}

Verify Additional Fields

In addition to verifying the signature, always validate relevant fields in the payload:

String transactionJson = scheme.unseal(sealedToken);
// Parse JSON and verify:
String orderId = ...;      // Matches your original order ID
String txnId = ...;        // Transaction ID from BillDesk
String authStatus = ...;   // Check auth_status is "SUCCESS" or "NA"
String amount = ...;       // Verify amount matches expected value (prevents discripancy)

Important: Do not rely solely on amount verification. Always check orderid, transactionid, and auth_status to ensure the webhook corresponds to the correct transaction.

About

BillDesk PG integration java client

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages