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.
-
- 2.1 Multiple Security Schemes
- 2.2 Extension Support
- 2.3 Single Invoke Method API
- 2.4 Payload Masking
- 2.5 Exception Hierarchy
- 2.6 Logging
- 2.7 Auto-generated Headers
-
- 3.1 Maven
- 3.2 Gradle
- 3.3 Transitive Dependencies
-
- 7.1 HMAC Scheme
- 7.2 JOSE Scheme (Asymmetric)
- 7.3 Symmetric JOSE Scheme
-
- 10.1 BD-Traceid (Idempotency Key)
- 10.2 BD-Timestamp
- 10.3 Auto-generation Note
- 10.4 Code Example — Sending Headers
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
| 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 |
The library is extensible via SPI interfaces:
- HttpClientExtension — Plug in custom HTTP clients (JDK built-in or Apache HttpClient 5)
ApiResponse response = client.invoke(new ApiRequest("/payments/ve1_2/orders/create", payload));Works for every BillDesk API endpoint. No per-endpoint method calls needed.
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
WARNlog will indicate the masking failure. For full payload visibility during development, disable masking temporarily with.withMaskingDisabled().
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 |
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.
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).
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.
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.
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<!-- 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>// 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'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-api1.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-api2.x and you want to align all transitive dependencies to the same version, explicitly declare theslf4j-api2.x dependency in your ownpom.xmlor build file. This will override the transitive 1.7.36 version pulled bybilldesk-client:<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.x</version> </dependency>
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());
}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
productionif not explicitly set. - HTTP client extension is automatically resolved by the platform: Java 11+ uses
JdkHttpClientExt, Java 8–10 falls back toLegacyJdkHttpClientExt. - Payload masking is enabled by default — sensitive fields are automatically masked in logs.
BillDeskClientimplementsAutoCloseable— wrap intry-with-resourcesor callclose()explicitly to release resources.
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();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)
);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();
}
}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).
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);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);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 throwIllegalArgumentExceptionat 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));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 | 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
├── BillDeskClientException
│ └── BillDeskClientTimeoutException (must catch before BillDeskClientException)
└── BillDeskSecurityException
└── BillDeskSignatureVerificationException (must catch before BillDeskSecurityException)
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());
}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
}You can use HTTP Client library of your own choice. To integrate your HTTP client library,
- Implement the interface (
HttpClientExtension) - Pass the implementation via
.withHttpClientExtension()
Only one HTTP client extension can be active at a time.
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):
JdkHttpClientExtuses the Java 11+java.net.http.HttpClientand is bundled undersrc/main/java11/. The JVM loads it automatically on Java 11+ and falls back toLegacyJdkHttpClientExt(Java 8HttpURLConnection-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.
The SDK ships two JDK-based HTTP client extensions, one for each Java generation:
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 SDKImportant: When using Constructor (3), the SDK does not apply a request-level timeout (the
requestTimeoutfield is set tonull). 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) |
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 readNote on per-request timeout:
LegacyJdkHttpClientExtdoes not support a per-request timeout (a known JDK 8 limitation).HttpURLConnectionexposes onlysetReadTimeout()(socket read) andsetConnectTimeout()(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 |
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. Usenew JdkHttpClientExt(connectMs, requestMs)for custom timeouts. - Java 8–10: Use
LegacyJdkHttpClientExt— the automatic default. Usenew 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();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();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.
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 |
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
}
}
}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.
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
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
}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.