diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java
index c3c8198c1..522562d11 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java
@@ -32,6 +32,9 @@ import org.geysermc.geyser.api.connection.GeyserConnection;
 import org.geysermc.geyser.api.event.connection.ConnectionEvent;
 import org.geysermc.geyser.api.network.RemoteServer;
 
+import java.util.Map;
+import java.util.Objects;
+
 /**
  * Called when a session has logged in, and is about to connect to a remote java server.
  * This event is cancellable, and can be used to prevent the player from connecting to the remote server.
@@ -40,10 +43,16 @@ public final class SessionLoginEvent extends ConnectionEvent implements Cancella
     private RemoteServer remoteServer;
     private boolean cancelled;
     private String disconnectReason;
+    private Map<String, byte[]> cookies;
+    private boolean transferring;
 
-    public SessionLoginEvent(@NonNull GeyserConnection connection, @NonNull RemoteServer remoteServer) {
+    public SessionLoginEvent(@NonNull GeyserConnection connection,
+                             @NonNull RemoteServer remoteServer,
+                             @NonNull Map<String, byte[]> cookies) {
         super(connection);
         this.remoteServer = remoteServer;
+        this.cookies = cookies;
+        this.transferring = false;
     }
 
     /**
@@ -106,4 +115,36 @@ public final class SessionLoginEvent extends ConnectionEvent implements Cancella
     public void remoteServer(@NonNull RemoteServer remoteServer) {
         this.remoteServer = remoteServer;
     }
+
+    /**
+     * Sets a map of cookies from a possible previous session. The Java server can send and request these
+     * to store information on the client across server transfers.
+     */
+    public void cookies(@NonNull Map<String, byte[]> cookies) {
+        Objects.requireNonNull(cookies);
+        this.cookies = cookies;
+    }
+
+    /**
+     * Gets a map of the sessions cookies, if set.
+     * @return the connections cookies
+     */
+    public @NonNull Map<String, byte[]> cookies() {
+        return cookies;
+    }
+
+    /**
+     * Determines the connection intent of the connection
+     */
+    public void transferring(boolean transferring) {
+        this.transferring = transferring;
+    }
+
+    /**
+     * Gets whether this login attempt to the Java server
+     * has the transfer intent
+     */
+    public boolean transferring() {
+        return this.transferring;
+    }
 }
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java
new file mode 100644
index 000000000..afdf596c2
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.api.event.java;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.event.connection.ConnectionEvent;
+
+import java.util.Map;
+
+public class ServerTransferEvent extends ConnectionEvent {
+
+    private final String host;
+    private final int port;
+    private final Map<String, byte[]> cookies;
+
+    public ServerTransferEvent(@NonNull GeyserConnection connection, String host, int port, Map<String, byte[]> cookies) {
+        super(connection);
+        this.host = host;
+        this.port = port;
+        this.cookies = cookies;
+    }
+
+    /**
+     * The host that the Java server requests a transfer to.
+     * @return the host
+     */
+    public String host() {
+        return this.host;
+    }
+
+    /**
+     * The port that the Java server requests a transfer to.
+     * @return the port
+     */
+    public int port() {
+        return this.port;
+    }
+
+    /**
+     * Gets a map of the sessions current cookies.
+     * @return the connections cookies
+     */
+    public @NonNull Map<String, byte[]> cookies() {
+        return cookies;
+    }
+
+}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java
index 43ecf7154..6aa5d563f 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java
@@ -175,8 +175,9 @@ public class GeyserSpigotInjector extends GeyserInjector {
         MinecraftProtocol protocol = new MinecraftProtocol();
         LocalSession session = new LocalSession(bootstrap.getGeyserConfig().getRemote().address(),
                 bootstrap.getGeyserConfig().getRemote().port(), this.serverSocketAddress,
-                InetAddress.getLoopbackAddress().getHostAddress(), protocol, protocol.createHelper());
+                InetAddress.getLoopbackAddress().getHostAddress(), protocol, protocol.createHelper(), false);
         session.connect();
+        session.disconnect("");
     }
 
     @Override
