How Betfair's non-interactive certificate login works, how to implement it in Java, and how to manage the session token lifecycle correctly in an automated system.
Betfair’s API has two authentication paths: interactive login (username, password, two-factor code via the browser-based SSO) and non-interactive login (certificate-based, for automated systems). If you’re building a trading bot or any system that runs unattended, you need the non-interactive path. The certificate login mechanism is well-documented but the implementation details — particularly around session token lifecycle management in a running Java application — have enough edge cases to trip you up.
Instead of username and password, you authenticate with:
.crt file and a private key .pem file, combined into a .p12 or loaded into a KeyStore).Betfair’s identity endpoint (https://identitysso-cert.betfair.com/api/certlogin) performs the mutual TLS handshake using your certificate, validates your credentials, and returns a sessionToken if everything checks out. That token is what you include in every subsequent API call via the X-Authentication header.
Betfair provides the client certificate as a .p12 (PKCS12) file. Load it into a KeyStore and wire it into an SSLContext:
public SSLContext buildSslContext(
String p12Path,
String p12Password) throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (InputStream in = new FileInputStream(p12Path)) {
keyStore.load(in, p12Password.toCharArray());
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, p12Password.toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
return sslContext;
}
Never hardcode p12Path or p12Password in source code. Load them from environment variables or AWS Secrets Manager.
With the SSLContext in hand, make the POST request to Betfair’s identity endpoint:
public String login(
SSLContext sslContext,
String username,
String password,
String appKey) throws Exception {
HttpClient httpClient = HttpClient.newBuilder()
.sslContext(sslContext)
.connectTimeout(Duration.ofSeconds(10))
.build();
String body = "username=" + URLEncoder.encode(username, StandardCharsets.UTF_8)
+ "&password=" + URLEncoder.encode(password, StandardCharsets.UTF_8);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://identitysso-cert.betfair.com/api/certlogin"))
.header("Content-Type", "application/x-www-form-urlencoded")
.header("X-Application", appKey)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new AuthenticationException(
"Login failed with status " + response.statusCode());
}
LoginResponse loginResponse = objectMapper.readValue(
response.body(), LoginResponse.class);
if (!"SUCCESS".equals(loginResponse.loginStatus())) {
throw new AuthenticationException(
"Login rejected: " + loginResponse.loginStatus());
}
return loginResponse.sessionToken();
}
public record LoginResponse(String sessionToken, String loginStatus) {}
The loginStatus field is worth checking explicitly — even a 200 response can indicate failure (e.g. INVALID_USERNAME_OR_PASSWORD, LOGIN_RESTRICTED, CERT_AUTH_REQUIRED).
A session token is valid for four hours. After four hours, the token expires and every API call returns an ANGX-0003 error. You need a strategy for keeping the token current.
The naive approach — re-login on every API call if you see an auth error — works but causes a thundering-herd problem if multiple threads hit the expiry simultaneously. The cleaner approach is proactive refresh:
@Component
public class BetfairSessionManager {
private static final Duration TOKEN_VALIDITY = Duration.ofHours(4);
private static final Duration REFRESH_BEFORE = Duration.ofMinutes(15);
private volatile String sessionToken;
private volatile Instant tokenExpiry;
private final Object refreshLock = new Object();
private final BetfairAuthClient authClient;
public String getSessionToken() {
if (needsRefresh()) {
refreshToken();
}
return sessionToken;
}
private boolean needsRefresh() {
return sessionToken == null
|| Instant.now().isAfter(tokenExpiry.minus(REFRESH_BEFORE));
}
private void refreshToken() {
synchronized (refreshLock) {
if (!needsRefresh()) return; // another thread already refreshed
log.info("Refreshing Betfair session token");
sessionToken = authClient.login();
tokenExpiry = Instant.now().plus(TOKEN_VALIDITY);
log.info("Session token refreshed, expires at {}", tokenExpiry);
}
}
}
The double-checked lock inside refreshToken() ensures that if multiple threads reach the expiry boundary simultaneously, only one performs the login — the rest wait and then use the freshly minted token.
Schedule a keep-alive to proactively refresh every three hours rather than relying solely on the expiry check:
@Scheduled(fixedDelay = 3, timeUnit = TimeUnit.HOURS)
public void keepAlive() {
try {
sessionManager.refreshToken();
} catch (AuthenticationException e) {
log.error("Failed to refresh session token — alerting", e);
alertService.raiseAuthFailure(e);
}
}
Auth failures in a live trading system are critical. The scheduled refresh failing silently would mean the next order placement fails with an auth error. Alert on it.
Every call to the Betfair Exchange API and Sports Betting API requires two headers:
public HttpRequest buildApiRequest(String endpoint, String body) {
return HttpRequest.newBuilder()
.uri(URI.create("https://api.betfair.com/exchange/betting/json-rpc/v1" + endpoint))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.header("X-Application", appKey)
.header("X-Authentication", sessionManager.getSessionToken())
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
}
X-Application is your app key. X-Authentication is the session token. Both are required. A common mistake is using the wrong app key — using the “delayed data” key for live API calls returns market data with a 60-second delay, which is disastrous for in-play trading.
Even with proactive refresh, you can encounter auth errors if the token is revoked externally (Betfair account settings change, concurrent session limit reached, or a network issue during refresh). Handle it at the call site:
public <T> T executeWithRetry(ApiCall<T> call) {
try {
return call.execute(sessionManager.getSessionToken());
} catch (BetfairAuthException e) {
log.warn("Auth error on API call, forcing token refresh");
sessionManager.forceRefresh();
return call.execute(sessionManager.getSessionToken());
}
}
Retry once after a forced refresh. If the second attempt also fails with an auth error, propagate it — there is a genuine account problem that cannot be resolved by retrying.
Your certificate file and credentials must not live in the application classpath or be committed to source control. For AWS-hosted systems, pull them at startup from Secrets Manager:
@Bean
public BetfairCredentials betfairCredentials(SecretsManagerClient secretsManager) {
GetSecretValueResponse response = secretsManager.getSecretValue(
GetSecretValueRequest.builder()
.secretId("betfair/credentials")
.build());
return objectMapper.readValue(response.secretString(), BetfairCredentials.class);
}
Store the .p12 as a base64-encoded string in Secrets Manager, decode it at startup, and write it to a temp file or load it directly into a KeyStore from a ByteArrayInputStream. Never write credential files to a persistent path accessible outside the process.
CERT_AUTH_REQUIRED: you’ve hit the standard login endpoint instead of the cert login endpoint. Check the URL.INVALID_USERNAME_OR_PASSWORD: the credentials are wrong, or the account is locked after too many failed attempts. Check your Betfair account directly.SSLHandshakeException: the certificate is not trusted, the wrong keystore type was used, or the p12 password is wrong. Log the exception chain — it usually tells you exactly which step failed.ANGX-0003 on API calls: the session token has expired. Your refresh logic has a gap.ACCOUNT_NOW_LOCKED: too many failed logins. Betfair temporarily locks accounts on repeated failures — exponential backoff on auth retries is essential.If you’re building automated trading infrastructure on Betfair and want to get the auth layer right before anything else, get in touch.