How to authenticate with the Betfair Exchange API from a server-side Java application using certificate-based non-interactive login, session token management, and automatic renewal.
Before a single market price can be fetched or an order placed, every Betfair API call must carry a valid session token in the X-Authentication header. Getting authentication right — and keeping it right over days or weeks of continuous operation — is more subtle than it looks. Betfair sessions expire after four hours of inactivity, and a trading system that loses its session in the middle of a race is far worse than one that never connected at all.
This post covers the non-interactive (certificate-based) login flow that server-side applications must use, the Java implementation, and the session renewal strategy that keeps a long-running service connected.
Betfair offers two login flows:
https://identitysso.betfair.com/api/login. Suitable for scripts and one-off tools. Not appropriate for production services — hardcoding credentials or prompting at startup defeats the purpose of automation.https://identitysso-cert.betfair.com/api/certlogin over a TLS connection authenticated with a client certificate. The certificate proves identity without requiring the password on every request.Production services must use the certificate path. Betfair also requires your application key (APP_KEY) to be sent on every API request.
Betfair requires a self-signed certificate. Generate one with OpenSSL:
openssl req -x509 -newkey rsa:2048 \
-keyout betfair.key \
-out betfair.crt \
-days 3650 -nodes \
-subj "/C=GB/CN=myapp"
# Combine into a PKCS12 keystore
openssl pkcs12 -export \
-inkey betfair.key \
-in betfair.crt \
-out betfair.p12 \
-passout pass:changeit
Upload betfair.crt to your Betfair account under API Access > Manage Certificates. Store betfair.p12 securely — in production, load it from AWS Secrets Manager, not from the filesystem.
The login endpoint requires mutual TLS — your client certificate authenticates you. Build an SSLContext from the keystore:
public class BetfairSslContextFactory {
public static SSLContext create(InputStream keystoreStream, String keystorePassword) {
try {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(keystoreStream, keystorePassword.toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, keystorePassword.toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, null);
return sslContext;
} catch (Exception e) {
throw new IllegalStateException("Failed to create Betfair SSL context", e);
}
}
}
The certificate login endpoint accepts application/x-www-form-urlencoded:
@Component
public class BetfairAuthClient {
private static final String LOGIN_URL =
"https://identitysso-cert.betfair.com/api/certlogin";
private final HttpClient httpClient;
private final String username;
private final String password;
public BetfairAuthClient(SSLContext sslContext,
@Value("${betfair.username}") String username,
@Value("${betfair.password}") String password) {
this.httpClient = HttpClient.newBuilder()
.sslContext(sslContext)
.build();
this.username = username;
this.password = password;
}
public String login() {
String body = "username=" + URLEncoder.encode(username, UTF_8)
+ "&password=" + URLEncoder.encode(password, UTF_8);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(LOGIN_URL))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
try {
HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
LoginResponse loginResponse =
objectMapper.readValue(response.body(), LoginResponse.class);
if (!"SUCCESS".equals(loginResponse.loginStatus())) {
throw new BetfairAuthException("Login failed: " + loginResponse.loginStatus());
}
return loginResponse.sessionToken();
} catch (IOException | InterruptedException e) {
throw new BetfairAuthException("Login request failed", e);
}
}
}
record LoginResponse(String sessionToken, String loginStatus) {}
A trading system makes many concurrent API calls. Rather than having each thread manage its own session, centralise the token in a SessionManager that handles renewal:
@Component
public class BetfairSessionManager {
private final BetfairAuthClient authClient;
private final AtomicReference<SessionState> state =
new AtomicReference<>(SessionState.empty());
// Renew the session proactively every 3.5 hours (tokens expire after 4h)
@Scheduled(fixedDelay = 3_600_000 * 3 + 1_800_000)
public void renewSession() {
String token = authClient.login();
state.set(SessionState.of(token, Instant.now()));
log.info("Betfair session renewed");
}
public String getSessionToken() {
SessionState current = state.get();
if (!current.isValid()) {
// Lazy initialisation on first call
renewSession();
current = state.get();
}
return current.token();
}
public boolean hasValidSession() {
return state.get().isValid();
}
record SessionState(String token, Instant issuedAt) {
static SessionState empty() { return new SessionState(null, null); }
static SessionState of(String token, Instant issuedAt) {
return new SessionState(token, issuedAt);
}
boolean isValid() {
return token != null && issuedAt != null
&& issuedAt.isAfter(Instant.now().minus(Duration.ofHours(4)));
}
}
}
The @Scheduled renewal runs every 3.5 hours — comfortably before the four-hour expiry. The AtomicReference swap is lock-free; threads reading the old token while renewal completes will get one more use of a still-valid token.
Add the token and app key to every outbound request:
public HttpRequest buildApiRequest(String endpoint, String jsonBody) {
return HttpRequest.newBuilder()
.uri(URI.create("https://api.betfair.com/exchange/betting/rest/v1.0/" + endpoint))
.header("X-Authentication", sessionManager.getSessionToken())
.header("X-Application", appKey)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
}
Even with proactive renewal, network partitions or Betfair-side invalidation can leave you holding a stale token. Detect the error and re-authenticate:
public <T> T executeWithRetry(Supplier<T> call) {
try {
return call.get();
} catch (BetfairApiException e) {
if ("INVALID_SESSION_TOKEN".equals(e.getErrorCode())) {
log.warn("Session token invalid — renewing and retrying once");
sessionManager.renewSession();
return call.get();
}
throw e;
}
}
Retry once after forced renewal. If the second attempt also fails, propagate — you have a deeper problem than a stale token.
Spring @Scheduled does not fire at context startup — the first scheduled execution is after the initial delay. Call renewSession() explicitly from a @PostConstruct method or ApplicationReadyEvent listener to ensure the session is established before any traffic reaches the service.
With solid session management in place, the rest of the Betfair API integration — market cataloguing, streaming subscriptions, order placement — can trust that a valid token is always available. The alternative is scattering authentication retry logic throughout your codebase.
If you’re building a Betfair trading system in Java and want help with the authentication infrastructure, get in touch.