Hire Me
← All Writing Betfair

API Authentication and Non-Interactive Login from Java

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.

Why non-interactive login

Betfair offers two login flows:

Production services must use the certificate path. Betfair also requires your application key (APP_KEY) to be sent on every API request.

Generating and registering the certificate

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.

Building the SSL context

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 login request

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) {}

Thread-safe session management

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.

Using the session token in API calls

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();
}

Handling INVALID_SESSION_TOKEN errors

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.

What happens at startup

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.

Samuel Jackson

Samuel Jackson

Senior Java Back End Developer & Contractor

Senior Java Back End Developer — Betfair Exchange API specialist, Spring Boot, AWS, and event-driven architecture. 20+ years delivering high-performance systems across betting, finance, energy, retail, and government. Available for Java contracting.