diff --git a/api/net/sf/briar/api/protocol/AuthorId.java b/api/net/sf/briar/api/protocol/AuthorId.java
index 578c89c2accf4374de24d59bfcd7541808d79f24..4f50e2ae22aeab557f30793135726d65e3464ae3 100644
--- a/api/net/sf/briar/api/protocol/AuthorId.java
+++ b/api/net/sf/briar/api/protocol/AuthorId.java
@@ -1,7 +1,10 @@
 package net.sf.briar.api.protocol;
 
+import java.io.IOException;
 import java.util.Arrays;
 
+import net.sf.briar.api.serial.Writer;
+
 /** Type-safe wrapper for a byte array that uniquely identifies an author. */
 public class AuthorId extends UniqueId {
 
@@ -9,6 +12,11 @@ public class AuthorId extends UniqueId {
 		super(id);
 	}
 
+	public void writeTo(Writer w) throws IOException {
+		w.writeUserDefinedTag(Tags.AUTHOR_ID);
+		w.writeRaw(id);
+	}
+
 	@Override
 	public boolean equals(Object o) {
 		if(o instanceof AuthorId)
diff --git a/api/net/sf/briar/api/protocol/BatchId.java b/api/net/sf/briar/api/protocol/BatchId.java
index ab33800761d9558416896dde3862f3c69d83793e..a1640913d6c21e7a052e5eea8d784c6732781dbe 100644
--- a/api/net/sf/briar/api/protocol/BatchId.java
+++ b/api/net/sf/briar/api/protocol/BatchId.java
@@ -1,7 +1,10 @@
 package net.sf.briar.api.protocol;
 
+import java.io.IOException;
 import java.util.Arrays;
 
+import net.sf.briar.api.serial.Writer;
+
 /**
  * Type-safe wrapper for a byte array that uniquely identifies a batch of
  * messages.
@@ -12,6 +15,11 @@ public class BatchId extends UniqueId {
 		super(id);
 	}
 
+	public void writeTo(Writer w) throws IOException {
+		w.writeUserDefinedTag(Tags.BATCH_ID);
+		w.writeRaw(id);
+	}
+
 	@Override
 	public boolean equals(Object o) {
 		if(o instanceof BatchId)
diff --git a/api/net/sf/briar/api/protocol/GroupId.java b/api/net/sf/briar/api/protocol/GroupId.java
index 532941867942b4bb02f4e8a6ed828820c7cbb20f..0cf267d16cfa6913431ecd1cff06e8d83e903a0d 100644
--- a/api/net/sf/briar/api/protocol/GroupId.java
+++ b/api/net/sf/briar/api/protocol/GroupId.java
@@ -1,7 +1,10 @@
 package net.sf.briar.api.protocol;
 
+import java.io.IOException;
 import java.util.Arrays;
 
+import net.sf.briar.api.serial.Writer;
+
 /**
  * Type-safe wrapper for a byte array that uniquely identifies a group to which
  * users may subscribe.
@@ -12,6 +15,11 @@ public class GroupId extends UniqueId {
 		super(id);
 	}
 
+	public void writeTo(Writer w) throws IOException {
+		w.writeUserDefinedTag(Tags.GROUP_ID);
+		w.writeRaw(id);
+	}
+
 	@Override
 	public boolean equals(Object o) {
 		if(o instanceof GroupId)
diff --git a/api/net/sf/briar/api/protocol/MessageId.java b/api/net/sf/briar/api/protocol/MessageId.java
index 64e0fd0369ce2a47a226078bc115e455990dcce6..41cfecc3dbdb793d1dbd9d64be890cbc1be9b4f6 100644
--- a/api/net/sf/briar/api/protocol/MessageId.java
+++ b/api/net/sf/briar/api/protocol/MessageId.java
@@ -1,7 +1,10 @@
 package net.sf.briar.api.protocol;
 
+import java.io.IOException;
 import java.util.Arrays;
 
+import net.sf.briar.api.serial.Writer;
+
 /** Type-safe wrapper for a byte array that uniquely identifies a message. */
 public class MessageId extends UniqueId {
 
@@ -15,6 +18,11 @@ public class MessageId extends UniqueId {
 		super(id);
 	}
 
+	public void writeTo(Writer w) throws IOException {
+		w.writeUserDefinedTag(Tags.MESSAGE_ID);
+		w.writeRaw(id);
+	}
+
 	@Override
 	public boolean equals(Object o) {
 		if(o instanceof MessageId)
diff --git a/api/net/sf/briar/api/protocol/Tags.java b/api/net/sf/briar/api/protocol/Tags.java
new file mode 100644
index 0000000000000000000000000000000000000000..f2767b1ac9a423e4ca951c34a109ac763c569471
--- /dev/null
+++ b/api/net/sf/briar/api/protocol/Tags.java
@@ -0,0 +1,16 @@
+package net.sf.briar.api.protocol;
+
+public interface Tags {
+
+	static final int HEADER = 0;
+	static final int BATCH_ID = 1;
+	static final int GROUP_ID = 2;
+	static final int TIMESTAMP = 3;
+	static final int SIGNATURE = 4;
+	static final int BATCH = 5;
+	static final int MESSAGE = 6;
+	static final int MESSAGE_ID = 7;
+	static final int AUTHOR = 8;
+	static final int MESSAGE_BODY = 9;
+	static final int AUTHOR_ID = 10;
+}
diff --git a/api/net/sf/briar/api/protocol/UniqueId.java b/api/net/sf/briar/api/protocol/UniqueId.java
index 22f57dfe2b80f5a093bc859abc48d5dfe6fca04c..170dc0c5a66166b193b03c81b5c4653a7c3981c9 100644
--- a/api/net/sf/briar/api/protocol/UniqueId.java
+++ b/api/net/sf/briar/api/protocol/UniqueId.java
@@ -3,8 +3,9 @@ package net.sf.briar.api.protocol;
 import java.util.Arrays;
 
 import net.sf.briar.api.serial.Raw;
+import net.sf.briar.api.serial.Writable;
 
-public abstract class UniqueId implements Raw {
+public abstract class UniqueId implements Raw, Writable {
 
 	public static final int LENGTH = 32;
 
diff --git a/api/net/sf/briar/api/serial/Reader.java b/api/net/sf/briar/api/serial/Reader.java
index bcac81bde5540af0d11e55020ccfeb7aa720efee..5e06136cf3669dd170144947f95ffc8e0a0120d3 100644
--- a/api/net/sf/briar/api/serial/Reader.java
+++ b/api/net/sf/briar/api/serial/Reader.java
@@ -59,4 +59,5 @@ public interface Reader {
 
 	boolean hasUserDefinedTag() throws IOException;
 	int readUserDefinedTag() throws IOException;
+	void readUserDefinedTag(int i) throws IOException;
 }
diff --git a/api/net/sf/briar/api/serial/Tag.java b/api/net/sf/briar/api/serial/Tag.java
index 09528efaf0c5ce552f4592cfa6dda10afc7b2f17..e924d6a50683dc13cd8cd7f38d1db27ce54fed2d 100644
--- a/api/net/sf/briar/api/serial/Tag.java
+++ b/api/net/sf/briar/api/serial/Tag.java
@@ -2,30 +2,30 @@ package net.sf.briar.api.serial;
 
 public interface Tag {
 
-	public static final byte FALSE = -1; // 1111 1111
-	public static final byte TRUE = -2; // 1111 1110
-	public static final byte INT8 = -3; // 1111 1101
-	public static final byte INT16 = -4; // 1111 1100
-	public static final byte INT32 = -5; // 1111 1011
-	public static final byte INT64 = -6; // 1111 1010
-	public static final byte FLOAT32 = -7; // 1111 1001
-	public static final byte FLOAT64 = -8; // 1111 1000
-	public static final byte STRING = -9; // 1111 0111
-	public static final byte RAW = -10; // 1111 0110
-	public static final byte LIST = -11; // 1111 0101
-	public static final byte MAP = -12; // 1111 0100
-	public static final byte LIST_START = -13; // 1111 0011
-	public static final byte MAP_START = -14; // 1111 0010
-	public static final byte END = -15; // 1111 0001
-	public static final byte NULL = -16; // 1111 0000
+	static final byte FALSE = -1; // 1111 1111
+	static final byte TRUE = -2; // 1111 1110
+	static final byte INT8 = -3; // 1111 1101
+	static final byte INT16 = -4; // 1111 1100
+	static final byte INT32 = -5; // 1111 1011
+	static final byte INT64 = -6; // 1111 1010
+	static final byte FLOAT32 = -7; // 1111 1001
+	static final byte FLOAT64 = -8; // 1111 1000
+	static final byte STRING = -9; // 1111 0111
+	static final byte RAW = -10; // 1111 0110
+	static final byte LIST = -11; // 1111 0101
+	static final byte MAP = -12; // 1111 0100
+	static final byte LIST_START = -13; // 1111 0011
+	static final byte MAP_START = -14; // 1111 0010
+	static final byte END = -15; // 1111 0001
+	static final byte NULL = -16; // 1111 0000
+	static final byte USER = -17; // 1110 1111
 
-	public static final byte USER = -32; // 1110 0000
+	static final int SHORT_MASK = 0xF0; // Match first four bits
+	static final int SHORT_STRING = 0x80; // 1000 xxxx
+	static final int SHORT_RAW = 0x90; // 1001 xxxx
+	static final int SHORT_LIST = 0xA0; // 1010 xxxx
+	static final int SHORT_MAP = 0xB0; // 1011 xxxx
 
-	public static final int SHORT_MASK = 0xF0; // Match first four bits
-	public static final int SHORT_STRING = 0x80; // 1000 xxxx
-	public static final int SHORT_RAW = 0x90; // 1001 xxxx
-	public static final int SHORT_LIST = 0xA0; // 1010 xxxx
-	public static final int SHORT_MAP = 0xB0; // 1011 xxxx
-	public static final int SHORT_USER_MASK = 0xE0; // Match first three bits
-	public static final int SHORT_USER = 0xC0; // 110x xxxx
+	static final int SHORT_USER_MASK = 0xE0; // Match first three bits
+	static final int SHORT_USER = 0xC0; // 110x xxxx
 }
diff --git a/api/net/sf/briar/api/serial/Writable.java b/api/net/sf/briar/api/serial/Writable.java
new file mode 100644
index 0000000000000000000000000000000000000000..7e75aa54351d4dfbed19c8813c8ac417b83784eb
--- /dev/null
+++ b/api/net/sf/briar/api/serial/Writable.java
@@ -0,0 +1,8 @@
+package net.sf.briar.api.serial;
+
+import java.io.IOException;
+
+public interface Writable {
+
+	void writeTo(Writer w) throws IOException;
+}
diff --git a/components/net/sf/briar/protocol/BundleReaderImpl.java b/components/net/sf/briar/protocol/BundleReaderImpl.java
index 006886728d0e3d873fbd3783680521c151aeb05b..219d131161c1bf29df1594102e9ce53447ca5bb4 100644
--- a/components/net/sf/briar/protocol/BundleReaderImpl.java
+++ b/components/net/sf/briar/protocol/BundleReaderImpl.java
@@ -20,8 +20,10 @@ import net.sf.briar.api.protocol.Header;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageParser;
 import net.sf.briar.api.protocol.UniqueId;
+import net.sf.briar.api.protocol.Tags;
 import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.Raw;
+import net.sf.briar.api.serial.RawByteArray;
 import net.sf.briar.api.serial.Reader;
 
 class BundleReaderImpl implements BundleReader {
@@ -59,23 +61,36 @@ class BundleReaderImpl implements BundleReader {
 		// Read the signed data
 		reader.addConsumer(counting);
 		reader.addConsumer(signing);
+		reader.readUserDefinedTag(Tags.HEADER);
+		// Acks
 		Set<BatchId> acks = new HashSet<BatchId>();
-		for(Raw raw : reader.readList(Raw.class)) {
-			byte[] b = raw.getBytes();
+		reader.readListStart();
+		while(!reader.hasListEnd()) {
+			reader.readUserDefinedTag(Tags.BATCH_ID);
+			byte[] b = reader.readRaw();
 			if(b.length != UniqueId.LENGTH) throw new FormatException();
 			acks.add(new BatchId(b));
 		}
+		reader.readListEnd();
+		// Subs
 		Set<GroupId> subs = new HashSet<GroupId>();
-		for(Raw raw : reader.readList(Raw.class)) {
-			byte[] b = raw.getBytes();
+		reader.readListStart();
+		while(!reader.hasListEnd()) {
+			reader.readUserDefinedTag(Tags.GROUP_ID);
+			byte[] b = reader.readRaw();
 			if(b.length != UniqueId.LENGTH) throw new FormatException();
 			subs.add(new GroupId(b));
 		}
+		reader.readListEnd();
+		// Transports
 		Map<String, String> transports =
 			reader.readMap(String.class, String.class);
+		// Timestamp
+		reader.readUserDefinedTag(Tags.TIMESTAMP);
 		long timestamp = reader.readInt64();
 		reader.removeConsumer(signing);
 		// Read and verify the signature
+		reader.readUserDefinedTag(Tags.SIGNATURE);
 		byte[] sig = reader.readRaw();
 		reader.removeConsumer(counting);
 		if(!signature.verify(sig)) throw new SignatureException();
@@ -106,9 +121,17 @@ class BundleReaderImpl implements BundleReader {
 		reader.addConsumer(counting);
 		reader.addConsumer(digesting);
 		reader.addConsumer(signing);
-		List<Raw> rawMessages = reader.readList(Raw.class);
+		reader.readUserDefinedTag(Tags.BATCH);
+		List<Raw> rawMessages = new ArrayList<Raw>();
+		reader.readListStart();
+		while(!reader.hasListEnd()) {
+			reader.readUserDefinedTag(Tags.MESSAGE);
+			rawMessages.add(new RawByteArray(reader.readRaw()));
+		}
+		reader.readListEnd();
 		reader.removeConsumer(signing);
 		// Read and verify the signature
+		reader.readUserDefinedTag(Tags.SIGNATURE);
 		byte[] sig = reader.readRaw();
 		reader.removeConsumer(digesting);
 		reader.removeConsumer(counting);
diff --git a/components/net/sf/briar/protocol/BundleWriterImpl.java b/components/net/sf/briar/protocol/BundleWriterImpl.java
index cef906ab7662b1e2a8cb09ba192b106146726c7a..6454192636f53d0bdb884a582bd2c71f73b14dec 100644
--- a/components/net/sf/briar/protocol/BundleWriterImpl.java
+++ b/components/net/sf/briar/protocol/BundleWriterImpl.java
@@ -11,6 +11,7 @@ import java.util.Map;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.BundleWriter;
 import net.sf.briar.api.protocol.GroupId;
+import net.sf.briar.api.protocol.Tags;
 import net.sf.briar.api.serial.Raw;
 import net.sf.briar.api.serial.Writer;
 import net.sf.briar.api.serial.WriterFactory;
@@ -20,7 +21,7 @@ class BundleWriterImpl implements BundleWriter {
 	private static enum State { START, FIRST_BATCH, MORE_BATCHES, END };
 
 	private final SigningDigestingOutputStream out;
-	private final Writer w;
+	private final Writer writer;
 	private final PrivateKey privateKey;
 	private final Signature signature;
 	private final MessageDigest messageDigest;
@@ -32,7 +33,7 @@ class BundleWriterImpl implements BundleWriter {
 			MessageDigest messageDigest, long capacity) {
 		this.out =
 			new SigningDigestingOutputStream(out, signature, messageDigest);
-		w = writerFactory.createWriter(this.out);
+		writer = writerFactory.createWriter(this.out);
 		this.privateKey = privateKey;
 		this.signature = signature;
 		this.messageDigest = messageDigest;
@@ -40,7 +41,7 @@ class BundleWriterImpl implements BundleWriter {
 	}
 
 	public long getRemainingCapacity() {
-		return capacity - w.getBytesWritten();
+		return capacity - writer.getBytesWritten();
 	}
 
 	public void addHeader(Iterable<BatchId> acks, Iterable<GroupId> subs,
@@ -51,18 +52,21 @@ class BundleWriterImpl implements BundleWriter {
 		signature.initSign(privateKey);
 		// Write the data to be signed
 		out.setSigning(true);
-		w.writeListStart();
-		for(BatchId ack : acks) w.writeRaw(ack);
-		w.writeListEnd();
-		w.writeListStart();
-		for(GroupId sub : subs) w.writeRaw(sub);
-		w.writeListEnd();
-		w.writeMap(transports);
-		w.writeInt64(System.currentTimeMillis());
+		writer.writeUserDefinedTag(Tags.HEADER);
+		writer.writeListStart();
+		for(BatchId ack : acks) ack.writeTo(writer);
+		writer.writeListEnd();
+		writer.writeListStart();
+		for(GroupId sub : subs) sub.writeTo(writer);
+		writer.writeListEnd();
+		writer.writeMap(transports);
+		writer.writeUserDefinedTag(Tags.TIMESTAMP);
+		writer.writeInt64(System.currentTimeMillis());
 		out.setSigning(false);
 		// Create and write the signature
 		byte[] sig = signature.sign();
-		w.writeRaw(sig);
+		writer.writeUserDefinedTag(Tags.SIGNATURE);
+		writer.writeRaw(sig);
 		// Expect a (possibly empty) list of batches
 		state = State.FIRST_BATCH;
 	}
@@ -70,7 +74,7 @@ class BundleWriterImpl implements BundleWriter {
 	public BatchId addBatch(Iterable<Raw> messages) throws IOException,
 	GeneralSecurityException {
 		if(state == State.FIRST_BATCH) {
-			w.writeListStart();
+			writer.writeListStart();
 			state = State.MORE_BATCHES;
 		}
 		if(state != State.MORE_BATCHES) throw new IllegalStateException();
@@ -80,13 +84,18 @@ class BundleWriterImpl implements BundleWriter {
 		// Write the data to be signed
 		out.setDigesting(true);
 		out.setSigning(true);
-		w.writeListStart();
-		for(Raw message : messages) w.writeRaw(message);
-		w.writeListEnd();
+		writer.writeUserDefinedTag(Tags.BATCH);
+		writer.writeListStart();
+		for(Raw message : messages) {
+			writer.writeUserDefinedTag(Tags.MESSAGE);
+			writer.writeRaw(message);
+		}
+		writer.writeListEnd();
 		out.setSigning(false);
 		// Create and write the signature
 		byte[] sig = signature.sign();
-		w.writeRaw(sig);
+		writer.writeUserDefinedTag(Tags.SIGNATURE);
+		writer.writeRaw(sig);
 		out.setDigesting(false);
 		// Calculate and return the ID
 		return new BatchId(messageDigest.digest());
@@ -94,12 +103,12 @@ class BundleWriterImpl implements BundleWriter {
 
 	public void finish() throws IOException {
 		if(state == State.FIRST_BATCH) {
-			w.writeListStart();
+			writer.writeListStart();
 			state = State.MORE_BATCHES;
 		}
 		if(state != State.MORE_BATCHES) throw new IllegalStateException();
-		w.writeListEnd();
-		w.close();
+		writer.writeListEnd();
+		writer.close();
 		state = State.END;
 	}
 }
diff --git a/components/net/sf/briar/serial/ReaderImpl.java b/components/net/sf/briar/serial/ReaderImpl.java
index 89ca72441dead46f2fe30548c6da3ce4ce83c978..a04112e1e31d83a18f31e00cd6123a10a26e6d00 100644
--- a/components/net/sf/briar/serial/ReaderImpl.java
+++ b/components/net/sf/briar/serial/ReaderImpl.java
@@ -478,4 +478,8 @@ class ReaderImpl implements Reader {
 			return tag;
 		}
 	}
+
+	public void readUserDefinedTag(int i) throws IOException {
+		if(readUserDefinedTag() != i) throw new FormatException();
+	}
 }
diff --git a/test/net/sf/briar/serial/ReaderImplTest.java b/test/net/sf/briar/serial/ReaderImplTest.java
index 0636a85b28bbba5516deefdaee3ee07c38bea0a5..9a1281b800019fc6aeec20d46159cad95195a37c 100644
--- a/test/net/sf/briar/serial/ReaderImplTest.java
+++ b/test/net/sf/briar/serial/ReaderImplTest.java
@@ -318,7 +318,7 @@ public class ReaderImplTest extends TestCase {
 
 	@Test
 	public void testReadUserDefinedTag() throws IOException {
-		setContents("C0" + "DF" + "E0" + "20" + "E0" + "FB7FFFFFFF");
+		setContents("C0" + "DF" + "EF" + "20" + "EF" + "FB7FFFFFFF");
 		assertEquals(0, r.readUserDefinedTag());
 		assertEquals(31, r.readUserDefinedTag());
 		assertEquals(32, r.readUserDefinedTag());
diff --git a/test/net/sf/briar/serial/WriterImplTest.java b/test/net/sf/briar/serial/WriterImplTest.java
index a6f364d07adde13261392fc147fbf448e610fab9..c45983d1232479aba2396ad8c5c16f97028d5086 100644
--- a/test/net/sf/briar/serial/WriterImplTest.java
+++ b/test/net/sf/briar/serial/WriterImplTest.java
@@ -297,7 +297,7 @@ public class WriterImplTest extends TestCase {
 		w.writeUserDefinedTag(32);
 		w.writeUserDefinedTag(Integer.MAX_VALUE);
 		// USER tag, 32 as uint7, USER tag, 2147483647 as int32
-		checkContents("E0" + "20" + "E0" + "FB7FFFFFFF");
+		checkContents("EF" + "20" + "EF" + "FB7FFFFFFF");
 	}
 
 	private void checkContents(String hex) throws IOException {