API Development
SOAP Services, Real Endpoints, and Why REST Isn't Here
🎯 What You'll Learn
- How Dione's SOAP-based API architecture actually works
- Real WSDL contracts and service implementations
- Authentication patterns and session management
- Business API endpoints and their quirks
- Debugging techniques and error handling
SOAP Services Reality
⚠️ No REST Here
Dione is a SOAP-only system. Every API endpoint is SOAP-based using Apache CXF. There are no REST endpoints, no JSON APIs, and no GraphQL. Understanding this is crucial.
Why SOAP in 2024?
Before you question this choice, understand the business context:
- Financial Industry Standard: When Dione was built, SOAP was the financial services standard
- Strong Typing: WSDL contracts provide compile-time safety for financial transactions
- Compliance Requirements: Auditors understand SOAP contracts better than REST documentation
- Tooling: Financial messaging standards (ISO 20022, FIX) have better SOAP tooling
- Error Handling: SOAP faults provide structured error information
Service Architecture Overview
┌─────────────────────────────────────┐
│ Client Applications │
│ (Customer Portal, Admin Panel) │
└─────────────────────────────────────┘
│ HTTP/SOAP
▼
┌─────────────────────────────────────┐
│ Apache CXF Endpoint │
│ (service-definition-beans.xml) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Service Implementation │
│ (AuthServiceIMPL, CustomerIMPL) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Handler Layer │
│ (Business Logic + Validation) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ DAO Layer │
│ (Database Operations) │
└─────────────────────────────────────┘
Service Endpoints Overview
All SOAP services are defined in the CXF configuration and deployed on JBoss. Here's what's actually available:
<!-- Authentication & Session Management -->
<jaxws:endpoint id="auth"
implementor="com.currenciesdirect.gtg.ngop.business.services.auth.AuthServiceIMPL"
address="/authServices"
serviceName="ns1:authService"
endpointName="ns1:authPort"
xmlns:ns1="http://com.currenciesdirect.gtg.ngop.business.services.auth/" />
<!-- Customer Management -->
<jaxws:endpoint id="customer"
implementor="com.currenciesdirect.gtg.ngop.business.services.customer.CustomerManagementIMPL"
address="/customerServices"
serviceName="ns2:customerService"
endpointName="ns2:customerPort"
xmlns:ns2="http://com.currenciesdirect.gtg.ngop.business.services.customer/" />
<!-- Payment Processing -->
<jaxws:endpoint id="payment"
implementor="com.currenciesdirect.gtg.ngop.business.services.pfx.payment.PaymentManagementIMPL"
address="/paymentServices"
serviceName="ns3:paymentService"
endpointName="ns3:paymentPort"
xmlns:ns3="http://com.currenciesdirect.gtg.ngop.business.services.pfx.payment/" />
<!-- FX Trading -->
<jaxws:endpoint id="fxTrading"
implementor="com.currenciesdirect.gtg.ngop.business.services.pfx.fxtrading.FXTradingIMPL"
address="/fxTradingServices"
serviceName="ns4:fxTradingService"
endpointName="ns4:fxTradingPort"
xmlns:ns4="http://com.currenciesdirect.gtg.ngop.business.services.pfx.fxtrading/" />
<!-- Open Banking -->
<jaxws:endpoint id="openBanking"
implementor="com.currenciesdirect.gtg.ngop.business.services.pfx.openbanking.OpenBankingIMPL"
address="/openBankingServices"
serviceName="ns5:openBankingService"
endpointName="ns5:openBankingPort"
xmlns:ns5="http://com.currenciesdirect.gtg.ngop.business.services.pfx.openbanking/" />
Service URL Pattern
All services follow this URL pattern:
# Development Environment
http://localhost:8080/Dione/services/{serviceName}
# Production Environment
https://api.currenciesdirect.com/Dione/services/{serviceName}
# Actual Service Endpoints
http://localhost:8080/Dione/services/authServices?wsdl
http://localhost:8080/Dione/services/customerServices?wsdl
http://localhost:8080/Dione/services/paymentServices?wsdl
http://localhost:8080/Dione/services/fxTradingServices?wsdl
WSDL Contracts & Client Generation
Generating Client Stubs
To consume Dione services, you generate client stubs from WSDL contracts:
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<wsdlOptions>
<wsdlOption>
<wsdl>http://localhost:8080/Dione/services/authServices?wsdl</wsdl>
<packagenames>
<packagename>com.currenciesdirect.client.auth</packagename>
</packagenames>
</wsdlOption>
<wsdlOption>
<wsdl>http://localhost:8080/Dione/services/customerServices?wsdl</wsdl>
<packagenames>
<packagename>com.currenciesdirect.client.customer</packagename>
</packagenames>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
Client Invocation Pattern
@Component
public class AuthServiceInvoker {
private WSAuth authService;
@PostConstruct
public void initializeService() {
try {
// Create service from WSDL
URL wsdlLocation = new URL("http://localhost:8080/Dione/services/authServices?wsdl");
AuthService service = new AuthService(wsdlLocation);
this.authService = service.getAuthPort();
// Configure timeouts
Client client = ClientProxy.getClient(authService);
HTTPConduit conduit = (HTTPConduit) client.getConduit();
HTTPClientPolicy policy = new HTTPClientPolicy();
policy.setConnectionTimeout(30000); // 30 seconds
policy.setReceiveTimeout(60000); // 60 seconds
conduit.setClient(policy);
} catch (Exception e) {
LOG.error("Failed to initialize auth service", e);
throw new RuntimeException("Cannot connect to authentication service");
}
}
public AuthResponse loginCustomer(String email, String password, String orgCode) {
try {
// Build request
AuthRequest request = new AuthRequest();
request.setEmail(email);
request.setPassword(password);
request.setOrgCode(orgCode);
// Call service
Holder<ResponseStatusHeader> statusHeader = new Holder<>();
AuthResponse response = authService.login(statusHeader, request, "en", "WEB");
// Check for errors
if (statusHeader.value != null && !"SUCCESS".equals(statusHeader.value.getStatus())) {
throw new ServiceException(statusHeader.value.getMessage());
}
return response;
} catch (Exception e) {
LOG.error("Login failed for user: " + email, e);
throw new ServiceException("Authentication failed: " + e.getMessage());
}
}
}
Authentication API
Login Operation
The most-used API endpoint. Understanding its quirks is essential:
@WebService(serviceName = "authService", portName = "authPort",
targetNamespace = "http://com.currenciesdirect.gtg.ngop.business.services.auth/")
public class AuthServiceIMPL implements WSAuth {
@Autowired
private LoginAndProfileHandlerInterface loginAndProfileHandlerRef;
@Override
public AuthResponse login(Holder<ResponseStatusHeader> responseStatusHeader,
AuthRequest authRequest, String locale, String source) {
LOG.info("Login attempt for user: {} org: {} source: {}",
authRequest.getEmail(), authRequest.getOrgCode(), source);
try {
// Business logic delegation
PojoMap sessionData = loginAndProfileHandlerRef.createNGOPSession(
responseStatusHeader, authRequest, locale, source);
// Build successful response
AuthResponse response = new AuthResponse();
response.setSessionID(sessionData.getString("sessionID"));
response.setCustomerID(sessionData.getInteger("customerID"));
response.setOrgID(sessionData.getInteger("orgID"));
response.setProfile(buildCustomerProfile(sessionData));
// Success status
ResponseStatusHeader successHeader = new ResponseStatusHeader();
successHeader.setStatus("SUCCESS");
successHeader.setMessage("Login successful");
responseStatusHeader.value = successHeader;
return response;
} catch (NGOPBaseException e) {
// Business exception (invalid credentials, etc.)
LOG.warn("Login failed for {}: {}", authRequest.getEmail(), e.getMessage());
ResponseStatusHeader errorHeader = new ResponseStatusHeader();
errorHeader.setStatus("BUSINESS_ERROR");
errorHeader.setMessage(e.getMessage());
errorHeader.setErrorCode(e.getErrorCode());
responseStatusHeader.value = errorHeader;
return new AuthResponse(); // Empty response on error
} catch (Exception e) {
// System exception
LOG.error("System error during login for " + authRequest.getEmail(), e);
ResponseStatusHeader errorHeader = new ResponseStatusHeader();
errorHeader.setStatus("SYSTEM_ERROR");
errorHeader.setMessage("Internal system error");
responseStatusHeader.value = errorHeader;
return new AuthResponse();
}
}
private CustomerProfile buildCustomerProfile(PojoMap sessionData) {
CustomerProfile profile = new CustomerProfile();
profile.setFirstName(sessionData.getString("firstName"));
profile.setLastName(sessionData.getString("lastName"));
profile.setEmail(sessionData.getString("email"));
profile.setKycStatus(sessionData.getString("kycStatus"));
profile.setAccountStatus(sessionData.getString("accountStatus"));
return profile;
}
}
Session Management
@Override
public ValidateSessionResponse validateSession(Holder<ResponseStatusHeader> responseStatusHeader,
ValidateSessionRequest request) {
String sessionId = request.getSessionID();
try {
// Get session from cache
PojoMap sessionData = cacheDataConsumer.get(sessionId);
if (sessionData == null) {
throw new NGOPBaseException("Session expired or invalid", "SESSION_INVALID");
}
// Check session timeout
Long lastActivity = sessionData.getLong("lastActivity");
if (System.currentTimeMillis() - lastActivity > SESSION_TIMEOUT) {
cacheDataConsumer.remove(sessionId);
throw new NGOPBaseException("Session timeout", "SESSION_TIMEOUT");
}
// Update last activity
sessionData.put("lastActivity", System.currentTimeMillis());
cacheDataConsumer.put(sessionId, sessionData, SESSION_TIMEOUT);
// Build response
ValidateSessionResponse response = new ValidateSessionResponse();
response.setCustomerID(sessionData.getInteger("customerID"));
response.setOrgID(sessionData.getInteger("orgID"));
response.setValid(true);
return response;
} catch (NGOPBaseException e) {
ValidateSessionResponse response = new ValidateSessionResponse();
response.setValid(false);
response.setReason(e.getMessage());
return response;
}
}
Customer Management API
Key Operations
- getCustomerProfile: Retrieve complete customer information
- updateCustomerProfile: Modify customer details
- getCustomerAccounts: List customer's currency accounts
- customerKYCUpdate: Update Know Your Customer status
@Override
public GetCustomerProfileResponse getCustomerProfile(
Holder<ResponseStatusHeader> responseStatusHeader,
GetCustomerProfileRequest request) {
Integer customerId = request.getCustomerID();
String sessionId = request.getSessionID();
try {
// Validate session
PojoMap sessionData = validateSessionAndGetData(sessionId);
// Security check - ensure customer can only access their own data
Integer sessionCustomerId = sessionData.getInteger("customerID");
if (!customerId.equals(sessionCustomerId)) {
throw new NGOPBaseException("Access denied", "ACCESS_DENIED");
}
// Get customer from database
Customer customer = customerDAO.getCustomerById(customerId);
if (customer == null) {
throw new NGOPBaseException("Customer not found", "CUSTOMER_NOT_FOUND");
}
// Transform to response object
GetCustomerProfileResponse response = new GetCustomerProfileResponse();
response.setCustomer(transformCustomerToProfile(customer));
// Get customer accounts
List<Account> accounts = accountDAO.getAccountsByCustomerId(customerId);
response.setAccounts(transformAccountsToProfileAccounts(accounts));
return response;
} catch (NGOPBaseException e) {
handleBusinessError(responseStatusHeader, e);
return new GetCustomerProfileResponse();
}
}
private CustomerProfile transformCustomerToProfile(Customer customer) {
CustomerProfile profile = new CustomerProfile();
profile.setCustomerID(customer.getCustomerId());
profile.setFirstName(customer.getFirstName());
profile.setLastName(customer.getLastName());
profile.setEmail(customer.getEmail());
profile.setPhone(customer.getPhone());
profile.setDateOfBirth(customer.getDateOfBirth());
profile.setKycStatus(customer.getKycStatus());
profile.setAccountStatus(customer.getAccountStatus());
// Address information
if (customer.getAddress() != null) {
Address address = new Address();
address.setLine1(customer.getAddress().getLine1());
address.setLine2(customer.getAddress().getLine2());
address.setCity(customer.getAddress().getCity());
address.setPostcode(customer.getAddress().getPostcode());
address.setCountry(customer.getAddress().getCountry());
profile.setAddress(address);
}
return profile;
}
Payment Processing API
Create Payment Flow
The payment API is the most complex because it orchestrates multiple systems:
@Override
public CreatePaymentResponse createPayment(
Holder<ResponseStatusHeader> responseStatusHeader,
CreatePaymentRequest request) {
LOG.info("Creating payment: {} {} {} → {} for customer {}",
request.getAmount(), request.getFromCurrency(),
request.getToCurrency(), request.getCustomerID());
try {
// 1. Validate session
PojoMap sessionData = validateSession(request.getSessionID());
// 2. Validate payment request
PaymentValidationResult validation = paymentValidationHandler.validatePaymentRequest(
request, sessionData);
if (!validation.isValid()) {
throw new NGOPBaseException(validation.getErrorMessage(), "VALIDATION_ERROR");
}
// 3. Get exchange rate from pricing engine
FxRate exchangeRate = pricingEngineService.getExchangeRate(
request.getFromCurrency(), request.getToCurrency(),
request.getAmount(), sessionData.getInteger("orgID"));
// 4. Create payment record
Payment payment = new Payment();
payment.setCustomerId(request.getCustomerID());
payment.setFromCurrency(request.getFromCurrency());
payment.setToCurrency(request.getToCurrency());
payment.setFromAmount(request.getAmount());
payment.setExchangeRate(exchangeRate.getRate());
payment.setToAmount(request.getAmount().multiply(exchangeRate.getRate()));
payment.setStatus("PENDING");
payment.setCreatedDate(new Date());
payment.setCreatedBy(sessionData.getInteger("customerID"));
// 5. Persist payment
payment = paymentDAO.save(payment);
// 6. Submit to payment processor (Titan)
TitanPaymentRequest titanRequest = buildTitanRequest(payment, request);
TitanPaymentResponse titanResponse = titanPaymentService.submitPayment(titanRequest);
// 7. Update payment with processor response
payment.setProcessorReferenceId(titanResponse.getReferenceId());
payment.setProcessorStatus(titanResponse.getStatus());
payment = paymentDAO.update(payment);
// 8. Build response
CreatePaymentResponse response = new CreatePaymentResponse();
response.setPaymentID(payment.getPaymentId());
response.setStatus(payment.getStatus());
response.setExchangeRate(payment.getExchangeRate());
response.setToAmount(payment.getToAmount());
response.setEstimatedSettlement(calculateSettlementDate(payment));
return response;
} catch (NGOPBaseException e) {
LOG.warn("Payment creation failed: {}", e.getMessage());
handleBusinessError(responseStatusHeader, e);
return new CreatePaymentResponse();
} catch (Exception e) {
LOG.error("System error creating payment", e);
handleSystemError(responseStatusHeader, "Payment creation failed");
return new CreatePaymentResponse();
}
}
private TitanPaymentRequest buildTitanRequest(Payment payment, CreatePaymentRequest request) {
TitanPaymentRequest titanRequest = new TitanPaymentRequest();
titanRequest.setAmount(payment.getFromAmount());
titanRequest.setCurrency(payment.getFromCurrency());
titanRequest.setCustomerReference(payment.getPaymentId().toString());
// Beneficiary details
titanRequest.setBeneficiaryName(request.getBeneficiaryName());
titanRequest.setBeneficiaryAccount(request.getBeneficiaryAccount());
titanRequest.setBeneficiaryBank(request.getBeneficiaryBank());
return titanRequest;
}
FX Trading API
Real-Time Rate Quotes
@Override
public GetRateQuoteResponse getRateQuote(
Holder<ResponseStatusHeader> responseStatusHeader,
GetRateQuoteRequest request) {
try {
// Validate session
validateSession(request.getSessionID());
// Get live rate from pricing engine
FxRate rate = pricingEngineService.getLiveRate(
request.getFromCurrency(),
request.getToCurrency(),
request.getAmount(),
request.getOrgID());
// Apply customer-specific margins
BigDecimal customerRate = applyCustomerMargin(rate.getRate(),
request.getCustomerID(), request.getFromCurrency());
// Build response
GetRateQuoteResponse response = new GetRateQuoteResponse();
response.setFromCurrency(request.getFromCurrency());
response.setToCurrency(request.getToCurrency());
response.setFromAmount(request.getAmount());
response.setExchangeRate(customerRate);
response.setToAmount(request.getAmount().multiply(customerRate));
response.setQuoteExpiry(calculateQuoteExpiry()); // 30 seconds
response.setQuoteReference(generateQuoteReference());
// Cache quote for later execution
cacheService.putQuote(response.getQuoteReference(), response, 30000);
return response;
} catch (Exception e) {
LOG.error("Failed to get rate quote", e);
handleSystemError(responseStatusHeader, "Rate service unavailable");
return new GetRateQuoteResponse();
}
}
private BigDecimal applyCustomerMargin(BigDecimal baseRate, Integer customerId, String currency) {
// Get customer tier (determines margin)
CustomerTier tier = customerDAO.getCustomerTier(customerId);
// Get currency-specific margin
BigDecimal margin = marginConfigService.getMargin(tier, currency);
// Apply margin (add for buy, subtract for sell)
return baseRate.multiply(BigDecimal.ONE.add(margin));
}
API Debugging & Testing
SOAP UI Testing
SoapUI is your best friend for testing Dione APIs:
<!-- Sample Login Request -->
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:auth="http://com.currenciesdirect.gtg.ngop.business.services.auth/">
<soapenv:Header/>
<soapenv:Body>
<auth:login>
<auth:responseStatusHeader/>
<auth:authRequest>
<auth:email>test@currenciesdirect.com</auth:email>
<auth:password>password123</auth:password>
<auth:orgCode>CD</auth:orgCode>
</auth:authRequest>
<auth:locale>en</auth:locale>
<auth:source>WEB</auth:source>
</auth:login>
</soapenv:Body>
</soapenv:Envelope>
Logging and Monitoring
<!-- logback.xml configuration for API debugging -->
<logger name="com.currenciesdirect.gtg.ngop.business.services" level="DEBUG"/>
<logger name="org.apache.cxf" level="INFO"/>
<logger name="org.apache.cxf.interceptor" level="DEBUG"/>
<!-- Enable SOAP message logging -->
<logger name="org.apache.cxf.services" level="DEBUG"/>
Common Debugging Commands
# Test WSDL accessibility
curl -v "http://localhost:8080/Dione/services/authServices?wsdl"
# Check service health
curl -X POST -H "Content-Type: text/xml" \
-d @login-request.xml \
"http://localhost:8080/Dione/services/authServices"
# Monitor JBoss logs for API calls
tail -f /opt/jboss/standalone/log/server.log | grep "AuthServiceIMPL"
# Check database for recent API activity
psql -U dione_user -d dione_db -c \
"SELECT * FROM audit_log WHERE operation_type = 'API_CALL' ORDER BY created_date DESC LIMIT 10;"
# Verify cache entries for sessions
echo "GET session:12345" | redis-cli
Error Handling Patterns
Standard Error Response Format
All Dione SOAP services use a consistent error handling pattern:
// Standard error handling in all service implementations
public class BaseServiceImpl {
protected void handleBusinessError(Holder<ResponseStatusHeader> responseStatusHeader,
NGOPBaseException e) {
ResponseStatusHeader errorHeader = new ResponseStatusHeader();
errorHeader.setStatus("BUSINESS_ERROR");
errorHeader.setMessage(e.getMessage());
errorHeader.setErrorCode(e.getErrorCode());
errorHeader.setTimestamp(new Date());
responseStatusHeader.value = errorHeader;
LOG.warn("Business error: {} ({})", e.getMessage(), e.getErrorCode());
}
protected void handleSystemError(Holder<ResponseStatusHeader> responseStatusHeader,
String message) {
ResponseStatusHeader errorHeader = new ResponseStatusHeader();
errorHeader.setStatus("SYSTEM_ERROR");
errorHeader.setMessage(message);
errorHeader.setErrorCode("SYS_ERROR");
errorHeader.setTimestamp(new Date());
responseStatusHeader.value = errorHeader;
LOG.error("System error: {}", message);
}
}
Common Error Codes
| Error Code | Description | Resolution |
|---|---|---|
| AUTH_FAILED | Invalid credentials | Check email/password combination |
| SESSION_INVALID | Session expired or not found | Login again to get new session |
| VALIDATION_ERROR | Request validation failed | Check required fields and formats |
| INSUFFICIENT_FUNDS | Customer account balance too low | Customer needs to fund account |
| RATE_EXPIRED | FX rate quote has expired | Get new rate quote |
| EXTERNAL_SERVICE_ERROR | Downstream service unavailable | Retry later or check service status |
Client-Side Error Handling
public class RobustServiceClient {
public AuthResponse loginWithRetry(String email, String password, String orgCode) {
int maxRetries = 3;
int delay = 1000; // 1 second
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
Holder<ResponseStatusHeader> statusHeader = new Holder<>();
AuthRequest request = buildAuthRequest(email, password, orgCode);
AuthResponse response = authService.login(statusHeader, request, "en", "WEB");
// Check response status
if (statusHeader.value != null) {
String status = statusHeader.value.getStatus();
if ("SUCCESS".equals(status)) {
return response;
} else if ("BUSINESS_ERROR".equals(status)) {
// Don't retry business errors
throw new BusinessException(statusHeader.value.getMessage(),
statusHeader.value.getErrorCode());
} else if ("SYSTEM_ERROR".equals(status)) {
LOG.warn("System error on attempt {}: {}", attempt, statusHeader.value.getMessage());
if (attempt == maxRetries) {
throw new SystemException("Service unavailable after " + maxRetries + " attempts");
}
// Wait before retry
Thread.sleep(delay * attempt);
continue;
}
}
return response;
} catch (BusinessException e) {
// Don't retry business errors
throw e;
} catch (Exception e) {
LOG.warn("Connection error on attempt {}: {}", attempt, e.getMessage());
if (attempt == maxRetries) {
throw new SystemException("Service unavailable: " + e.getMessage());
}
try {
Thread.sleep(delay * attempt);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new SystemException("Interrupted during retry");
}
}
}
throw new SystemException("Unexpected error in retry logic");
}
}
⚠️ API Development Best Practices
- Always validate sessions - every API call must check session validity
- Use consistent error handling - follow the ResponseStatusHeader pattern
- Log everything - API calls are audited for compliance
- Handle timeouts gracefully - external services can be slow
- Validate business rules - don't rely on client-side validation
- Use database transactions - financial operations must be atomic