diff --git a/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/api/entity/event/v1/ServerPlayerEvents.java b/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/api/entity/event/v1/ServerPlayerEvents.java
index 82e16b5eb..78ef61420 100644
--- a/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/api/entity/event/v1/ServerPlayerEvents.java
+++ b/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/api/entity/event/v1/ServerPlayerEvents.java
@@ -16,6 +16,8 @@
 
 package net.fabricmc.fabric.api.entity.event.v1;
 
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.damage.DamageSource;
 import net.minecraft.server.network.ServerPlayerEntity;
 
 import net.fabricmc.fabric.api.event.Event;
@@ -45,6 +47,31 @@ public final class ServerPlayerEvents {
 		}
 	});
 
+	/**
+	 * An event that is called when a player takes fatal damage.
+	 *
+	 * <p>Mods can cancel this to keep the player alive.
+	 *
+	 * <p>Vanilla checks for player health {@code <= 0} each tick (with {@link LivingEntity#isDead()}), and kills if true -
+	 * so the player will still die next tick if this event is cancelled. It's assumed that the listener will do
+	 * something to prevent this, for example:
+	 *
+	 * <ul>
+	 *     <li>a minigame mod teleporting the player into a 'respawn room' and setting their health to 20.0</li>
+	 *     <li>a mod that changes death mechanics switching the player over to the mod's play-mode, where death doesn't
+	 *     apply</li>
+	 * </ul>
+	 */
+	public static final Event<AllowDeath> ALLOW_DEATH = EventFactory.createArrayBacked(AllowDeath.class, callbacks -> (player, damageSource, damageAmount) -> {
+		for (AllowDeath callback : callbacks) {
+			if (!callback.allowDeath(player, damageSource, damageAmount)) {
+				return false;
+			}
+		}
+
+		return true;
+	});
+
 	@FunctionalInterface
 	public interface CopyFrom {
 		/**
@@ -69,6 +96,19 @@ public final class ServerPlayerEvents {
 		void afterRespawn(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer, boolean alive);
 	}
 
+	@FunctionalInterface
+	public interface AllowDeath {
+		/**
+		 * Called when a player takes fatal damage (before totems of undying can take effect).
+		 *
+		 * @param player the player
+		 * @param damageSource the fatal damage damageSource
+		 * @param damageAmount the damageAmount of damage that has killed the player
+		 * @return true if the death should go ahead, false otherwise.
+		 */
+		boolean allowDeath(ServerPlayerEntity player, DamageSource damageSource, float damageAmount);
+	}
+
 	private ServerPlayerEvents() {
 	}
 }
diff --git a/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/mixin/entity/event/LivingEntityMixin.java b/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/mixin/entity/event/LivingEntityMixin.java
index 27fc8c68a..ec1acc9c8 100644
--- a/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/mixin/entity/event/LivingEntityMixin.java
+++ b/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/mixin/entity/event/LivingEntityMixin.java
@@ -17,24 +17,40 @@
 package net.fabricmc.fabric.mixin.entity.event;
 
 import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.Redirect;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
 
 import net.minecraft.entity.Entity;
 import net.minecraft.entity.LivingEntity;
 import net.minecraft.entity.damage.DamageSource;
+import net.minecraft.server.network.ServerPlayerEntity;
 import net.minecraft.server.world.ServerWorld;
 
 import net.fabricmc.fabric.api.entity.event.v1.ServerEntityCombatEvents;
+import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
 
 @Mixin(LivingEntity.class)
 abstract class LivingEntityMixin extends EntityMixin {
+	@Shadow
+	public abstract boolean isDead();
+
 	@Inject(method = "onDeath", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;onKilledOther(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/entity/LivingEntity;)V", shift = At.Shift.AFTER), locals = LocalCapture.CAPTURE_FAILEXCEPTION)
 	private void onEntityKilledOther(DamageSource source, CallbackInfo ci, Entity attacker) {
 		// FIXME: Cannot use shadowed fields from supermixins - needs a fix so people can use fabric api in a dev environment even though this is fine in this repo and prod.
 		//  A temporary fix is to just cast the mixin to LivingEntity and access the world field with a few ugly casts.
 		ServerEntityCombatEvents.AFTER_KILLED_OTHER_ENTITY.invoker().afterKilledOtherEntity((ServerWorld) ((LivingEntity) (Object) this).world, attacker, (LivingEntity) (Object) this);
 	}
+
+	@Redirect(method = "damage", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;isDead()Z", ordinal = 1))
+	boolean beforePlayerKilled(LivingEntity livingEntity, DamageSource source, float amount) {
+		if (livingEntity instanceof ServerPlayerEntity) {
+			return isDead() && ServerPlayerEvents.ALLOW_DEATH.invoker().allowDeath((ServerPlayerEntity) livingEntity, source, amount);
+		}
+
+		return isDead();
+	}
 }
diff --git a/fabric-entity-events-v1/src/testmod/java/net/fabricmc/fabric/test/entity/event/EntityEventTests.java b/fabric-entity-events-v1/src/testmod/java/net/fabricmc/fabric/test/entity/event/EntityEventTests.java
index a7740581e..c2b976231 100644
--- a/fabric-entity-events-v1/src/testmod/java/net/fabricmc/fabric/test/entity/event/EntityEventTests.java
+++ b/fabric-entity-events-v1/src/testmod/java/net/fabricmc/fabric/test/entity/event/EntityEventTests.java
@@ -19,6 +19,9 @@ package net.fabricmc.fabric.test.entity.event;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import net.minecraft.item.Items;
+import net.minecraft.util.Hand;
+
 import net.fabricmc.api.ModInitializer;
 import net.fabricmc.fabric.api.entity.event.v1.ServerEntityCombatEvents;
 import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents;
@@ -48,5 +51,16 @@ public final class EntityEventTests implements ModInitializer {
 		ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) -> {
 			LOGGER.info("Respawned {}, [{}, {}]", oldPlayer.getGameProfile().getName(), oldPlayer.getServerWorld().getRegistryKey().getValue(), newPlayer.getServerWorld().getRegistryKey().getValue());
 		});
+
+		ServerPlayerEvents.ALLOW_DEATH.register((player, source, amount) -> {
+			LOGGER.info("{} is going to die to {} damage from {} damage source", player.getGameProfile().getName(), amount, source.getName());
+
+			if (player.getStackInHand(Hand.MAIN_HAND).getItem() == Items.APPLE) {
+				player.setHealth(3.0f);
+				return false;
+			}
+
+			return true;
+		});
 	}
 }