diff --git a/build-data/paper.at b/build-data/paper.at
index cf91314d93..8e27f71577 100644
--- a/build-data/paper.at
+++ b/build-data/paper.at
@@ -628,6 +628,11 @@ public net.minecraft.world.level.block.entity.trialspawner.TrialSpawner stateAcc
 public net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData currentMobs
 public net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData detectedPlayers
 public net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData nextSpawnData
+public net.minecraft.world.level.block.entity.vault.VaultBlockEntity serverData
+public net.minecraft.world.level.block.entity.vault.VaultServerData getRewardedPlayers()Ljava/util/Set;
+public net.minecraft.world.level.block.entity.vault.VaultServerData pauseStateUpdatingUntil(J)V
+public net.minecraft.world.level.block.entity.vault.VaultServerData stateUpdatingResumesAt()J
+public net.minecraft.world.level.block.entity.vault.VaultSharedData getConnectedPlayers()Ljava/util/Set;
 public net.minecraft.world.level.block.state.BlockBehaviour getMenuProvider(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/MenuProvider;
 public net.minecraft.world.level.block.state.BlockBehaviour hasCollision
 public net.minecraft.world.level.block.state.BlockBehaviour$BlockStateBase destroySpeed
diff --git a/paper-api/src/main/java/org/bukkit/block/Vault.java b/paper-api/src/main/java/org/bukkit/block/Vault.java
index 64c7b432c3..6963f29693 100644
--- a/paper-api/src/main/java/org/bukkit/block/Vault.java
+++ b/paper-api/src/main/java/org/bukkit/block/Vault.java
@@ -1,7 +1,184 @@
 package org.bukkit.block;
 
+import org.bukkit.World;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.loot.LootTable;
+import org.jetbrains.annotations.Unmodifiable;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import java.util.Collection;
+import java.util.Set;
+import java.util.UUID;
+
 /**
- * Represents a captured state of a trial spawner.
+ * Represents a captured state of a vault.
  */
+@NullMarked
 public interface Vault extends TileState {
+    /**
+     * Gets the range in blocks at which this vault will become active when a player is near.
+     *
+     * @return This vault's activation range.
+     */
+    double getActivationRange();
+
+    /**
+     * Sets the range in blocks at which the vault will become active when a player is near.
+     *
+     * @param activationRange The new activation range.
+     * @throws IllegalArgumentException if the new range is not a number, or if the new range is more than {@link #getDeactivationRange()}.
+     */
+    void setActivationRange(double activationRange);
+
+    /**
+     * Gets the range in blocks at which this vault will become inactive when a player is not near.
+     *
+     * @return This vault's deactivation range.
+     */
+    double getDeactivationRange();
+
+    /**
+     * Sets the range in blocks at which this vault will become inactive when a player is not near.
+     *
+     * @param deactivationRange The new deactivation range
+     * @throws IllegalArgumentException if the new range is not a number, or if the new range is less than {@link #getActivationRange()}.
+     */
+    void setDeactivationRange(double deactivationRange);
+
+    /**
+     * Gets the {@link ItemStack} that players must use to unlock this vault.
+     *
+     * @return The item that players must use to unlock this vault.
+     */
+    ItemStack getKeyItem();
+
+    /**
+     * Sets the {@link ItemStack} that players must use to unlock this vault.
+     *
+     * @param key The key item.
+     */
+    void setKeyItem(ItemStack key);
+
+    /**
+     * Gets the {@link LootTable} that this vault will select rewards from.
+     *
+     * @return The loot table.
+     */
+    LootTable getLootTable();
+
+    /**
+     * Sets the {@link LootTable} that this vault will select rewards from.
+     *
+     * @param lootTable The new loot table.
+     */
+    void setLootTable(LootTable lootTable);
+
+    /**
+     * Gets the loot table that this vault will display items from.
+     * <p>
+     * Falls back to the regular {@link #getLootTable() loot table} if unset.
+     *
+     * @return The {@link LootTable} that will be used to display items.
+     */
+    @Nullable
+    LootTable getDisplayedLootTable();
+
+    /**
+     * Sets the loot table that this vault will display items from.
+     *
+     * @param lootTable The new loot table to display, or {@code null} to clear this display override.
+     */
+    void setDisplayedLootTable(@Nullable LootTable lootTable);
+
+    /**
+     * Gets the next time (in {@link World#getGameTime() game time}) that this vault block will be updated/ticked at.
+     *
+     * @return The next time that this vault block will be updated/ticked at.
+     * @see World#getGameTime()
+     */
+    long getNextStateUpdateTime();
+
+    /**
+     * Sets the next time that this vault block will be updated/ticked at, if this value is less than or equals to the current
+     * {@link World#getGameTime()}, then it will be updated in the first possible tick.
+     *
+     * @param nextStateUpdateTime The next time that this vault block will be updated/ticked at.
+     * @see World#getGameTime()
+     */
+    void setNextStateUpdateTime(long nextStateUpdateTime);
+
+    /**
+     * Gets the players who have used a key on this vault and unlocked it.
+     *
+     * @return An unmodifiable collection of player uuids.
+     *
+     * @apiNote Only the most recent 128 player UUIDs will be stored by vault blocks.
+     */
+    @Unmodifiable
+    Collection<UUID> getRewardedPlayers();
+
+    /**
+     * Adds a player as rewarded for this vault.
+     *
+     * @param playerUUID The player's uuid.
+     * @return {@code true} if this player was previously not rewarded, and has been added as a result of this operation.
+     *
+     * @apiNote Only the most recent 128 player UUIDs will be stored by vault blocks. Attempting to add more will result in
+     *      the first player UUID being removed.
+     */
+    boolean addRewardedPlayer(UUID playerUUID);
+
+    /**
+     * Removes a player as rewarded for this vault, allowing them to use a {@link #getKeyItem() key item} again to receive rewards.
+     *
+     * @param playerUUID The player's uuid.
+     * @return {@code true} if this player was previously rewarded, and has been removed as a result of this operation.
+     *
+     * @apiNote Only the most recent 128 player UUIDs will be stored by vault blocks.
+     */
+    boolean removeRewardedPlayer(UUID playerUUID);
+
+    /**
+     * Returns whether a given player has already been rewarded by this vault.
+     *
+     * @param playerUUID The player's uuid.
+     * @return Whether this player was previously rewarded by this vault.
+     */
+    boolean hasRewardedPlayer(UUID playerUUID);
+
+    /**
+     * Gets an unmodifiable set of "connected players"; players who are inside this vault's activation range and who have not received rewards yet.
+     *
+     * @apiNote Vaults will only periodically scan for nearby players, so it may take until the next {@link #getNextStateUpdateTime() update time} for this
+     * collection to be updated upon a player entering its range.
+     *
+     * @return An unmodifiable set of connected player uuids.
+     */
+    @Unmodifiable
+    Set<UUID> getConnectedPlayers();
+
+    /**
+     * Returns whether a given player is currently connected to this vault.
+     *
+     * @param playerUUID the player's uuid
+     * @return {@code true} if this player is currently connected to this vault.
+     *
+     * @see #getConnectedPlayers()
+     */
+    boolean hasConnectedPlayer(UUID playerUUID);
+
+    /**
+     * Gets the item currently being displayed inside this vault. Displayed items will automatically cycle between random items from the {@link #getDisplayedLootTable()}
+     * or {@link #getLootTable()} loot tables while this vault is active.
+     *
+     * @return The item currently being displayed inside this vault.
+     */
+    ItemStack getDisplayedItem();
+
+    /**
+     * Sets the item to display inside this vault until the next cycle.
+     *
+     * @param displayedItem The item to display
+     */
+    void setDisplayedItem(ItemStack displayedItem);
 }
diff --git a/paper-server/patches/sources/net/minecraft/world/level/block/entity/vault/VaultServerData.java.patch b/paper-server/patches/sources/net/minecraft/world/level/block/entity/vault/VaultServerData.java.patch
new file mode 100644
index 0000000000..4447be70e6
--- /dev/null
+++ b/paper-server/patches/sources/net/minecraft/world/level/block/entity/vault/VaultServerData.java.patch
@@ -0,0 +1,40 @@
+--- a/net/minecraft/world/level/block/entity/vault/VaultServerData.java
++++ b/net/minecraft/world/level/block/entity/vault/VaultServerData.java
+@@ -66,7 +_,12 @@
+ 
+     @VisibleForTesting
+     public void addToRewardedPlayers(Player player) {
+-        this.rewardedPlayers.add(player.getUUID());
++    // Paper start - Vault API
++        addToRewardedPlayers(player.getUUID());
++    }
++    public boolean addToRewardedPlayers(final java.util.UUID player) {
++        final boolean removed = this.rewardedPlayers.add(player);
++    // Paper end - Vault API
+         if (this.rewardedPlayers.size() > 128) {
+             Iterator<UUID> iterator = this.rewardedPlayers.iterator();
+             if (iterator.hasNext()) {
+@@ -76,6 +_,7 @@
+         }
+ 
+         this.markChanged();
++        return removed; // Paper - Vault API
+     }
+ 
+     public long stateUpdatingResumesAt() {
+@@ -131,4 +_,15 @@
+     public float ejectionProgress() {
+         return this.totalEjectionsNeeded == 1 ? 1.0F : 1.0F - Mth.inverseLerp((float)this.getItemsToEject().size(), 1.0F, (float)this.totalEjectionsNeeded);
+     }
++
++    // Paper start - Vault API
++    public boolean removeFromRewardedPlayers(final UUID uuid) {
++        if (this.rewardedPlayers.remove(uuid)) {
++            this.markChanged();
++            return true;
++        }
++
++        return false;
++    }
++    // Paper end - Vault API
+ }
diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftVault.java b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftVault.java
index bfee498287..a8e455ac98 100644
--- a/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftVault.java
+++ b/paper-server/src/main/java/org/bukkit/craftbukkit/block/CraftVault.java
@@ -1,17 +1,33 @@
 package org.bukkit.craftbukkit.block;
 
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import net.minecraft.resources.ResourceKey;
 import net.minecraft.world.level.block.entity.vault.VaultBlockEntity;
+import net.minecraft.world.level.block.entity.vault.VaultConfig;
 import org.bukkit.Location;
 import org.bukkit.World;
 import org.bukkit.block.Vault;
+import org.bukkit.craftbukkit.CraftLootTable;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.loot.LootTable;
+import org.jetbrains.annotations.Unmodifiable;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
 
+@NullMarked
 public class CraftVault extends CraftBlockEntityState<VaultBlockEntity> implements Vault {
 
     public CraftVault(World world, VaultBlockEntity tileEntity) {
         super(world, tileEntity);
     }
 
-    protected CraftVault(CraftVault state, Location location) {
+    protected CraftVault(CraftVault state, @Nullable Location location) {
         super(state, location);
     }
 
@@ -24,4 +40,124 @@ public class CraftVault extends CraftBlockEntityState<VaultBlockEntity> implemen
     public CraftVault copy(Location location) {
         return new CraftVault(this, location);
     }
+
+    @Override
+    public double getActivationRange() {
+        return this.getSnapshot().getConfig().activationRange();
+    }
+
+    @Override
+    public void setActivationRange(final double activationRange) {
+        Preconditions.checkArgument(Double.isFinite(activationRange), "activation range must not be NaN or infinite");
+        Preconditions.checkArgument(activationRange <= this.getDeactivationRange(), "New activation range (%s) must be less or equal to deactivation range (%s)", activationRange, this.getDeactivationRange());
+
+        final VaultConfig config = this.getSnapshot().getConfig();
+        this.getSnapshot().setConfig(new VaultConfig(config.lootTable(), activationRange, config.deactivationRange(), config.keyItem(), config.overrideLootTableToDisplay()));
+    }
+
+    @Override
+    public double getDeactivationRange() {
+        return this.getSnapshot().getConfig().deactivationRange();
+    }
+
+    @Override
+    public void setDeactivationRange(final double deactivationRange) {
+        Preconditions.checkArgument(Double.isFinite(deactivationRange), "deactivation range must not be NaN or infinite");
+        Preconditions.checkArgument(deactivationRange >= this.getActivationRange(), "New deactivation range (%s) must be more or equal to activation range (%s)", deactivationRange, this.getActivationRange());
+
+        final VaultConfig config = this.getSnapshot().getConfig();
+        this.getSnapshot().setConfig(new VaultConfig(config.lootTable(), config.activationRange(), deactivationRange, config.keyItem(), config.overrideLootTableToDisplay()));
+    }
+
+    @Override
+    public ItemStack getKeyItem() {
+        return this.getSnapshot().getConfig().keyItem().asBukkitCopy();
+    }
+
+    @Override
+    public void setKeyItem(final ItemStack key) {
+        Preconditions.checkArgument(key != null, "key must not be null");
+
+        final VaultConfig config = this.getSnapshot().getConfig();
+        this.getSnapshot().setConfig(new VaultConfig(config.lootTable(), config.activationRange(), config.deactivationRange(), CraftItemStack.asNMSCopy(key), config.overrideLootTableToDisplay()));
+    }
+
+    @Override
+    public LootTable getLootTable() {
+        return CraftLootTable.minecraftToBukkit(this.getSnapshot().getConfig().lootTable());
+    }
+
+    @Override
+    public void setLootTable(final LootTable lootTable) {
+        final ResourceKey<net.minecraft.world.level.storage.loot.LootTable> lootTableKey = CraftLootTable.bukkitToMinecraft(lootTable);
+        Preconditions.checkArgument(lootTableKey != null, "lootTable must not be null");
+
+        final VaultConfig config = this.getSnapshot().getConfig();
+        this.getSnapshot().setConfig(new VaultConfig(lootTableKey, config.activationRange(), config.deactivationRange(), config.keyItem(), config.overrideLootTableToDisplay()));
+    }
+
+    @Override
+    public @Nullable LootTable getDisplayedLootTable() {
+        return this.getSnapshot().getConfig().overrideLootTableToDisplay().map(CraftLootTable::minecraftToBukkit).orElse(null);
+    }
+
+    @Override
+    public void setDisplayedLootTable(final @Nullable LootTable lootTable) {
+        final VaultConfig config = this.getSnapshot().getConfig();
+
+        this.getSnapshot().setConfig(new VaultConfig(config.lootTable(), config.activationRange(), config.deactivationRange(), config.keyItem(), Optional.ofNullable(CraftLootTable.bukkitToMinecraft(lootTable))));
+    }
+
+    @Override
+    public long getNextStateUpdateTime() {
+        return this.getSnapshot().serverData.stateUpdatingResumesAt();
+    }
+
+    @Override
+    public void setNextStateUpdateTime(final long nextStateUpdateTime) {
+        this.getSnapshot().serverData.pauseStateUpdatingUntil(nextStateUpdateTime);
+    }
+
+    @Override
+    public @Unmodifiable Collection<UUID> getRewardedPlayers() {
+        return ImmutableSet.copyOf(this.getSnapshot().serverData.getRewardedPlayers());
+    }
+
+    @Override
+    public boolean addRewardedPlayer(final UUID playerUUID) {
+        Preconditions.checkArgument(playerUUID != null, "playerUUID must not be null");
+        return this.getSnapshot().serverData.addToRewardedPlayers(playerUUID);
+    }
+
+    @Override
+    public boolean removeRewardedPlayer(final UUID playerUUID) {
+        Preconditions.checkArgument(playerUUID != null, "playerUUID must not be null");
+        return this.getSnapshot().serverData.removeFromRewardedPlayers(playerUUID);
+    }
+
+    @Override
+    public boolean hasRewardedPlayer(final UUID playerUUID) {
+        return this.getSnapshot().serverData.getRewardedPlayers().contains(playerUUID);
+    }
+
+    @Override
+    public @Unmodifiable Set<UUID> getConnectedPlayers() {
+        return ImmutableSet.copyOf(this.getSnapshot().getSharedData().getConnectedPlayers());
+    }
+
+    @Override
+    public boolean hasConnectedPlayer(final UUID playerUUID) {
+        return this.getSnapshot().getSharedData().getConnectedPlayers().contains(playerUUID);
+    }
+
+    @Override
+    public ItemStack getDisplayedItem() {
+        return CraftItemStack.asBukkitCopy(this.getSnapshot().getSharedData().getDisplayItem());
+    }
+
+    @Override
+    public void setDisplayedItem(final ItemStack displayedItem) {
+        Preconditions.checkArgument(displayedItem != null, "displayedItem must not be null");
+        this.getSnapshot().getSharedData().setDisplayItem(CraftItemStack.asNMSCopy(displayedItem));
+    }
 }