Skip to main content
PlaceholderAPI provides a powerful placeholder system used across thousands of plugins. Foundation integrates seamlessly, allowing you to both use existing placeholders and register your own.

Check if loaded

if (HookManager.isPlaceholderAPILoaded()) {
    // PlaceholderAPI is available
}

Replace placeholders

Basic replacement

String message = "Hello %player_name%, you have %vault_eco_balance%!";
message = HookManager.replacePlaceholders(player, message);
Common.tell(player, message);

With offline players

OfflinePlayer offline = Bukkit.getOfflinePlayer(uuid);
String message = "Player: %player_name%";
message = HookManager.replacePlaceholders(offline, message);

Without a player

Some placeholders don’t require a player:
String message = "Server TPS: %server_tps%";
message = HookManager.replacePlaceholders(null, message);

Relational placeholders

Relational placeholders compare two players:
Player player1 = ...
Player player2 = ...

String message = "%rel_player1_name% and %rel_player2_name%";
message = HookManager.replaceRelationPlaceholders(player1, player2, message);

Register custom placeholders

Simple expansion

Use Foundation’s Variables system to register placeholders:
public class MyPlugin extends SimplePlugin {

    @Override
    protected void onPluginStart() {
        // Register custom placeholders
        Variables.addExpansion(new MyExpansion());
    }
}

class MyExpansion extends SimpleExpansion {

    @Override
    protected String onReplace(CommandSender sender, String identifier) {
        if (!(sender instanceof Player))
            return null;
            
        Player player = (Player) sender;
        
        // %myplugin_health%
        if ("health".equals(identifier))
            return String.valueOf(player.getHealth());
            
        // %myplugin_level%
        if ("level".equals(identifier))
            return String.valueOf(player.getLevel());
            
        // %myplugin_world%
        if ("world".equals(identifier))
            return player.getWorld().getName();
            
        return null; // Unknown placeholder
    }
}

With parameters

Handle placeholders with parameters like %myplugin_stat_kills%:
class StatsExpansion extends SimpleExpansion {

    @Override
    protected String onReplace(CommandSender sender, String identifier) {
        if (!(sender instanceof Player))
            return null;
            
        Player player = (Player) sender;
        
        // %myplugin_stat_<type>%
        if (identifier.startsWith("stat_")) {
            String statType = identifier.substring(5); // Remove "stat_"
            
            return String.valueOf(getPlayerStat(player, statType));
        }
        
        return null;
    }
    
    private int getPlayerStat(Player player, String type) {
        // Your stat retrieval logic
        return 0;
    }
}

Legacy method (deprecated)

The addPlaceholder() method is deprecated. Use Variables.addExpansion() instead for better flexibility.
// Old way - still works but not recommended
HookManager.addPlaceholder("health", player -> 
    String.valueOf(player.getHealth())
);

Advanced expansion example

class PlayerDataExpansion extends SimpleExpansion {

    private final PlayerDataCache cache;
    
    public PlayerDataExpansion(PlayerDataCache cache) {
        this.cache = cache;
    }

    @Override
    protected String onReplace(CommandSender sender, String identifier) {
        if (!(sender instanceof Player))
            return null;
            
        Player player = (Player) sender;
        PlayerData data = cache.getData(player);
        
        if (data == null)
            return "No data";
        
        switch (identifier.toLowerCase()) {
            case "kills":
                return String.valueOf(data.getKills());
                
            case "deaths":
                return String.valueOf(data.getDeaths());
                
            case "kdr":
                double kdr = data.getDeaths() > 0 
                    ? (double) data.getKills() / data.getDeaths()
                    : data.getKills();
                return String.format("%.2f", kdr);
                
            case "rank":
                return data.getRank();
                
            case "points":
                return String.valueOf(data.getPoints());
                
            default:
                return null;
        }
    }
}
Register in your main class:
@Override
protected void onPluginStart() {
    Variables.addExpansion(new PlayerDataExpansion(playerCache));
}
Now players can use:
  • %myplugin_kills%
  • %myplugin_deaths%
  • %myplugin_kdr%
  • %myplugin_rank%
  • %myplugin_points%

Bracket placeholders

Foundation supports both % and {} placeholder styles:
// Both formats work
String msg1 = "Hello %player_name%!";
String msg2 = "Hello {player_name}!";

msg1 = HookManager.replacePlaceholders(player, msg1);
msg2 = HookManager.replacePlaceholders(player, msg2);

Implementation details

Foundation automatically injects your plugin’s variables into PlaceholderAPI:
PlaceholderAPIHook.java
PlaceholderAPIHook() {
    injector = new VariablesInjector();
    
    try {
        injector.register();
    } catch (final Throwable throwable) {
        Common.error(throwable, "Failed to inject our variables into PlaceholderAPI!");
    }
}
The replacement process:
PlaceholderAPIHook.java
final String replacePlaceholders(final OfflinePlayer player, String msg) {
    try {
        return this.setPlaceholders(player, msg);
    } catch (final Throwable t) {
        Common.error(t,
            "PlaceholderAPI failed to replace variables!",
            "Player: " + (player == null ? "none" : player.getName()),
            "Message: " + msg,
            "Error: %error");
        
        return msg;
    }
}
Foundation includes a watchdog timer that prevents PlaceholderAPI from hanging your server if a placeholder expansion takes too long to respond (over 1.5 seconds on main thread, 4 seconds on async threads).

Common placeholders

Popular PlaceholderAPI expansions you can use: Player placeholders
  • %player_name% - Player name
  • %player_displayname% - Display name
  • %player_health% - Current health
  • %player_level% - Experience level
  • %player_world% - Current world
Vault placeholders
  • %vault_eco_balance% - Money balance
  • %vault_rank% - Permission group
  • %vault_prefix% - Chat prefix
Server placeholders
  • %server_name% - Server name
  • %server_online% - Online players
  • %server_max_players% - Max players
  • %server_tps% - Server TPS

Best practices

Check for null - Always handle null players in expansions:
if (!(sender instanceof Player))
    return null;
Return null for unknown - Return null for unrecognized placeholders:
if ("health".equals(identifier))
    return String.valueOf(player.getHealth());
    
return null; // Unknown placeholder
Use lowercase - Compare identifiers in lowercase:
switch (identifier.toLowerCase()) {
    case "kills":
        return String.valueOf(getKills(player));
}
Format numbers - Format decimal numbers for readability:
return String.format("%.2f", kdr); // 2 decimal places

Complete example

Here’s a full plugin with PlaceholderAPI integration:
public class StatsPlugin extends SimplePlugin {

    private PlayerStatsCache statsCache;

    @Override
    protected void onPluginStart() {
        statsCache = new PlayerStatsCache();
        
        // Register placeholders
        Variables.addExpansion(new StatsExpansion());
    }
    
    class StatsExpansion extends SimpleExpansion {
    
        @Override
        protected String onReplace(CommandSender sender, String identifier) {
            if (!(sender instanceof Player))
                return null;
                
            Player player = (Player) sender;
            PlayerStats stats = statsCache.getStats(player);
            
            if (identifier.equals("total_kills"))
                return String.valueOf(stats.getKills());
                
            if (identifier.equals("total_deaths"))
                return String.valueOf(stats.getDeaths());
                
            return null;
        }
    }
}
Usage in messages:
messages:
  stats: "Kills: %statsplugin_total_kills% | Deaths: %statsplugin_total_deaths%"

Build docs developers (and LLMs) love