Java Integration Guide
This guide shows you how to integrate Oncade APIs into Java applications. We provide examples for standalone Java applications using the built-in HTTP client, Spring Boot applications, and Jakarta RESTful Web Services (JAX-RS) for server environments.
Table of Contents
Prerequisites
- Java 11 or higher (Java 21 recommended for best performance)
- Oncade developer account with access to the game you are integrating
- Server API key and game ID from DevPortal → Games
- Secure environment variables for
ONCADE_SERVER_API_KEY,ONCADE_GAME_ID,ONCADE_API_BASE_URL, andONCADE_WEBHOOK_SECRET - Maven 3.6+ or Gradle (if using a build tool)
Project Setup
For Maven projects, add the following dependencies to your pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yourcompany</groupId>
<artifactId>oncade-integration</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Jakarta RESTful Web Services -->
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>4.0.0</version>
</dependency>
<!-- Jackson for JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<!-- JUnit for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>If you're using Gradle, add equivalent dependencies to your build.gradle file.
1. Making API Calls with Java HTTP Client
For standalone Java applications, use the built-in java.net.http.HttpClient (available since Java 11) to make requests to Oncade APIs. This approach works well for simple integrations or when you don't have a JAX-RS runtime.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.UUID;
public class OncadeApiClient {
private static final String BASE_URL = System.getenv().getOrDefault("ONCADE_API_BASE_URL", "https://oncade.gg");
private static final String API_KEY = System.getenv("ONCADE_SERVER_API_KEY");
private static final String GAME_ID = System.getenv("ONCADE_GAME_ID");
private final HttpClient client;
public OncadeApiClient() {
this.client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
}
public HttpResponse<String> initiateAccountLink(String email) throws Exception {
String body = String.format("{\"email\": \"%s\"}", email);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/v1/users/link/initiate"))
.header("Authorization", "Bearer " + API_KEY)
.header("X-Game-Id", GAME_ID)
.header("X-Oncade-API-Version", "v1")
.header("Content-Type", "application/json")
.header("Idempotency-Key", UUID.randomUUID().toString())
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
public HttpResponse<String> getLinkDetails(String sessionKey) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/v1/users/link/details?session=" + sessionKey))
.header("Authorization", "Bearer " + API_KEY)
.header("X-Game-Id", GAME_ID)
.header("X-Oncade-API-Version", "v1")
.GET()
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
}Usage Example
OncadeApiClient client = new OncadeApiClient();
// Initiate account linking
HttpResponse<String> response = client.initiateAccountLink("player@example.com");
if (response.statusCode() == 200) {
// Parse JSON response
String jsonResponse = response.body();
// Extract url and sessionKey from response
}
// Poll for link details
HttpResponse<String> detailsResponse = client.getLinkDetails(sessionKey);
if (detailsResponse.statusCode() == 200) {
// Parse and check userRef field
}2. Spring Boot Integration
Spring Boot provides a powerful and opinionated framework for building Java applications. It offers dependency injection, auto-configuration, and excellent support for REST APIs. This approach is ideal for modern Java applications that benefit from Spring's ecosystem.
Spring Boot Configuration
First, configure your Oncade API credentials using Spring Boot's configuration properties:
oncade:
api:
base-url: ${ONCADE_API_BASE_URL:https://oncade.gg}
server-api-key: ${ONCADE_SERVER_API_KEY}
game-id: ${ONCADE_GAME_ID}
webhook:
secret: ${ONCADE_WEBHOOK_SECRET}Configuration Properties Class
Create a configuration properties class to bind these values:
package com.yourcompany.oncade.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "oncade.api")
public class OncadeProperties {
private String baseUrl = "https://oncade.gg";
private String serverApiKey;
private String gameId;
// Getters and setters
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getServerApiKey() {
return serverApiKey;
}
public void setServerApiKey(String serverApiKey) {
this.serverApiKey = serverApiKey;
}
public String getGameId() {
return gameId;
}
public void setGameId(String gameId) {
this.gameId = gameId;
}
}Spring WebClient Service
Use Spring WebClient (reactive HTTP client) to make API calls. WebClient is thread-safe and supports connection pooling:
package com.yourcompany.oncade.service;
import com.yourcompany.oncade.config.OncadeProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.UUID;
@Service
public class OncadeApiService {
private final WebClient webClient;
private final OncadeProperties properties;
public OncadeApiService(OncadeProperties properties) {
this.properties = properties;
this.webClient = WebClient.builder()
.baseUrl(properties.getBaseUrl())
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader("Authorization", "Bearer " + properties.getServerApiKey())
.defaultHeader("X-Game-Id", properties.getGameId())
.defaultHeader("X-Oncade-API-Version", "v1")
.build();
}
public Mono<LinkInitiateResponse> initiateAccountLink(String email) {
LinkInitiateRequest request = new LinkInitiateRequest(email);
return webClient.post()
.uri("/api/v1/users/link/initiate")
.header("Idempotency-Key", UUID.randomUUID().toString())
.bodyValue(request)
.retrieve()
.bodyToMono(LinkInitiateResponse.class)
.timeout(Duration.ofSeconds(10))
.onErrorMap(WebClientResponseException.class, this::handleError);
}
public Mono<LinkDetailsResponse> getLinkDetails(String sessionKey) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/api/v1/users/link/details")
.queryParam("session", sessionKey)
.build())
.retrieve()
.bodyToMono(LinkDetailsResponse.class)
.timeout(Duration.ofSeconds(10))
.onErrorMap(WebClientResponseException.class, this::handleError);
}
private RuntimeException handleError(WebClientResponseException ex) {
// Log error and return appropriate exception
return new OncadeApiException(
"Oncade API error: " + ex.getMessage(),
ex.getStatusCode().value()
);
}
// Request/Response DTOs
public record LinkInitiateRequest(String email) {}
public record LinkInitiateResponse(String url, String sessionKey) {}
public record LinkDetailsResponse(String gameId, String gameName, String userRef) {}
}Spring Boot REST Controller
Create REST controllers to expose Oncade functionality through your API:
package com.yourcompany.oncade.controller;
import com.yourcompany.oncade.service.OncadeApiService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/oncade")
public class OncadeController {
private final OncadeApiService apiService;
public OncadeController(OncadeApiService apiService) {
this.apiService = apiService;
}
@PostMapping("/users/link/initiate")
public Mono<ResponseEntity<LinkInitiateResponse>> initiateLink(
@RequestBody LinkInitiateRequest request) {
return apiService.initiateAccountLink(request.email())
.map(ResponseEntity::ok)
.onErrorResume(OncadeApiException.class, ex ->
Mono.just(ResponseEntity
.status(ex.getStatusCode())
.build()));
}
@GetMapping("/users/link/details")
public Mono<ResponseEntity<LinkDetailsResponse>> getLinkDetails(
@RequestParam String session) {
return apiService.getLinkDetails(session)
.map(ResponseEntity::ok)
.onErrorResume(OncadeApiException.class, ex ->
Mono.just(ResponseEntity
.status(ex.getStatusCode())
.build()));
}
// DTOs
public record LinkInitiateRequest(String email) {}
public record LinkInitiateResponse(String url, String sessionKey) {}
public record LinkDetailsResponse(String gameId, String gameName, String userRef) {}
}Spring Boot Webhook Controller
Handle webhooks with Spring Boot, including signature verification:
package com.yourcompany.oncade.controller;
import com.yourcompany.oncade.service.WebhookVerificationService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import java.util.Map;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
private final WebhookVerificationService verificationService;
public WebhookController(WebhookVerificationService verificationService) {
this.verificationService = verificationService;
}
@PostMapping("/oncade")
public Mono<ResponseEntity<Map<String, String>>> handleWebhook(
@RequestHeader("x-oncade-signature") String signature,
@RequestBody String rawBody) {
if (!verificationService.verifySignature(rawBody, signature)) {
return Mono.just(ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Invalid signature")));
}
// Parse and process webhook payload
WebhookPayload payload = parsePayload(rawBody);
// Handle different event types
switch (payload.event()) {
case "User.Account.Link.Succeeded":
handleAccountLinkSucceeded(payload.data());
break;
case "Purchases.Started":
handlePurchaseStarted(payload.data());
break;
// Add other event handlers...
}
return Mono.just(ResponseEntity.ok(Map.of("received", "true")));
}
private WebhookPayload parsePayload(String rawBody) {
// Use Jackson ObjectMapper to parse JSON
// Implementation depends on your JSON parsing setup
return null; // Placeholder
}
private void handleAccountLinkSucceeded(Map<String, Object> data) {
String userRef = (String) data.get("userRef");
String sessionKey = (String) data.get("sessionKey");
// Store userRef and sessionKey in your database
}
private void handlePurchaseStarted(Map<String, Object> data) {
String purchaseId = (String) data.get("purchaseId");
// Handle purchase started event
}
public record WebhookPayload(String event, Map<String, Object> data) {}
}Webhook Verification Service
Create a service for webhook signature verification:
package com.yourcompany.oncade.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@Service
public class WebhookVerificationService {
private final String webhookSecret;
public WebhookVerificationService(
@Value(String.fromCharCode(36) + "{oncade.webhook.secret}") String webhookSecret) {
this.webhookSecret = webhookSecret;
}
public boolean verifySignature(String rawBody, String signature) {
if (signature == null || webhookSecret == null) {
return false;
}
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(
webhookSecret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256");
mac.init(secretKeySpec);
byte[] expectedBytes = mac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
String expected = bytesToHex(expectedBytes);
// Use constant-time comparison to prevent timing attacks
return constantTimeEquals(signature, expected);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
return false;
}
}
private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
private boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
}Exception Handling
Create custom exceptions and global exception handlers:
package com.yourcompany.oncade.exception;
public class OncadeApiException extends RuntimeException {
private final int statusCode;
public OncadeApiException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}
public int getStatusCode() {
return statusCode;
}
}Spring Boot Maven Dependencies
Add Spring Boot dependencies to your pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.yourcompany</groupId>
<artifactId>oncade-integration</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web (includes WebClient) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Spring Boot Configuration Processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>3. Jakarta RESTful Web Services (JAX-RS) Integration
For Jakarta EE/Jakarta RESTful Web Services runtimes, you can create REST endpoints that proxy requests to Oncade APIs. This approach is ideal when you need to expose Oncade functionality through your own API layer.
Creating REST Resources
Define JAX-RS resource classes with annotations to map HTTP methods and paths:
package com.oncade.example.rest;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
@Path("/api")
@Produces(MediaType.APPLICATION_JSON)
public class OncadeProxyResource {
private final OncadeHttpClient client;
public OncadeProxyResource() {
this.client = new OncadeHttpClient();
}
@POST
@Path("/v1/users/link/initiate")
@Consumes(MediaType.APPLICATION_JSON)
public Response initiateLink(@Context HttpHeaders headers,
@Context UriInfo uriInfo,
byte[] body) {
return client.forward("POST", "/api/v1/users/link/initiate", headers, uriInfo, () -> body);
}
@GET
@Path("/v1/users/link/details")
public Response getLinkDetails(@Context HttpHeaders headers,
@Context UriInfo uriInfo) {
return client.forward("GET", "/api/v1/users/link/details", headers, uriInfo, null);
}
@GET
@Path("/v1/users/{userId}")
public Response getUser(@PathParam("userId") String userId,
@Context HttpHeaders headers,
@Context UriInfo uriInfo) {
return client.forward("GET", "/api/v1/users/" + userId, headers, uriInfo, null);
}
}HTTP Client Implementation
Create a reusable HTTP client class that forwards requests to Oncade APIs while preserving headers and query parameters:
package com.oncade.example;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
public class OncadeHttpClient {
private static final String DEFAULT_BASE_URL = "https://oncade.gg";
private static final Set<String> EXCLUDED_REQUEST_HEADERS = Set.of("host", "content-length");
private static final Set<String> EXCLUDED_RESPONSE_HEADERS = Set.of("content-length", "transfer-encoding");
private final HttpClient client;
private final String baseUrl;
public OncadeHttpClient() {
this(DEFAULT_BASE_URL, Duration.ofSeconds(5));
}
public OncadeHttpClient(String baseUrl, Duration connectTimeout) {
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
this.client = HttpClient.newBuilder()
.connectTimeout(connectTimeout)
.build();
}
public Response forward(String method,
String upstreamPath,
HttpHeaders incomingHeaders,
UriInfo uriInfo,
Supplier<byte[]> bodySupplier) {
try {
HttpRequest request = buildRequest(method, upstreamPath, incomingHeaders, uriInfo, bodySupplier);
HttpResponse<byte[]> upstream = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
return toResponse(upstream);
} catch (UncheckedIOException e) {
return Response.status(Response.Status.BAD_GATEWAY)
.entity(e.getMessage())
.build();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Request interrupted")
.build();
} catch (Exception e) {
return Response.status(Response.Status.BAD_GATEWAY)
.entity(e.getMessage())
.build();
}
}
private HttpRequest buildRequest(String method,
String upstreamPath,
HttpHeaders incomingHeaders,
UriInfo uriInfo,
Supplier<byte[]> bodySupplier) throws URISyntaxException {
String query = uriInfo != null && uriInfo.getRequestUri() != null
? uriInfo.getRequestUri().getRawQuery()
: null;
StringBuilder target = new StringBuilder(baseUrl);
if (!upstreamPath.startsWith("/")) {
target.append('/');
}
target.append(upstreamPath);
if (query != null && !query.isEmpty()) {
target.append('?').append(query);
}
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(new URI(target.toString()))
.method(method, bodyPublisher(method, bodySupplier));
if (incomingHeaders != null) {
for (Map.Entry<String, List<String>> header : incomingHeaders.getRequestHeaders().entrySet()) {
String headerName = header.getKey();
if (headerName == null || EXCLUDED_REQUEST_HEADERS.contains(headerName.toLowerCase(Locale.ROOT))) {
continue;
}
for (String value : header.getValue()) {
builder.header(headerName, value);
}
}
}
return builder.build();
}
private HttpRequest.BodyPublisher bodyPublisher(String method, Supplier<byte[]> bodySupplier) {
if ("GET".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) {
return HttpRequest.BodyPublishers.noBody();
}
if (bodySupplier == null) {
return HttpRequest.BodyPublishers.noBody();
}
byte[] body = bodySupplier.get();
if (body == null || body.length == 0) {
return HttpRequest.BodyPublishers.noBody();
}
return HttpRequest.BodyPublishers.ofByteArray(body);
}
private Response toResponse(HttpResponse<byte[]> upstream) {
Response.ResponseBuilder builder = Response.status(upstream.statusCode());
byte[] body = upstream.body();
if (body != null && body.length > 0) {
builder.entity(body);
}
upstream.headers().map().forEach((name, values) -> {
if (name == null || EXCLUDED_RESPONSE_HEADERS.contains(name.toLowerCase(Locale.ROOT))) {
return;
}
for (String value : values) {
builder.header(name, value);
}
});
return builder.build();
}
}This implementation handles request forwarding, header preservation, and error handling. It excludes certain headers like Host and Content-Length that should not be forwarded.
4. Webhook Verification
All webhook endpoints must verify the x-oncade-signature header using HMAC-SHA256. This ensures the request originated from Oncade and hasn't been tampered with.
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class WebhookVerifier {
public static boolean verifySignature(String rawBody, String signature, String secret) {
if (signature == null || secret == null) {
return false;
}
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] expectedBytes = mac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
String expected = bytesToHex(expectedBytes);
// Use constant-time comparison to prevent timing attacks
return constantTimeEquals(signature, expected);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
return false;
}
}
private static String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
private static boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
}JAX-RS Webhook Endpoint
Here's a complete example of a webhook endpoint that verifies signatures and handles events:
package com.oncade.example.rest;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.InputStream;
@Path("/webhooks")
public class WebhookResource {
private static final String WEBHOOK_SECRET = System.getenv("ONCADE_WEBHOOK_SECRET");
@POST
@Path("/oncade")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response handleWebhook(@Context HttpHeaders headers, InputStream bodyStream) {
try {
// Read raw body for signature verification
String rawBody = new String(bodyStream.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
String signature = headers.getHeaderString("x-oncade-signature");
// Verify signature
if (!WebhookVerifier.verifySignature(rawBody, signature, WEBHOOK_SECRET)) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity("{\"error\": \"Invalid signature\"}")
.build();
}
// Parse JSON payload
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
java.util.Map<String, Object> payload = mapper.readValue(rawBody, java.util.Map.class);
String event = (String) payload.get("event");
java.util.Map<String, Object> data = (java.util.Map<String, Object>) payload.get("data");
// Handle different event types
switch (event) {
case "User.Account.Link.Succeeded":
handleAccountLinkSucceeded(data);
break;
case "Purchases.Started":
handlePurchaseStarted(data);
break;
// Add other event handlers...
}
return Response.ok("{\"received\": true}").build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("{\"error\": \"Processing failed\"}")
.build();
}
}
private void handleAccountLinkSucceeded(java.util.Map<String, Object> data) {
String userRef = (String) data.get("userRef");
String sessionKey = (String) data.get("sessionKey");
// Store userRef and sessionKey in your database
}
private void handlePurchaseStarted(java.util.Map<String, Object> data) {
String purchaseId = (String) data.get("purchaseId");
// Handle purchase started event
}
}Always verify the signature before processing webhook payloads. Store your webhook secret securely (e.g., in environment variables) and never commit it to version control.
5. Required Headers
All requests to Oncade APIs must include the following headers:
Authorization: Bearer <your_server_api_key>— Your server API key from the DevPortalX-Game-Id: <your_game_id>— Your game identifierX-Oncade-API-Version: v1— API version headerContent-Type: application/json— For POST/PUT requests with JSON bodiesIdempotency-Key: <uuid>— Required for POST/PUT requests to ensure safe retries
6. Error Handling
Oncade APIs return standard HTTP status codes. Always check the response status before processing the body:
HttpResponse<String> response = client.initiateAccountLink(email);
if (response.statusCode() >= 200 && response.statusCode() < 300) {
// Success - parse JSON response
String jsonBody = response.body();
} else if (response.statusCode() == 401) {
// Authentication failed - check API key
} else if (response.statusCode() == 400) {
// Bad request - check request body and headers
} else if (response.statusCode() >= 500) {
// Server error - retry with exponential backoff
}7. Testing Your Integration
Test your integration using the following steps:
- Set environment variables:
ONCADE_SERVER_API_KEY,ONCADE_GAME_ID,ONCADE_API_BASE_URL - Test account linking flow: Create a link session, verify the response, and poll for completion
- Test webhook verification: Send a test webhook with a valid signature and verify it's accepted
- Test error cases: Verify proper handling of invalid API keys, missing headers, and malformed requests
8. Additional Resources
- Account Linking Guide — Detailed walkthrough of the account linking flow
- Webhooks Guide — Complete webhook integration documentation
- API Reference — Complete API endpoint documentation
9. Best Practices
- Security: Never commit API keys or secrets to version control. Use environment variables or secure configuration management.
- Error Handling: Implement retry logic with exponential backoff for transient failures (5xx status codes).
- Idempotency: Always include an
Idempotency-Keyheader for POST/PUT requests to ensure safe retries. - Webhook Verification: Always verify webhook signatures before processing events. Use constant-time comparison to prevent timing attacks.
- Connection Pooling: Reuse HTTP client instances rather than creating new ones for each request. Spring WebClient handles this automatically.
- Timeouts: Set appropriate connection and read timeouts to prevent hanging requests. Use
timeout()on reactive streams or configure timeouts on your HTTP client. - Spring Boot: Use
@ConfigurationPropertiesfor type-safe configuration, dependency injection for testability, and@RestControllerAdvicefor centralized exception handling. - Reactive Programming: When using Spring WebClient, leverage reactive streams (
Mono/Flux) for better resource utilization and non-blocking I/O.