diff --git a/core/src/main/java/org/geysermc/geyser/item/type/WrittenBookItem.java b/core/src/main/java/org/geysermc/geyser/item/type/WrittenBookItem.java
index a11c4f583..4a2028fc7 100644
--- a/core/src/main/java/org/geysermc/geyser/item/type/WrittenBookItem.java
+++ b/core/src/main/java/org/geysermc/geyser/item/type/WrittenBookItem.java
@@ -70,6 +70,5 @@ public class WrittenBookItem extends Item {
         builder.putString("title", bookContent.getTitle().getRaw())
                 .putString("author", bookContent.getAuthor())
                 .putInt("generation", bookContent.getGeneration());
-        // TODO isResolved
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java b/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java
index 19b0b423f..b5598d063 100644
--- a/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java
+++ b/core/src/main/java/org/geysermc/geyser/network/netty/LocalSession.java
@@ -54,11 +54,14 @@ public final class LocalSession extends TcpSession {
     private final String clientIp;
     private final PacketCodecHelper codecHelper;
 
-    public LocalSession(String host, int port, SocketAddress targetAddress, String clientIp, PacketProtocol protocol, MinecraftCodecHelper codecHelper) {
+    private final boolean transferring;
+
+    public LocalSession(String host, int port, SocketAddress targetAddress, String clientIp, PacketProtocol protocol, MinecraftCodecHelper codecHelper, boolean transferring) {
         super(host, port, protocol);
         this.targetAddress = targetAddress;
         this.clientIp = clientIp;
         this.codecHelper = codecHelper;
+        this.transferring = transferring;
     }
 
     @Override
@@ -79,7 +82,7 @@ public final class LocalSession extends TcpSession {
                 public void initChannel(@NonNull LocalChannelWithRemoteAddress channel) {
                     channel.spoofedRemoteAddress(new InetSocketAddress(clientIp, 0));
                     PacketProtocol protocol = getPacketProtocol();
-                    protocol.newClientSession(LocalSession.this, false);
+                    protocol.newClientSession(LocalSession.this, transferring);
 
                     refreshReadTimeoutHandler(channel);
                     refreshWriteTimeoutHandler(channel);
diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
index 175d9eac2..869999357 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -569,6 +569,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
     @Setter
     private @Nullable ItemData currentBook = null;
 
+    /**
+     * Stores cookies sent by the Java server.
+     */
+    @Setter @Getter
+    private Map<String, byte[]> cookies = new Object2ObjectOpenHashMap<>();
+
     private final GeyserCameraData cameraData;
 
     private final GeyserEntityData entityData;
@@ -853,7 +859,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
      * After getting whatever credentials needed, we attempt to join the Java server.
      */
     private void connectDownstream() {
-        SessionLoginEvent loginEvent = new SessionLoginEvent(this, remoteServer);
+        SessionLoginEvent loginEvent = new SessionLoginEvent(this, remoteServer, new Object2ObjectOpenHashMap<>());
         GeyserImpl.getInstance().eventBus().fire(loginEvent);
         if (loginEvent.isCancelled()) {
             String disconnectReason = loginEvent.disconnectReason() == null ?
@@ -862,6 +868,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
             return;
         }
 
+        this.cookies = loginEvent.cookies();
         this.remoteServer = loginEvent.remoteServer();
         boolean floodgate = this.remoteServer.authType() == AuthType.FLOODGATE;
 
@@ -873,7 +880,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
             // We're going to connect through the JVM and not through TCP
             downstream = new LocalSession(this.remoteServer.address(), this.remoteServer.port(),
                     geyser.getBootstrap().getSocketAddress(), upstream.getAddress().getAddress().getHostAddress(),
-                    this.protocol, this.protocol.createHelper());
+                    this.protocol, this.protocol.createHelper(), loginEvent.transferring());
             this.downstream = new DownstreamSession(downstream);
         } else {
             downstream = new TcpClientSession(this.remoteServer.address(), this.remoteServer.port(), this.protocol);
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaCookieRequestTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaCookieRequestTranslator.java
new file mode 100644
index 000000000..06f020423
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaCookieRequestTranslator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.translator.protocol.java.entity.player;
+
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.translator.protocol.PacketTranslator;
+import org.geysermc.mcprotocollib.protocol.packet.common.clientbound.ClientboundCookieRequestPacket;
+import org.geysermc.mcprotocollib.protocol.packet.common.clientbound.ServerboundCookieResponsePacket;
+
+public class JavaCookieRequestTranslator extends PacketTranslator<ClientboundCookieRequestPacket> {
+    @Override
+    public void translate(GeyserSession session, ClientboundCookieRequestPacket packet) {
+        ServerboundCookieResponsePacket responsePacket = new ServerboundCookieResponsePacket(
+                packet.getKey(),
+                session.getCookies().get(packet.getKey())
+        );
+        session.sendDownstreamPacket(responsePacket);
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaStoreCookieTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaStoreCookieTranslator.java
new file mode 100644
index 000000000..499dfda8d
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaStoreCookieTranslator.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.translator.protocol.java.entity.player;
+
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.translator.protocol.PacketTranslator;
+import org.geysermc.mcprotocollib.protocol.packet.common.clientbound.ClientboundStoreCookiePacket;
+
+public class JavaStoreCookieTranslator extends PacketTranslator<ClientboundStoreCookiePacket> {
+    @Override
+    public void translate(GeyserSession session, ClientboundStoreCookiePacket packet) {
+        session.getCookies().put(packet.getKey(), packet.getPayload());
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaTransferPacketTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaTransferPacketTranslator.java
new file mode 100644
index 000000000..1e9bd1537
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaTransferPacketTranslator.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.translator.protocol.java.entity.player;
+
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.event.java.ServerTransferEvent;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.translator.protocol.PacketTranslator;
+import org.geysermc.mcprotocollib.protocol.packet.common.clientbound.ClientboundTransferPacket;
+
+public class JavaTransferPacketTranslator extends PacketTranslator<ClientboundTransferPacket> {
+    @Override
+    public void translate(GeyserSession session, ClientboundTransferPacket packet) {
+        GeyserImpl.getInstance().eventBus().fire(new ServerTransferEvent(
+                session,
+                packet.getHost(),
+                packet.getPort(),
+                session.getCookies()));
+    }
+}