diff --git a/README.md b/README.md
index a322234acd7f1470c369a0d10714dc80ddf45142..87ac6d17986684565ddae99f0406643f6f2b4545 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,19 @@ WebAssembly bindings for xxDK.
 $ GOOS=js GOARCH=wasm go build -o xxdk.wasm
 ```
 
+### Running Tests
+
+To run unit tests, you need to first install
+[wasmbrowsertest](https://github.com/agnivade/wasmbrowsertest).
+
+`wasm/wasm_js.s` contains commands only recognized by the Go WebAssembly
+compiler and thus cause compile errors when running tests. It needs to be
+excluded when running tests.
+
+```shell
+$ GOOS=js GOARCH=wasm go test
+```
+
 ## Testing
 
 The `test` directory contains a website and server to run the compiled
diff --git a/main.go b/main.go
index a7edc16bac3ffa8f5ad22a88731672de45a80173..9e5d8932e68f54b251dc1955cc98168e3e18a7bc 100644
--- a/main.go
+++ b/main.go
@@ -89,8 +89,10 @@ func main() {
 		js.FuncOf(wasm.UpdateCommonErrors))
 
 	// bindings/fileTransfer.go
-	js.Global().Set("InitFileTransfer",
-		js.FuncOf(wasm.InitFileTransfer))
+	js.Global().Set("InitFileTransfer", js.FuncOf(wasm.InitFileTransfer))
+
+	// bindings/group.go
+	js.Global().Set("NewGroupChat", js.FuncOf(wasm.NewGroupChat))
 
 	<-make(chan bool)
 	os.Exit(0)
diff --git a/wasm/group.go b/wasm/group.go
new file mode 100644
index 0000000000000000000000000000000000000000..d75c10a5624a4b9235930b9564be5d602e16f220
--- /dev/null
+++ b/wasm/group.go
@@ -0,0 +1,364 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package wasm
+
+import (
+	"gitlab.com/elixxir/client/bindings"
+	"syscall/js"
+)
+
+////////////////////////////////////////////////////////////////////////////////
+// Group Chat                                                                 //
+////////////////////////////////////////////////////////////////////////////////
+
+// GroupChat wraps the [bindings.GroupChat] object so its methods can be wrapped
+// to be Javascript compatible.
+type GroupChat struct {
+	api *bindings.GroupChat
+}
+
+// newGroupChatJS creates a new Javascript compatible object
+// (map[string]interface{}) that matches the GroupChat structure.
+func newGroupChatJS(api *bindings.GroupChat) map[string]interface{} {
+	gc := GroupChat{api}
+	gcMap := map[string]interface{}{
+		"MakeGroup":     js.FuncOf(gc.MakeGroup),
+		"ResendRequest": js.FuncOf(gc.ResendRequest),
+		"JoinGroup":     js.FuncOf(gc.JoinGroup),
+		"Send":          js.FuncOf(gc.Send),
+		"GetGroups":     js.FuncOf(gc.GetGroups),
+		"GetGroup":      js.FuncOf(gc.GetGroup),
+		"NumGroups":     js.FuncOf(gc.NumGroups),
+	}
+
+	return gcMap
+}
+
+// NewGroupChat creates a bindings-layer group chat manager.
+//
+// Parameters:
+//  - args[0] - ID of E2e object in tracker (int).
+//  - args[1] - Javascript object that has functions that implement the
+//    [bindings.GroupRequest] interface.
+//  - args[2] - Javascript object that has functions that implement the
+//    [bindings.GroupChatProcessor] interface.
+//
+// Returns:
+//  - Javascript representation of the GroupChat object.
+//  - Throws a TypeError if creating the GroupChat fails.
+func NewGroupChat(_ js.Value, args []js.Value) interface{} {
+	requestFunc := &groupRequest{args[1].Get("Callback").Invoke}
+	p := &groupChatProcessor{
+		args[2].Get("Process").Invoke, args[2].Get("String").Invoke}
+
+	api, err := bindings.NewGroupChat(args[0].Int(), requestFunc, p)
+	if err != nil {
+		Throw(TypeError, err.Error())
+		return nil
+	}
+
+	return newGroupChatJS(api)
+}
+
+// MakeGroup creates a new Group and sends a group request to all members in the
+// group.
+//
+// Parameters:
+//  - args[0] - JSON of array of [id.ID]; it contains the IDs of members the
+//    user wants to add to the group (Uint8Array).
+//  - args[1] - the initial message sent to all members in the group. This is an
+//    optional parameter and may be nil (Uint8Array).
+//  - args[2] - the name of the group decided by the creator. This is an
+//    optional  parameter and may be nil. If nil the group will be assigned the
+//    default name (Uint8Array).
+//
+// Returns:
+//  - JSON of [bindings.GroupReport], which can be passed into
+//    Cmix.WaitForRoundResult to see if the group request message send
+//    succeeded.
+//  - Throws a TypeError if making the group fails.
+func (g *GroupChat) MakeGroup(_ js.Value, args []js.Value) interface{} {
+	// (membershipBytes, message, name []byte) ([]byte, error)
+	membershipBytes := CopyBytesToGo(args[0])
+	message := CopyBytesToGo(args[1])
+	name := CopyBytesToGo(args[2])
+
+	report, err := g.api.MakeGroup(membershipBytes, message, name)
+	if err != nil {
+		Throw(TypeError, err.Error())
+		return nil
+	}
+
+	return CopyBytesToJS(report)
+}
+
+// ResendRequest resends a group request to all members in the group.
+//
+// Parameters:
+//  - args[0] - group's ID (Uint8Array). This can be found in the report
+//  returned by GroupChat.MakeGroup.
+//
+// Returns:
+//  - JSON of [bindings.GroupReport] (Uint8Array), which can be passed into
+//    Cmix.WaitForRoundResult to see if the group request message send
+//    succeeded.
+//  - Throws a TypeError if resending the request fails.
+func (g *GroupChat) ResendRequest(_ js.Value, args []js.Value) interface{} {
+	report, err := g.api.ResendRequest(CopyBytesToGo(args[0]))
+	if err != nil {
+		Throw(TypeError, err.Error())
+		return nil
+	}
+
+	return CopyBytesToJS(report)
+}
+
+// JoinGroup allows a user to join a group when a request is received.
+// If an error is returned, handle it properly first; you may then retry later
+// with the same trackedGroupId.
+//
+// Parameters:
+//  - args[0] - ID of the Group object in tracker (int). This is received by
+//    [bindings.GroupRequest.Callback].
+//
+// Returns:
+//  - Throws a TypeError if joining the group fails.
+func (g *GroupChat) JoinGroup(_ js.Value, args []js.Value) interface{} {
+	err := g.api.JoinGroup(args[0].Int())
+	if err != nil {
+		Throw(TypeError, err.Error())
+		return nil
+	}
+
+	return nil
+}
+
+// LeaveGroup deletes a group so a user no longer has access.
+//
+// Parameters:
+//  - args[0] - group's ID (Uint8Array). This can be found in the report
+//    returned by GroupChat.MakeGroup.
+//
+// Returns:
+//  - Throws a TypeError if leaving the group fails.
+func (g *GroupChat) LeaveGroup(_ js.Value, args []js.Value) interface{} {
+	err := g.api.LeaveGroup(CopyBytesToGo(args[0]))
+	if err != nil {
+		Throw(TypeError, err.Error())
+		return nil
+	}
+
+	return nil
+}
+
+// Send is the bindings-level function for sending to a group.
+//
+// Parameters:
+//  - args[0] - group's ID (Uint8Array). This can be found in the report
+//    returned by GroupChat.MakeGroup.
+//  - args[1] - the message that the user wishes to send to the group
+//    (Uint8Array).
+//  - args[2] - the tag associated with the message (string). This tag may be
+//    empty.
+//
+// Returns:
+//  - JSON of [bindings.GroupSendReport] (Uint8Array), which can be passed into
+//    Cmix.WaitForRoundResult to see if the group message send succeeded.
+func (g *GroupChat) Send(_ js.Value, args []js.Value) interface{} {
+	groupId := CopyBytesToGo(args[0])
+	message := CopyBytesToGo(args[1])
+
+	report, err := g.api.Send(groupId, message, args[2].String())
+	if err != nil {
+		Throw(TypeError, err.Error())
+		return nil
+	}
+
+	return CopyBytesToJS(report)
+}
+
+// GetGroups returns a list of group IDs that the user is a member of.
+//
+// Returns:
+//  - JSON of array of [id.ID] representing all group ID's (Uint8Array).
+//  - Throws a TypeError if getting the groups fails.
+func (g *GroupChat) GetGroups(js.Value, []js.Value) interface{} {
+	// () ([]byte, error)
+	groups, err := g.api.GetGroups()
+	if err != nil {
+		Throw(TypeError, err.Error())
+		return nil
+	}
+
+	return CopyBytesToJS(groups)
+}
+
+// GetGroup returns the group with the group ID. If no group exists, then the
+// error "failed to find group" is returned.
+//
+// Parameters:
+//  - args[0] - group's ID (Uint8Array). This can be found in the report
+//    returned by GroupChat.MakeGroup.
+//
+// Returns:
+//  - Javascript representation of the Group object.
+//  - Throws a TypeError if getting the group fails
+func (g *GroupChat) GetGroup(_ js.Value, args []js.Value) interface{} {
+	grp, err := g.api.GetGroup(CopyBytesToGo(args[0]))
+	if err != nil {
+		Throw(TypeError, err.Error())
+		return nil
+	}
+
+	return newGroupJS(grp)
+}
+
+// NumGroups returns the number of groups the user is a part of.
+//
+// Returns:
+//  - int
+func (g *GroupChat) NumGroups(js.Value, []js.Value) interface{} {
+	return g.api.NumGroups()
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Group Structure                                                            //
+////////////////////////////////////////////////////////////////////////////////
+
+// Group wraps the [bindings.Group] object so its methods can be wrapped to be
+// Javascript compatible.
+type Group struct {
+	api *bindings.Group
+}
+
+// newGroupJS creates a new Javascript compatible object
+// (map[string]interface{}) that matches the Group structure.
+func newGroupJS(api *bindings.Group) map[string]interface{} {
+	g := Group{api}
+	gMap := map[string]interface{}{
+		"GetName":        js.FuncOf(g.GetName),
+		"GetID":          js.FuncOf(g.GetID),
+		"GetTrackedID":   js.FuncOf(g.GetTrackedID),
+		"GetInitMessage": js.FuncOf(g.GetInitMessage),
+		"GetCreatedNano": js.FuncOf(g.GetCreatedNano),
+		"GetCreatedMS":   js.FuncOf(g.GetCreatedMS),
+		"GetMembership":  js.FuncOf(g.GetMembership),
+		"Serialize":      js.FuncOf(g.Serialize),
+	}
+
+	return gMap
+}
+
+// GetName returns the name set by the user for the group.
+//
+// Returns:
+//  - Uint8Array
+func (g *Group) GetName(js.Value, []js.Value) interface{} {
+	return CopyBytesToJS(g.api.GetName())
+}
+
+// GetID return the 33-byte unique group ID. This represents the id.ID object.
+//
+// Returns:
+//  - Uint8Array
+func (g *Group) GetID(js.Value, []js.Value) interface{} {
+	return CopyBytesToJS(g.api.GetID())
+}
+
+// GetTrackedID returns the tracked ID of the Group object. This is used by the
+// backend tracker.
+//
+// Returns:
+//  - int
+func (g *Group) GetTrackedID(js.Value, []js.Value) interface{} {
+	return g.api.GetTrackedID()
+}
+
+// GetInitMessage returns initial message sent with the group request.
+//
+// Returns:
+//  - Uint8Array
+func (g *Group) GetInitMessage(js.Value, []js.Value) interface{} {
+	return CopyBytesToJS(g.api.GetInitMessage())
+}
+
+// GetCreatedNano returns the time the group was created in nanoseconds. This is
+// also the time the group requests were sent.
+//
+// Returns:
+//  - int
+func (g *Group) GetCreatedNano(js.Value, []js.Value) interface{} {
+	return g.api.GetCreatedNano()
+}
+
+// GetCreatedMS returns the time the group was created in milliseconds. This is
+// also the time the group requests were sent.
+//
+// Returns:
+//  - int
+func (g *Group) GetCreatedMS(js.Value, []js.Value) interface{} {
+	return g.api.GetCreatedMS()
+}
+
+// GetMembership retrieves a list of group members. The list is in order;
+// the first contact is the leader/creator of the group.
+// All subsequent members are ordered by their ID.
+//
+// Returns:
+//  - JSON of [group.Membership] (Uint8Array).
+//  - Throws a TypeError if marshalling fails.
+func (g *Group) GetMembership(js.Value, []js.Value) interface{} {
+	membership, err := g.api.GetMembership()
+	if err != nil {
+		Throw(TypeError, err.Error())
+		return nil
+	}
+
+	return CopyBytesToJS(membership)
+}
+
+// Serialize serializes the Group.
+//
+// Returns:
+//  - Byte representation of the Group (Uint8Array).
+func (g *Group) Serialize(js.Value, []js.Value) interface{} {
+	return CopyBytesToJS(g.api.Serialize())
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Callbacks                                                                  //
+////////////////////////////////////////////////////////////////////////////////
+
+// groupRequest wraps Javascript callbacks to adhere to the
+// [bindings.GroupRequest] interface.
+type groupRequest struct {
+	callback func(args ...interface{}) js.Value
+}
+
+func (gr *groupRequest) Callback(g *bindings.Group) {
+	gr.callback(newGroupJS(g))
+}
+
+// groupChatProcessor wraps Javascript callbacks to adhere to the
+// [bindings.GroupChatProcessor] interface.
+type groupChatProcessor struct {
+	callback func(args ...interface{}) js.Value
+	string   func(args ...interface{}) js.Value
+}
+
+func (gcp *groupChatProcessor) Process(decryptedMessage, msg,
+	receptionId []byte, ephemeralId, roundId int64, err error) {
+	gcp.callback(CopyBytesToJS(decryptedMessage), CopyBytesToJS(msg),
+		CopyBytesToJS(receptionId), ephemeralId, roundId, err.Error())
+}
+
+func (gcp *groupChatProcessor) String() string {
+	return gcp.string().String()
+}
diff --git a/wasm/group_test.go b/wasm/group_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..fbd4b8879f77e335c950f43c2d574a1af5f50a63
--- /dev/null
+++ b/wasm/group_test.go
@@ -0,0 +1,56 @@
+////////////////////////////////////////////////////////////////////////////////
+// Copyright © 2020 xx network SEZC                                           //
+//                                                                            //
+// Use of this source code is governed by a license that can be found in the  //
+// LICENSE file                                                               //
+////////////////////////////////////////////////////////////////////////////////
+
+//go:build js && wasm
+
+package wasm
+
+import (
+	"gitlab.com/elixxir/client/bindings"
+	"reflect"
+	"testing"
+)
+
+// Tests that the map representing GroupChat returned by newGroupChatJS contains
+// all of the methods on GroupChat.
+func Test_newGroupChatJS(t *testing.T) {
+	gcType := reflect.TypeOf(&GroupChat{})
+
+	gc := newGroupChatJS(&bindings.GroupChat{})
+	if len(gc) != gcType.NumMethod() {
+		t.Errorf("GroupChat JS object does not have all methods."+
+			"\nexpected: %d\nreceived: %d", gcType.NumMethod(), len(gc))
+	}
+
+	for i := 0; i < gcType.NumMethod(); i++ {
+		method := gcType.Method(i)
+
+		if _, exists := gc[method.Name]; !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}
+
+// Tests that the map representing Group returned by newGroupJS contains all of
+// the methods on Group.
+func Test_newGroupJS(t *testing.T) {
+	gType := reflect.TypeOf(&Group{})
+
+	g := newGroupJS(&bindings.Group{})
+	if len(g) != gType.NumMethod() {
+		t.Errorf("Group JS object does not have all methods."+
+			"\nexpected: %d\nreceived: %d", gType.NumMethod(), len(g))
+	}
+
+	for i := 0; i < gType.NumMethod(); i++ {
+		method := gType.Method(i)
+
+		if _, exists := g[method.Name]; !exists {
+			t.Errorf("Method %s does not exist.", method.Name)
+		}
+	}
+}