Hire Me
← All Writing Betfair

Extracting Rich Market Metadata from the Betfair Catalogue API

How to use the Betfair listMarketCatalogue API to extract event metadata, runner details, market rules, and competition context — with practical Java code and field projection optimisation.

Before you can trade a Betfair market you need to understand what you’re looking at. The market ID is not enough. You need the event, the start time, the runners with their names and form numbers, the market type, the rules, the competition. All of this comes from listMarketCatalogue — the Betfair API call that bridges the gap between a raw market ID and everything a trading system needs to reason about it.

listMarketCatalogue is not rate-limited the way listMarketBook is. You can call it more freely, but the response size varies enormously based on which MarketProjection fields you request. Getting projection right — asking for exactly what you need, nothing more — is important both for response latency and for the parsing work downstream.

The API shape

listMarketCatalogue takes a filter (to narrow which markets to return), a list of MarketProjection fields (to control what data is included), and pagination parameters. In Java with a REST client:

public List<MarketCatalogue> listMarketCatalogue(
        MarketFilter filter,
        Set<MarketProjection> projections,
        int maxResults) throws BetfairApiException {

    var request = new ListMarketCatalogueRequest(filter, projections, maxResults);
    var response = betfairClient.post("/betting/rest/v1.0/listMarketCatalogue/", request);
    return response.getResult();
}

The MarketFilter controls which markets come back. The MarketProjection set controls what metadata is included per market. Projections are the most important parameter to get right.

MarketProjection — only ask for what you need

The available projections are:

Projection What it adds
COMPETITION Competition name and ID (league, tournament)
EVENT Event details: name, venue, timezone, open date
EVENT_TYPE Sport type (Horse Racing, Football, etc.)
MARKET_START_TIME Scheduled start time
MARKET_DESCRIPTION Rules, betting type, suspend time
RUNNER_DESCRIPTION Runner names, sortPriority
RUNNER_METADATA Form, jockey, trainer, cloth number

RUNNER_METADATA in particular can significantly increase response size — for a horse racing market it may include form strings, colour descriptions, and official ratings. Only request it when you need it.

For a simple market discovery use case — finding upcoming markets and their start times:

var filter = MarketFilter.builder()
        .eventTypeIds(Set.of("7"))           // Horse Racing
        .marketCountries(Set.of("GB", "IE"))
        .marketTypeCodes(Set.of("WIN"))
        .bspOnly(false)
        .build();

var projections = Set.of(
        MarketProjection.EVENT,
        MarketProjection.MARKET_START_TIME,
        MarketProjection.RUNNER_DESCRIPTION
);

var markets = catalogue.listMarketCatalogue(filter, projections, 100);

This gives you everything needed to build a market schedule: event name, start time, and runner names. No RUNNER_METADATA, no MARKET_DESCRIPTION — keep the response lean.

Extracting runner information

The runner catalogue data is the most frequently needed part for a trading system. Each RunnerCatalog carries the runner’s ID (the selectionId used in all subsequent API calls), its name, and its sort priority:

public record RunnerInfo(
        long selectionId,
        String runnerName,
        int sortPriority,
        String clothNumber,    // from metadata, may be null
        String jockeyName      // from metadata, may be null
) {}

public List<RunnerInfo> extractRunners(MarketCatalogue market) {
    return market.getRunners().stream()
            .sorted(Comparator.comparingInt(RunnerCatalog::getSortPriority))
            .map(r -> new RunnerInfo(
                    r.getSelectionId(),
                    r.getRunnerName(),
                    r.getSortPriority(),
                    extractMetadata(r, "CLOTH_NUMBER"),
                    extractMetadata(r, "JOCKEY_NAME")
            ))
            .toList();
}

private String extractMetadata(RunnerCatalog runner, String key) {
    if (runner.getMetadata() == null) return null;
    return runner.getMetadata().get(key);
}

The selectionId is what you pass to listMarketBook and to order placement. Storing it alongside the runner name at catalogue time means you never need to call the catalogue API during live trading — only at setup.

Market description and rules

MARKET_DESCRIPTION gives you the MarketDescription object, which contains:

For automated trading the two fields that matter most are turnInPlayEnabled and suspendTime:

public boolean isInPlayEligible(MarketCatalogue market) {
    var desc = market.getDescription();
    return desc != null && desc.getTurnInPlayEnabled();
}

public Instant getSuspendTime(MarketCatalogue market) {
    var desc = market.getDescription();
    if (desc == null || desc.getSuspendTime() == null) return null;
    return desc.getSuspendTime().toInstant();
}

Knowing the suspend time lets your trading system start winding down positions before the market goes in-play. Knowing whether a market goes in-play at all determines whether your pre-race position carries any post-race risk.

Building a market index

For a system that tracks multiple markets simultaneously, build an index at startup from the catalogue data:

public class MarketIndex {
    private final Map<String, MarketCatalogue> byMarketId = new ConcurrentHashMap<>();
    private final Map<Long, String> selectionIdToMarket = new ConcurrentHashMap<>();

    public void index(List<MarketCatalogue> markets) {
        markets.forEach(market -> {
            byMarketId.put(market.getMarketId(), market);
            market.getRunners().forEach(runner ->
                selectionIdToMarket.put(runner.getSelectionId(), market.getMarketId())
            );
        });
    }

    public Optional<MarketCatalogue> findByMarketId(String marketId) {
        return Optional.ofNullable(byMarketId.get(marketId));
    }

    public Optional<String> findMarketBySelection(long selectionId) {
        return Optional.ofNullable(selectionIdToMarket.get(selectionId));
    }

    public String getRunnerName(String marketId, long selectionId) {
        return findByMarketId(marketId)
                .flatMap(m -> m.getRunners().stream()
                        .filter(r -> r.getSelectionId() == selectionId)
                        .findFirst())
                .map(RunnerCatalog::getRunnerName)
                .orElse("Unknown");
    }
}

This lets any part of your system resolve a selectionId back to a name, or look up market metadata by market ID, without further API calls during live operation.

Refreshing catalogue data

Catalogue data is mostly static for a given market — runner names and market type don’t change. But a few things can change:

Non-runners: A runner removed before the race will appear with RunnerStatus.REMOVED in listMarketBook, but the catalogue will still list it. Your model needs to handle removed runners gracefully rather than treating them as active.

Late market information: For racing markets, some metadata (final jockey, draw) may not be populated until shortly before the scheduled start. If your strategy depends on this data, refresh the catalogue an hour before and again 30 minutes before the start.

Market name corrections: Rare, but markets occasionally have name corrections applied. For display purposes, periodic refreshes are reasonable.

A pragmatic refresh schedule for a racing system: catalogue on discovery, then once more 30 minutes before the scheduled start, then rely on the listMarketBook data for everything that changes live.

ProTips

Cache aggressively: listMarketCatalogue responses are expensive relative to market book updates. Cache everything at index time and only re-call when you know something has changed.

Use locale-aware sorting: sortPriority on runners is the canonical display order for the Betfair UI. Always use it for consistent ordering.

Check metadata nullity: RUNNER_METADATA values are not always populated — even when you request the projection. Fields like CLOTH_NUMBER may be absent for non-racing markets or early in the data lifecycle. Null-check every metadata access.

Event ID is stable: The eventId from the EVENT projection is the parent event (the race meeting, the football fixture). Using it lets you group related markets (WIN, EACH_WAY, FORECAST) under a single event.

If you’re building a Betfair trading framework and want to talk through market data architecture, get in touch.

Share LinkedIn →
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.