Repository: ably/ably-go
Pull request #300 (2021-03-31 18:08:24)

Remove Ping public Interface
- Disabled Ping functionality
- References #263 


Patch:
diff --git a/README.md b/README.md
index b17ac97..6992a4b 100644
--- a/README.md
+++ b/README.md
@@ -243,38 +243,22 @@ if err != nil {
 
 ### Querying the History
 
-<!-- GO EXAMPLE
-{
--->
-
 ```go
-pages, err := channel.History().Pages(ctx)
-if err != nil {
-	panic(err)
-}
-for pages.Next(ctx) {
-	for _, message := range pages.Items() {
+page, err := channel.History(ctx, nil)
+for ; err == nil && page != nil; page, err = page.Next(ctx) {
+	for _, message := range page.Messages() {
 		fmt.Println(message)
 	}
 }
-if err := pages.Err(); err != nil {
+if err != nil {
 	panic(err)
 }
-
 ```
 
-<!-- GO EXAMPLE
-}
--->
-
 ### Presence on a channel
 
-<!-- GO EXAMPLE
-{
--->
-
 ```go
-page, err := channel.Presence.Get(ctx, nil)
+page, err = channel.Presence.Get(ctx, nil)
 for ; err == nil && page != nil; page, err = page.Next(ctx) {
 	for _, presence := range page.PresenceMessages() {
 		fmt.Println(presence)
@@ -285,52 +269,30 @@ if err != nil {
 }
 ```
 
-<!-- GO EXAMPLE
-}
--->
-
 ### Querying the Presence History
 
-<!-- GO EXAMPLE
-{
--->
-
 ```go
-pages, err := channel.Presence.History().Pages(ctx)
-if err != nil {
-	panic(err)
-}
-for pages.Next(ctx) {
-	for _, presence := range pages.Items() {
+page, err = channel.Presence.History(ctx, nil)
+for ; err == nil && page != nil; page, err = page.Next(ctx) {
+	for _, presence := range page.PresenceMessages() {
 		fmt.Println(presence)
 	}
 }
-if err := pages.Err(); err != nil {
+if err != nil {
 	panic(err)
 }
 ```
 
-<!-- GO EXAMPLE
-}
--->
-
 ### Fetching your application's stats
 
-<!-- GO EXAMPLE
-{
--->
-
 ```go
-pages, err := client.Stats().Pages(ctx)
-if err != nil {
-	panic(err)
-}
-for pages.Next(ctx) {
-	for _, stat := range pages.Items() {
+page, err = client.Stats(ctx, &ably.PaginateParams{})
+for ; err == nil && page != nil; page, err = page.Next(ctx) {
+	for _, stat := range page.Stats() {
 		fmt.Println(stat)
 	}
 }
-if err := pages.Err(); err != nil {
+if err != nil {
 	panic(err)
 }
 ```
@@ -339,10 +301,6 @@ if err := pages.Err(); err != nil {
 }
 -->
 
-<!-- GO EXAMPLE
-}
--->
-
 ## Feature support
 
 This library targets the Ably 1.2 [client library specification](https://www.ably.io/documentation/client-lib-development-guide/features).
diff --git a/ably/doc.go b/ably/doc.go
index 1c0c474..e053372 100644
--- a/ably/doc.go
+++ b/ably/doc.go
@@ -33,28 +33,4 @@
 //
 // For messages and presence messages, "on" is called "subscribe" and "off" is
 // called "unsubscribe".
-//
-// Paginated results
-//
-// Most requests to the Ably REST API return a single page of results, with
-// hyperlinks to the first and next pages in the whole collection of results.
-// To facilitate navigating through these pages, the library provides access to
-// such paginated results though a common pattern.
-//
-// A method that prepares a paginated request returns a Request object with two
-// methods: Pages and Items. Pages returns a PaginatedResult, an iterator that,
-// on each iteration, yields a whole page of results. Items is simply a
-// convenience wrapper that yields single results insteads.
-//
-// In both cases, calling the method validates the request and may return an
-// error.
-//
-// Then, for accessing the results, the Next method from the resulting
-// iterator object must be called repeatedly; each time it returns true, the
-// result that has been retrieved can be inspected with the Items or Item method
-// from the iterator object. Finally, once it returns false, the Err method must
-// be called to check if the iterator stopped due to some error, or else, it
-// just finished going through all pages.
-//
-// See the PaginatedResults example.
 package ably
diff --git a/ably/paginated_result.go b/ably/paginated_result.go
index cfbffa3..8d6e406 100644
--- a/ably/paginated_result.go
+++ b/ably/paginated_result.go
@@ -16,164 +16,6 @@ import (
 	"github.com/ably/ably-go/ably/proto"
 )
 
-type Direction string
-
-const (
-	Backwards Direction = "backwards"
-	Forwards  Direction = "forwards"
-)
-
-type paginatedRequestNew struct {
-	path   string
-	params url.Values
-
-	query queryFunc
-}
-
-func (r *REST) newPaginatedRequest(path string, params url.Values) paginatedRequestNew {
-	return paginatedRequestNew{
-		path:   path,
-		params: params,
-
-		query: query(r.get),
-	}
-}
-
-// PaginatedResultNew is a generic iterator for PaginatedResult pagination.
-// Items decoding is delegated to type-specific wrappers.
-//
-// See "Paginated results" section in the package-level documentation.
-type PaginatedResultNew struct {
-	basePath  string
-	nextLink  string
-	firstLink string
-	res       *http.Response
-	err       error
-
-	query queryFunc
-	first bool
-}
-
-// load loads the first page of results. Must be called from the type-specific
-// wrapper Pages method that creates the PaginatedResult object.
-func (p *PaginatedResultNew) load(ctx context.Context, r paginatedRequestNew) error {
-	p.basePath = path.Dir(r.path)
-	p.firstLink = (&url.URL{
-		Path:     r.path,
-		RawQuery: r.params.Encode(),
-	}).String()
-	p.query = r.query
-	return p.First(ctx)
-}
-
-// loadItems loads the first page of results and returns a next function. Must
-// be called from the type-specific wrapper Items method that creates the
-// PaginatedItems object.
-//
-// The returned next function must be called from the wrapper's Next method, and
-// returns the index of the object that should be returned by the Item method,
-// previously loading the next page if necessary.
-//
-// pageDecoder will be called each time a new page is retrieved under the hood.
-// It should return a destination object on which the page of results will be
-// decoded, and a pageLength function that, when called after the page has been
-// decoded, must return the length of the page.
-func (p *PaginatedResultNew) loadItems(
-	ctx context.Context,
-	r paginatedRequestNew,
-	pageDecoder func() (page interface{}, pageLength func() int),
-) (
-	next func(context.Context) (int, bool),
-	err error,
-) {
-	err = p.load(ctx, r)
-	if err != nil {
-		return nil, err
-	}
-
-	var page interface{}
-	var pageLen int
-	nextItem := 0
-
-	return func(ctx context.Context) (int, bool) {
-		if nextItem == 0 {
-			var getLen func() int
-			page, getLen = pageDecoder()
-			hasNext := p.next(ctx, &page)
-			if !hasNext {
-				return 0, false
-			}
-			pageLen = getLen()
-			if pageLen == 0 { // compatible with hasNext if first page is empty
-				return 0, false
-			}
-		}
-
-		idx := nextItem
-		nextItem = (nextItem + 1) % pageLen
-		return idx, true
-	}, nil
-}
-
-func (p *PaginatedResultNew) goTo(ctx context.Context, link string) error {
-	var err error
-	p.res, err = p.query(ctx, link)
-	if err != nil {
-		return err
-	}
-	p.nextLink = ""
-	for _, rawLink := range p.res.Header["Link"] {
-		m := relLinkRegexp.FindStringSubmatch(rawLink)
-		if len(m) == 0 {
-			continue
-		}
-		relPath, rel := m[1], m[2]
-		path := path.Join(p.basePath, relPath)
-		switch rel {
-		case "first":
-			p.firstLink = path
-		case "next":
-			p.nextLink = path
-		}
-	}
-	return nil
-}
-
-// next loads the next page of items, if there is one. It returns whether a page
-// was successfully loaded or not; after it returns false, Err should be
-// called to check for any errors.
-//
-// Items can then be inspected with the type-specific Items method.
-//
-// For items iterators, use the next function returned by loadItems instead.
-func (p *PaginatedResultNew) next(ctx context.Context, into interface{}) bool {
-	if !p.first {
-		if p.nextLink == "" {
-			return false
-		}
-		p.err = p.goTo(ctx, p.nextLink)
-		if p.err != nil {
-			return false
-		}
-	}
-	p.first = false
-
-	p.err = decodeResp(p.res, into)
-	return p.err == nil
-}
-
-// First loads the first page of items. Next should be called before inspecting
-// the Items.
-func (p *PaginatedResultNew) First(ctx context.Context) error {
-	p.first = true
-	return p.goTo(ctx, p.firstLink)
-}
-
-// Err returns the error that caused Next to fail, if there was one.
-func (p *PaginatedResultNew) Err() error {
-	return p.err
-}
-
 // relLinkRegexp is the regexp that matches our pagination format
 var relLinkRegexp = regexp.MustCompile(`<(?P<url>[^>]+)>; rel="(?P<rel>[^"]+)"`)
 
@@ -218,11 +60,52 @@ type paginatedRequest struct {
 }
 
 func decodePaginatedResult(opts *proto.ChannelOptions, typ reflect.Type, resp *http.Response) (interface{}, error) {
-	v := reflect.New(typ)
-	if err := decodeResp(resp, v.Interface()); err != nil {
-		return nil, err
+	switch typ {
+	case msgType:
+		var o []map[string]interface{}
+		err := decodeResp(resp, &o)
+		if err != nil {
+			return nil, err
+		}
+		rs := make([]*proto.Message, len(o))
+		for k, v := range o {
+			m := &proto.Message{
+				ChannelOptions: opts,
+			}
+			err = m.FromMap(v)
+			if err != nil {
+				return nil, err
+			}
+			rs[k] = m
+		}
+		return rs, nil
+	case presMsgType:
+		var o []map[string]interface{}
+		err := decodeResp(resp, &o)
+		if err != nil {
+			return nil, err
+		}
+		rs := make([]*proto.PresenceMessage, len(o))
+		for k, v := range o {
+			m := &proto.PresenceMessage{
+				Message: proto.Message{
+					ChannelOptions: opts,
+				},
+			}
+			err = m.FromMap(v)
+			if err != nil {
+				return nil, err
+			}
+			rs[k] = m
+		}
+		return rs, nil
+	default:
+		v := reflect.New(typ)
+		if err := decodeResp(resp, v.Interface()); err != nil {
+			return nil, err
+		}
+		return v.Elem().Interface(), nil
 	}
-	return v.Elem().Interface(), nil
 }
 
 func newPaginatedResult(ctx context.Context, opts *proto.ChannelOptions, req paginatedRequest) (*PaginatedResult, error) {
@@ -327,6 +210,16 @@ func (p *PaginatedResult) Items() []interface{} {
 	return p.items
 }
 
+// Messages gives a slice of messages for the current page. The method panics if
+// the underlying paginated result is not a message.
+func (p *PaginatedResult) Messages() []*Message {
+	items, ok := p.typItems.([]*proto.Message)
+	if !ok {
+		panic(errInvalidType{typ: p.req.typ})
+	}
+	return items
+}
+
 // PresenceMessages gives a slice of presence messages for the current path.
 // The method panics if the underlying paginated result is not a presence message.
 func (p *PaginatedResult) PresenceMessages() []*PresenceMessage {
@@ -337,9 +230,18 @@ func (p *PaginatedResult) PresenceMessages() []*PresenceMessage {
 	return items
 }
 
+// Stats gives a slice of statistics for the current page. The method panics if
+// the underlying paginated result is not statistics.
+func (p *PaginatedResult) Stats() []*Stats {
+	items, ok := p.typItems.([]*proto.Stats)
+	if !ok {
+		panic(errInvalidType{typ: p.req.typ})
+	}
+	return items
+}
+
 type Stats = proto.Stats
 type StatsMessageTypes = proto.MessageTypes
-type StatsMessageCount = proto.MessageCount
 type StatsMessageTraffic = proto.MessageTraffic
 type StatsConnectionTypes = proto.ConnectionTypes
 type StatsResourceCount = proto.ResourceCount
diff --git a/ably/proto/crypto.go b/ably/proto/crypto.go
index 525ec45..13aebcc 100644
--- a/ably/proto/crypto.go
+++ b/ably/proto/crypto.go
@@ -81,9 +81,6 @@ type ChannelOptions struct {
 // GetCipher returns a ChannelCipher based on the algorithms set in the
 // ChannelOptions.CipherParams.
 func (c *ChannelOptions) GetCipher() (ChannelCipher, error) {
-	if c == nil {
-		return nil, errors.New("no cipher configured")
-	}
 	if c.cipher != nil {
 		return c.cipher, nil
 	}
diff --git a/ably/proto/message.go b/ably/proto/message.go
index 2667918..89c72b8 100644
--- a/ably/proto/message.go
+++ b/ably/proto/message.go
@@ -1,13 +1,15 @@
 package proto
 
 import (
-	"bytes"
 	"crypto/aes"
 	"encoding/base64"
 	"encoding/json"
+	"errors"
 	"fmt"
+	"reflect"
 	"strings"
-	"unicode/utf8"
+
+	"github.com/ugorji/go/codec"
 )
 
 // encodings
@@ -19,117 +21,309 @@ const (
 )
 
 type Message struct {
-	ID           string                 `json:"id,omitempty" codec:"id,omitempty"`
-	ClientID     string                 `json:"clientId,omitempty" codec:"clientId,omitempty"`
-	ConnectionID string                 `json:"connectionId,omitempty" codec:"connectionID,omitempty"`
-	Name         string                 `json:"name,omitempty" codec:"name,omitempty"`
-	Data         interface{}            `json:"data,omitempty" codec:"data,omitempty"`
-	Encoding     string                 `json:"encoding,omitempty" codec:"encoding,omitempty"`
-	Timestamp    int64                  `json:"timestamp,omitempty" codec:"timestamp,omitempty"`
-	Extras       map[string]interface{} `json:"extras,omitempty" codec:"extras,omitempty"`
+	ID              string                 `json:"id,omitempty" codec:"id,omitempty"`
+	ClientID        string                 `json:"clientId,omitempty" codec:"clientId,omitempty"`
+	ConnectionID    string                 `json:"connectionId,omitempty" codec:"connectionID,omitempty"`
+	Name            string                 `json:"name,omitempty" codec:"name,omitempty"`
+	Data            interface{}            `json:"data,omitempty" codec:"data,omitempty"`
+	Encoding        string                 `json:"encoding,omitempty" codec:"encoding,omitempty"`
+	Timestamp       int64                  `json:"timestamp" codec:"timestamp"`
+	Extras          map[string]interface{} `json:"extras" codec:"extras"`
+	*ChannelOptions `json:"-" codec:"-"`
 }
 
-func (m Message) String() string {
-	return fmt.Sprintf("<Message %q data=%v>", m.Name, m.Data)
+func (m *Message) maybeJSONEncode() error {
+	if m.Data == nil {
+		return nil
+	}
+	switch m.Data.(type) {
+	case json.Marshaler:
+		bs, err := m.Data.(json.Marshaler).MarshalJSON()
+		if err != nil {
+			return err
+		}
+		m.Data = string(bs)
+		m.Encoding = mergeEncoding(m.Encoding, JSON)
+		return nil
+	case map[string]interface{}:
+		bs, err := json.Marshal(m.Data)
+		if err != nil {
+			return err
+		}
+		m.Data = string(bs)
+		m.Encoding = mergeEncoding(m.Encoding, JSON)
+		return nil
+	}
+	dataType := reflect.TypeOf(m.Data)
+
+	// marshal any sort of slice except for []byte (i.e. []uint8)
+	if dataType.Kind() == reflect.Slice && dataType.Elem().Kind() != reflect.Uint8 {
+		bs, err := json.Marshal(m.Data)
+		if err != nil {
+			return err
+		}
+		m.Data = string(bs)
+		m.Encoding = mergeEncoding(m.Encoding, JSON)
+	}
+	return nil
 }
 
-func unencodableDataErr(data interface{}) error {
-	return fmt.Errorf("message data type %T must be string, []byte, or a value that can be encoded as a JSON object or array", data)
+func (m Message) HasCipher() bool {
+	if m.ChannelOptions != nil {
+		c, _ := m.ChannelOptions.GetCipher()
+		return c != nil
+	}
+	return false
 }
 
-func (m Message) WithEncodedData(cipher ChannelCipher) (Message, error) {
-	// TODO: Unexport once proto gets merged into package ably.
+func (m Message) encode() (Message, error) {
 	if m.Data == nil {
 		return m, nil
 	}
-
-	switch d := m.Data.(type) {
+	err := m.maybeJSONEncode()
+	if err != nil {
+		return m, err
+	}
+	switch m.Data.(type) {
 	case string:
-		if utf8.ValidString(d) {
+		if m.HasCipher() {
 			m.Encoding = mergeEncoding(m.Encoding, UTF8)
-		} else {
-			// If string isn't UTF-8, convert to []byte to encode it as base64
-			// below.
-			m.Data = []byte(d)
+		}
+	case []byte:
+		// ok
+	default:
+		return Message{}, errors.New("unsupported payload type")
+	}
+	if m.ChannelOptions != nil {
+		if cipher, err := m.GetCipher(); err == nil {
+			// since we know that m.Data is either []byte or string at this point, coerceBytes is always
+			// safe here
+			bs, err := coerceBytes(m.Data)
+			if err != nil {
+				return Message{}, err
+			}
+			e, err := cipher.Encrypt(bs)
+			if err != nil {
+				return Message{}, err
+			}
+			m.Data = e
+			m.Encoding = mergeEncoding(m.Encoding, cipher.GetAlgorithm())
 		}
 	}
-	switch d := m.Data.(type) {
+
+	return m, nil
+}
+
+func (m Message) encodeJSON() (Message, error) {
+	e, err := m.encode()
+	if err != nil {
+		return m, err
+	}
+	if d, ok := e.Data.([]byte); ok {
+		b := base64.StdEncoding.EncodeToString(d)
+		e.Data = b
+		e.Encoding = mergeEncoding(e.Encoding, Base64)
+	}
+	return e, nil
+}
+
+func coerceString(i interface{}) (string, error) {
+	switch v := i.(type) {
+	case []byte:
+		return string(v), nil
 	case string:
+		return v, nil
+	default:
+		return "", fmt.Errorf("UTF8 encoding can only handle types string or []byte, but got %T", i)
+	}
+}
+
+func coerceBytes(i interface{}) ([]byte, error) {
+	switch v := i.(type) {
 	case []byte:
-		m.Data = base64.StdEncoding.EncodeToString(d)
-		m.Encoding = mergeEncoding(m.Encoding, Base64)
+		return v, nil
+	case string:
+		return []byte(v), nil
 	default:
-		// RSL4c3, RSL4d3: JSON is only for objects and arrays. So marshal data
-		// into JSON, then check if if's one of those.
-		b, err := json.Marshal(d)
+		return nil, fmt.Errorf("coerceBytes should never get anything but strings or []byte, got %T", i)
+	}
+}
+
+// ToMap returns a map of all message field names to their respective value if
+// the field are set.
+//
+// Spec RSL1j
+func (m Message) ToMap() map[string]interface{} {
+	ctx := make(map[string]interface{})
+	if m.ID != "" {
+		ctx["id"] = m.ID
+	}
+	if m.ClientID != "" {
+		ctx["clientId"] = m.ClientID
+	}
+	if m.ConnectionID != "" {
+		ctx["connectionId"] = m.ConnectionID
+	}
+	if m.Name != "" {
+		ctx["name"] = m.Name
+	}
+	encoding := m.Encoding
+	if m.Data != nil {
+		ctx["data"] = m.Data
+	}
+	if encoding != "" {
+		ctx["encoding"] = encoding
+	}
+	if m.Timestamp != 0 {
+		ctx["timestamp"] = m.Timestamp
+	}
+	if m.Extras != nil {
+		ctx["extras"] = m.Extras
+	}
+	return ctx
+}
+
+func (m Message) MarshalJSON() ([]byte, error) {
+	e, err := m.encodeJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(e.ToMap())
+}
+
+func (m *Message) UnmarshalJSON(data []byte) error {
+	var ctx map[string]interface{}
+	if err := json.Unmarshal(data, &ctx); err != nil {
+		return err
+	}
+	return m.FromMap(ctx)
+}
+
+func (m Message) CodecEncodeSelf(encoder *codec.Encoder) {
+	e, err := m.encode()
+	if err != nil {
+		panic(err)
+	}
+	encoder.MustEncode(e.ToMap())
+}
+
+// CodecDecodeSelf implements codec.Selfer interface for msgpack decoding.
+func (m *Message) CodecDecodeSelf(decoder *codec.Decoder) {
+	ctx := make(map[string]interface{})
+	decoder.MustDecode(&ctx)
+	m.FromMap(ctx)
+}
+
+func (m *Message) FromMap(ctx map[string]interface{}) error {
+	if v, ok := ctx["id"]; ok {
+		x, err := coerceString(v)
 		if err != nil {
-			return Message{}, fmt.Errorf("%s; encoding as JSON: %w", unencodableDataErr(d), err)
+			return err
 		}
-		token, _ := json.NewDecoder(bytes.NewReader(b)).Token()
-		if token != json.Delim('[') && token != json.Delim('{') {
-			return Message{}, fmt.Errorf("%s; encoded as JSON %T", unencodableDataErr(d), token)
+		m.ID = string(x)
+	}
+	if v, ok := ctx["clientId"]; ok {
+		x, err := coerceString(v)
+		if err != nil {
+			return err
 		}
-		m.Data = string(b)
-		m.Encoding = mergeEncoding(m.Encoding, JSON)
+		m.ClientID = string(x)
 	}
-
-	if cipher == nil {
-		return m, nil
+	if v, ok := ctx["connectionId"]; ok {
+		x, err := coerceString(v)
+		if err != nil {
+			return err
+		}
+		m.ConnectionID = string(x)
 	}
+	if v, ok := ctx["name"]; ok {
+		x, err := coerceString(v)
+		if err != nil {
+			return err
+		}
+		m.Name = string(x)
+	}
+	if v, ok := ctx["encoding"]; ok {
+		x, err := coerceString(v)
+		if err != nil {
+			return err
+		}
+		m.Encoding = string(x)
+	}
+	if v, ok := ctx["data"]; ok {
+		m.Data = v
+		dec, err := m.decode()
+		if err != nil {
+			return err
+		}
+		*m = dec
+	}
+	if v, ok := ctx["timestamp"]; ok {
+		switch e := v.(type) {
+		case float64:
+			m.Timestamp = int64(e)
+		case uint64:
+			m.Timestamp = int64(e)
+		case int64:
+			m.Timestamp = e
+		}
+	}
+	if v, ok := ctx["extras"]; ok {
+		m.Extras = v.(map[string]interface{})
+	}
+	return nil
+}
+
+// MemberKey returns string that allows to uniquely identify connected clients.
+func (m *Message) MemberKey() string {
+	return m.ConnectionID + ":" + m.ClientID
+}
 
-	// since we know that m.Data is either []byte or string at this point, coerceBytes is always
-	// safe here
-	bs, err := coerceBytes(m.Data)
+func (m Message) Decrypt() (interface{}, error) {
+	cipher, err := m.GetCipher()
 	if err != nil {
-		panic(err)
+		return nil, err
 	}
-	e, err := cipher.Encrypt(bs)
+
+	d, err := coerceBytes(m.Data)
 	if err != nil {
-		return Message{}, fmt.Errorf("encrypting message data: %w", err)
+		return nil, err
 	}
-	if _, ok := m.Data.(string); ok {
-		// Not sure why this is useful, and can't find the requirement in the
-		// spec, but test fixtures expect this.
-		m.Encoding = mergeEncoding(m.Encoding, UTF8)
+	v, err := cipher.Decrypt(d)
+	if err != nil {
+		return nil, err
 	}
-	m.Data = base64.StdEncoding.EncodeToString(e)
-	m.Encoding = mergeEncoding(m.Encoding, cipher.GetAlgorithm())
-	m.Encoding = mergeEncoding(m.Encoding, Base64)
-	return m, nil
+	return v, nil
 }
 
-func (m Message) WithDecodedData(cipher ChannelCipher) (Message, error) {
-	// TODO: Unexport once proto gets merged into package ably.
-
+func (m Message) decode() (Message, error) {
 	// strings.Split on empty string returns []string{""}
 	if m.Data == nil || m.Encoding == "" {
 		return m, nil
 	}
 	encodings := strings.Split(m.Encoding, "/")
-	for len(encodings) > 0 {
-		encoding := encodings[len(encodings)-1]
-		encodings = encodings[:len(encodings)-1]
-		switch encoding {
+	for i := len(encodings) - 1; i >= 0; i-- {
+		switch encodings[i] {
 		case Base64:
 			d, err := coerceString(m.Data)
 			if err != nil {
-				return m, err
+				return Message{}, err
 			}
 			data, err := base64.StdEncoding.DecodeString(d)
 			if err != nil {
-				return m, err
+				return Message{}, err
 			}
 			m.Data = data
 		case UTF8:
 			d, err := coerceString(m.Data)
 			if err != nil {
-				return m, err
+				return Message{}, err
 			}
 			m.Data = d
 		case JSON:
 			d, err := coerceBytes(m.Data)
 			if err != nil {
-				return m, err
+				return Message{}, err
 			}
 			var result interface{}
 			if err := json.Unmarshal(d, &result); err != nil {
@@ -137,55 +331,22 @@ func (m Message) WithDecodedData(cipher ChannelCipher) (Message, error) {
 			}
 			m.Data = result
 		default:
-			if strings.HasPrefix(encoding, Cipher) {
-				if cipher == nil {
-					return m, fmt.Errorf("message data is encrypted as %s, but cipher wasn't provided", encoding)
-				}
-				d, err := coerceBytes(m.Data)
+			switch {
+			case strings.HasPrefix(encodings[i], Cipher):
+				d, err := m.Decrypt()
 				if err != nil {
 					return m, err
 				}
-				d, err = cipher.Decrypt(d)
-				if err != nil {
-					return m, fmt.Errorf("decrypting message data: %w", err)
-				}
 				m.Data = d
-			} else {
-				return m, fmt.Errorf("unknown encoding %s", encoding)
+			default:
+				return m, fmt.Errorf("unknown encoding %s", encodings[i])
 			}
+
 		}
-		m.Encoding = strings.Join(encodings, "/")
 	}
 	return m, nil
 }
 
-func coerceString(i interface{}) (string, error) {
-	switch v := i.(type) {
-	case []byte:
-		return string(v), nil
-	case string:
-		return v, nil
-	default:
-		return "", fmt.Errorf("UTF8 encoding can only handle types string or []byte, but got %T", i)
-	}
-}
-
-func coerceBytes(i interface{}) ([]byte, error) {
-	switch v := i.(type) {
-	case []byte:
-		return v, nil
-	case string:
-		return []byte(v), nil
-	default:
-		return nil, fmt.Errorf("coerceBytes should never get anything but strings or []byte, got %T", i)
-	}
-}
-
-// MemberKey returns string that allows to uniquely identify connected clients.
-func (m *Message) MemberKey() string {
-	return m.ConnectionID + ":" + m.ClientID
-}
-
 func addPadding(src []byte) []byte {
 	padlen := byte(aes.BlockSize - (len(src) % aes.BlockSize))
 	data := make([]byte, len(src)+int(padlen))
diff --git a/ably/proto/presence_message.go b/ably/proto/presence_message.go
index ff40e2c..78b423e 100644
--- a/ably/proto/presence_message.go
+++ b/ably/proto/presence_message.go
@@ -1,7 +1,9 @@
 package proto
 
 import (
-	"fmt"
+	"encoding/json"
+
+	"github.com/ugorji/go/codec"
 )
 
 type PresenceAction int64
@@ -19,12 +21,57 @@ type PresenceMessage struct {
 	Action PresenceAction `json:"action" codec:"action"`
 }
 
-func (m PresenceMessage) String() string {
-	return fmt.Sprintf("<PresenceMessage %v data=%v>", [...]string{
-		"absent",
-		"present",
-		"enter",
-		"leave",
-		"update",
-	}[m.Action], m.Data)
+func (m PresenceMessage) MarshalJSON() ([]byte, error) {
+	e, err := m.encodeJSON()
+	if err != nil {
+		return nil, err
+	}
+	ctx := e.ToMap()
+	ctx["action"] = m.Action
+	return json.Marshal(ctx)
+}
+
+func (m *PresenceMessage) UnmarshalJSON(data []byte) error {
+	var ctx map[string]interface{}
+	if err := json.Unmarshal(data, &ctx); err != nil {
+		return err
+	}
+	return m.FromMap(ctx)
+}
+
+// CodecEncodeSelf encodes PresenceMessage into a msgpack format.
+func (m PresenceMessage) CodecEncodeSelf(encoder *codec.Encoder) {
+	e, err := m.encode()
+	if err != nil {
+		panic(err)
+	}
+	ctx := e.ToMap()
+	ctx["action"] = m.Action
+	encoder.MustEncode(ctx)
+}
+
+// CodecDecodeSelf implements codec.Selfer interface for msgpack decoding.
+func (m *PresenceMessage) CodecDecodeSelf(decoder *codec.Decoder) {
+	ctx := make(map[string]interface{})
+	decoder.MustDecode(&ctx)
+	if err := m.FromMap(ctx); err != nil {
+		panic(err)
+	}
+}
+
+func (m *PresenceMessage) FromMap(ctx map[string]interface{}) error {
+	msg := &m.Message
+	if err := msg.FromMap(ctx); err != nil {
+		return err
+	}
+	if v, ok := ctx["action"]; ok {
+		m.Action = PresenceAction(coerceInt64(v))
+	}
+	return nil
+}
+
+func (m PresenceMessage) ToMap() map[string]interface{} {
+	ctx := m.Message.ToMap()
+	ctx["action"] = m.Action
+	return ctx
 }
diff --git a/ably/proto/protocol_message.go b/ably/proto/protocol_message.go
index a543b45..a9db03b 100644
--- a/ably/proto/protocol_message.go
+++ b/ably/proto/protocol_message.go
@@ -1,6 +1,7 @@
 package proto
 
 import (
+	"encoding/json"
 	"fmt"
 	"time"
 )
@@ -92,6 +93,81 @@ type ProtocolMessage struct {
 	Flags             Flag               `json:"flags,omitempty" codec:"flags,omitempty"`
 }
 
+func (p *ProtocolMessage) UnmarshalJSON(b []byte) error {
+	ctx := make(map[string]interface{})
+	err := json.Unmarshal(b, &ctx)
+	if err != nil {
+		return err
+	}
+	p.FromMap(ctx)
+	return nil
+}
+
+func (p *ProtocolMessage) FromMap(ctx map[string]interface{}) {
+	if v, ok := ctx["messages"]; ok {
+		i := v.([]interface{})
+		for _, v := range i {
+			var msg Message
+			msg.FromMap(v.(map[string]interface{}))
+			p.Messages = append(p.Messages, &msg)
+		}
+	}
+	if v, ok := ctx["presence"]; ok {
+		i := v.([]interface{})
+		for _, v := range i {
+			var msg PresenceMessage
+			msg.FromMap(v.(map[string]interface{}))
+			p.Presence = append(p.Presence, &msg)
+		}
+	}
+	if v, ok := ctx["id"]; ok {
+		p.ID = v.(string)
+	}
+	if v, ok := ctx["applicationId"]; ok {
+		p.ApplicationID = v.(string)
+	}
+	if v, ok := ctx["connectionId"]; ok {
+		p.ConnectionID = v.(string)
+	}
+	if v, ok := ctx["connectionKey"]; ok {
+		p.ConnectionKey = v.(string)
+	}
+	if v, ok := ctx["channel"]; ok {
+		p.Channel = v.(string)
+	}
+	if v, ok := ctx["channelSerial"]; ok {
+		p.ChannelSerial = v.(string)
+	}
+	if v, ok := ctx["connectionDetails"]; ok {
+		c := &ConnectionDetails{}
+		c.FromMap(v.(map[string]interface{}))
+		p.ConnectionDetails = c
+	}
+	if v, ok := ctx["error"]; ok {
+		c := &ErrorInfo{}
+		c.FromMap(v.(map[string]interface{}))
+		p.Error = c
+	}
+	if v, ok := ctx["msgSerial"]; ok {
+		p.MsgSerial = coerceInt64(v)
+	}
+	if v, ok := ctx["connectionSerial"]; ok {
+		p.ConnectionSerial = coerceInt64(v)
+	}
+	if v, ok := ctx["timestamp"]; ok {
+		p.Timestamp = coerceInt64(v)
+	}
+	if v, ok := ctx["count"]; ok {
+		p.Count = int(coerceInt64(v))
+	}
+	if v, ok := ctx["action"]; ok {
+		p.Action = Action(coerceInt8(v))
+	}
+	if v, ok := ctx["flags"]; ok {
+		p.Flags = Flag(coerceInt64(v))
+	}
+}
+
 func (msg *ProtocolMessage) String() string {
 	switch msg.Action {
 	case ActionHeartbeat:
diff --git a/ably/proto/stat.go b/ably/proto/stat.go
index 622d686..621262b 100644
--- a/ably/proto/stat.go
+++ b/ably/proto/stat.go
@@ -1,9 +1,6 @@
 package proto
 
-import (
-	"fmt"
-	"time"
-)
+import "time"
 
 const (
 	StatGranularityMinute = "minute"
@@ -49,7 +46,7 @@ type MessageCount struct {
 
 type MessageTypes struct {
 	All      MessageCount `json:"all" codec:"all"`
-	Messages MessageCount `json:"messages" codec:"messages"`
+	Plain    MessageCount `json:"plain" codec:"plain"`
 	Presence MessageCount `json:"presence" codec:"presence"`
 }
 
@@ -139,7 +136,3 @@ type Stats struct {
 	XchgConsumer  XchgMessages    `json:"xchgConsumer" codec:"xchgConsumer"`
 	PeakRates     Rates           `json:"peakRates" codec:"peakRates"`
 }
-
-func (s Stats) String() string {
-	return fmt.Sprintf("<Stats %v; unit=%v; count=%v>", s.IntervalID, s.Unit, s.Count)
-}
diff --git a/ably/realtime_channel.go b/ably/realtime_channel.go
index 7a1a676..9ebecb5 100644
--- a/ably/realtime_channel.go
+++ b/ably/realtime_channel.go
@@ -431,9 +431,11 @@ func (c *RealtimeChannel) PublishBatch(ctx context.Context, messages []*Message)
 	return res.Wait(ctx)
 }
 
-// History is equivalent to RESTChannel.History.
-func (c *RealtimeChannel) History(o ...HistoryOption) HistoryRequest {
-	return c.client.rest.Channels.Get(c.Name).History(o...)
+// History gives the channel's message history according to the given parameters.
+// The returned result can be inspected for the messages via the Messages()
+// method.
+func (c *RealtimeChannel) History(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	return c.client.rest.Channels.Get(c.Name).History(ctx, params)
 }
 
 func (c *RealtimeChannel) send(msg *proto.ProtocolMessage) (result, error) {
diff --git a/ably/realtime_client.go b/ably/realtime_client.go
index a904600..98ca2ce 100644
--- a/ably/realtime_client.go
+++ b/ably/realtime_client.go
@@ -51,9 +51,11 @@ func (c *Realtime) Close() {
 	c.Connection.Close()
 }
 
-// Stats is the same as REST.Stats.
-func (c *Realtime) Stats(o ...StatsOption) StatsRequest {
-	return c.rest.Stats(o...)
+// Stats gives the clients metrics according to the given parameters. The
+// returned result can be inspected for the statistics via the Stats()
+// method.
+func (c *Realtime) Stats(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	return c.rest.Stats(ctx, params)
 }
 
 // Time
diff --git a/ably/realtime_conn.go b/ably/realtime_conn.go
index 849639b..f55b7ed 100644
--- a/ably/realtime_conn.go
+++ b/ably/realtime_conn.go
@@ -470,9 +470,9 @@ func (c *Connection) Key() string {
 //
 // Ping returns non-nil error without any attempt of communication with Ably
 // if the connection state is ConnectionStateClosed or ConnectionStateFailed.
-func (c *Connection) Ping() (ping, pong time.Duration, err error) {
-	return 0, 0, errors.New("TODO")
-}
+//func (c *Connection) Ping() (ping, pong time.Duration, err error) {
+//	return 0, 0, errors.New("TODO")
+//}
 
 // ErrorReason gives last known error that caused connection transit to
 // ConnectionStateFailed state.
diff --git a/ably/rest_channel.go b/ably/rest_channel.go
index d35613d..41000ff 100644
--- a/ably/rest_channel.go
+++ b/ably/rest_channel.go
@@ -2,15 +2,11 @@ package ably
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"net/url"
-	"strconv"
 	"strings"
-	"time"
 
 	"github.com/ably/ably-go/ably/internal/ablyutil"
-	"github.com/ugorji/go/codec"
 
 	"github.com/ably/ably-go/ably/proto"
 )
@@ -85,16 +81,18 @@ func PublishBatchWithParams(params map[string]string) PublishBatchOption {
 
 // PublishBatchWithOptions is PublishBatch with optional parameters.
 func (c *RESTChannel) PublishBatchWithOptions(ctx context.Context, messages []*Message, options ...PublishBatchOption) error {
+	// TODO: Use context
 	var publishOpts publishBatchOptions
 	for _, o := range options {
 		o(&publishOpts)
 	}
-	for i, m := range messages {
-		cipher, _ := c.options.GetCipher()
-		var err error
-		*m, err = (*m).WithEncodedData(cipher)
-		if err != nil {
-			return fmt.Errorf("encoding data for message #%d: %w", i, err)
+	msgPtrs := make([]*proto.Message, 0, len(messages))
+	for _, m := range messages {
+		msgPtrs = append(msgPtrs, (*proto.Message)(m))
+	}
+	if c.options != nil {
+		for _, v := range messages {
+			v.ChannelOptions = c.options
 		}
 	}
 	useIdempotent := c.client.opts.idempotentRESTPublishing()
@@ -143,179 +141,16 @@ func (c *RESTChannel) PublishBatchWithOptions(ctx context.Context, messages []*M
 	return res.Body.Close()
 }
 
-// History gives the channel's message history.
-func (c *RESTChannel) History(o ...HistoryOption) HistoryRequest {
-	params := (&historyOptions{}).apply(o...)
-	return HistoryRequest{
-		r:       c.client.newPaginatedRequest("/channels/"+c.Name+"/history", params),
-		channel: c,
-	}
-}
-
-// A HistoryOption configures a call to RESTChannel.History or RealtimeChannel.History.
-type HistoryOption func(*historyOptions)
-
-func HistoryWithStart(t time.Time) HistoryOption {
-	return func(o *historyOptions) {
-		o.params.Set("start", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func HistoryWithEnd(t time.Time) HistoryOption {
-	return func(o *historyOptions) {
-		o.params.Set("end", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func HistoryWithLimit(limit int) HistoryOption {
-	return func(o *historyOptions) {
-		o.params.Set("limit", strconv.Itoa(limit))
-	}
-}
-
-func HistoryWithDirection(d Direction) HistoryOption {
-	return func(o *historyOptions) {
-		o.params.Set("direction", string(d))
-	}
-}
-
-type historyOptions struct {
-	params url.Values
-}
-
-func (o *historyOptions) apply(opts ...HistoryOption) url.Values {
-	o.params = make(url.Values)
-	for _, opt := range opts {
-		opt(o)
-	}
-	return o.params
-}
-
-// HistoryRequest represents a request prepared by the RESTChannel.History or
-// RealtimeChannel.History method, ready to be performed by its Pages or Items methods.
-type HistoryRequest struct {
-	r       paginatedRequestNew
-	channel *RESTChannel
-}
-
-// Pages returns an iterator for whole pages of History.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r HistoryRequest) Pages(ctx context.Context) (*MessagesPaginatedResult, error) {
-	var res MessagesPaginatedResult
-	return &res, res.load(ctx, r.r)
-}
-
-// A MessagesPaginatedResult is an iterator for the result of a History request.
-//
-// See "Paginated results" section in the package-level documentation.
-type MessagesPaginatedResult struct {
-	PaginatedResultNew
-	items []*Message
-}
-
-// Next retrieves the next page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *MessagesPaginatedResult) Next(ctx context.Context) bool {
-	p.items = nil // avoid mutating already returned items
-	return p.next(ctx, &p.items)
-}
-
-// Items returns the current page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *MessagesPaginatedResult) Items() []*Message {
-	return p.items
-}
-
-// Items returns a convenience iterator for single History, over an underlying
-// paginated iterator.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r HistoryRequest) Items(ctx context.Context) (*MessagesPaginatedItems, error) {
-	var res MessagesPaginatedItems
-	var err error
-	res.next, err = res.loadItems(ctx, r.r, func() (interface{}, func() int) {
-		res.items = nil // avoid mutating already returned Items
-		return r.channel.fullMessagesDecoder(&res.items), func() int {
-			return len(res.items)
-		}
-	})
-	return &res, err
-}
-
-// fullMessagesDecoder wraps a destination slice of messages in a decoder value
-// that decodes both the message itself from the transport-level encoding and
-// the data field within from its message-specific encoding.
-func (c *RESTChannel) fullMessagesDecoder(dst *[]*Message) interface{} {
-	return &fullMessagesDecoder{dst: dst, c: c}
-}
-
-type fullMessagesDecoder struct {
-	dst *[]*Message
-	c   *RESTChannel
-}
-
-func (t *fullMessagesDecoder) UnmarshalJSON(b []byte) error {
-	err := json.Unmarshal(b, &t.dst)
+// History gives the channel's message history according to the given parameters.
+// The returned result can be inspected for the messages via the Messages()
+// method.
+func (c *RESTChannel) History(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	path := c.baseURL + "/history"
+	rst, err := newPaginatedResult(ctx, c.options, paginatedRequest{typ: msgType, path: path, params: params, query: query(c.client.get), logger: c.logger(), respCheck: checkValidHTTPResponse})
 	if err != nil {
-		return err
+		return nil, err
 	}
-	t.decodeMessagesData()
-	return nil
-}
-
-func (t *fullMessagesDecoder) CodecEncodeSelf(*codec.Encoder) {
-	panic("messagesDecoderForChannel cannot be used as encoder")
-}
-
-func (t *fullMessagesDecoder) CodecDecodeSelf(decoder *codec.Decoder) {
-	decoder.MustDecode(&t.dst)
-	t.decodeMessagesData()
-}
-
-var _ interface {
-	json.Unmarshaler
-	codec.Selfer
-} = (*fullMessagesDecoder)(nil)
-
-func (t *fullMessagesDecoder) decodeMessagesData() {
-	cipher, _ := t.c.options.GetCipher()
-	for _, m := range *t.dst {
-		var err error
-		*m, err = m.WithDecodedData(cipher)
-		if err != nil {
-			// RSL6b
-			t.c.logger().sugar().Errorf("Couldn't fully decode message data from channel %q: %w", t.c.Name, err)
-		}
-	}
-}
-
-type MessagesPaginatedItems struct {
-	PaginatedResultNew
-	items []*Message
-	item  *Message
-	next  func(context.Context) (int, bool)
-}
-
-// Next retrieves the next result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *MessagesPaginatedItems) Next(ctx context.Context) bool {
-	i, ok := p.next(ctx)
-	if !ok {
-		return false
-	}
-	p.item = p.items[i]
-	return true
-}
-
-// Item returns the current result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *MessagesPaginatedItems) Item() *Message {
-	return p.item
+	return rst, nil
 }
 
 func (c *RESTChannel) logger() *LoggerOptions {
diff --git a/ably/rest_client.go b/ably/rest_client.go
index 0a3ecbb..4615ce1 100644
--- a/ably/rest_client.go
+++ b/ably/rest_client.go
@@ -14,9 +14,7 @@ import (
 	"net/http"
 	"net/http/httptrace"
 	"net/http/httputil"
-	"net/url"
 	"reflect"
-	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -27,6 +25,7 @@ import (
 
 var (
 	msgType     = reflect.TypeOf((*[]*proto.Message)(nil)).Elem()
+	statType    = reflect.TypeOf((*[]*proto.Stats)(nil)).Elem()
 	presMsgType = reflect.TypeOf((*[]*proto.PresenceMessage)(nil)).Elem()
 	arrayTyp    = reflect.TypeOf((*[]interface{})(nil)).Elem()
 )
@@ -155,141 +154,11 @@ func (c *REST) Time(ctx context.Context) (time.Time, error) {
 	return time.Unix(times[0]/1000, times[0]%1000), nil
 }
 
-// Stats retrieves statistics about the Ably app's activity.
-func (c *REST) Stats(o ...StatsOption) StatsRequest {
-	params := (&statsOptions{}).apply(o...)
-	return StatsRequest{r: c.newPaginatedRequest("/stats", params)}
-}
-
-// A StatsOption configures a call to REST.Stats or Realtime.Stats.
-type StatsOption func(*statsOptions)
-
-func StatsWithStart(t time.Time) StatsOption {
-	return func(o *statsOptions) {
-		o.params.Set("start", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func StatsWithEnd(t time.Time) StatsOption {
-	return func(o *statsOptions) {
-		o.params.Set("end", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func StatsWithLimit(limit int) StatsOption {
-	return func(o *statsOptions) {
-		o.params.Set("limit", strconv.Itoa(limit))
-	}
-}
-
-func StatsWithDirection(d Direction) StatsOption {
-	return func(o *statsOptions) {
-		o.params.Set("direction", string(d))
-	}
-}
-
-type PeriodUnit string
-
-const (
-	PeriodMinute PeriodUnit = "minute"
-	PeriodHour   PeriodUnit = "hour"
-	PeriodDay    PeriodUnit = "day"
-	PeriodMonth  PeriodUnit = "month"
-)
-
-func StatsWithUnit(d PeriodUnit) StatsOption {
-	return func(o *statsOptions) {
-		o.params.Set("unit", string(d))
-	}
-}
-
-type statsOptions struct {
-	params url.Values
-}
-
-func (o *statsOptions) apply(opts ...StatsOption) url.Values {
-	o.params = make(url.Values)
-	for _, opt := range opts {
-		opt(o)
-	}
-	return o.params
-}
-
-// StatsRequest represents a request prepared by the REST.Stats or
-// Realtime.Stats method, ready to be performed by its Pages or Items methods.
-type StatsRequest struct {
-	r paginatedRequestNew
-}
-
-// Pages returns an iterator for whole pages of Stats.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r StatsRequest) Pages(ctx context.Context) (*StatsPaginatedResult, error) {
-	var res StatsPaginatedResult
-	return &res, res.load(ctx, r.r)
-}
-
-// A StatsPaginatedResult is an iterator for the result of a Stats request.
-//
-// See "Paginated results" section in the package-level documentation.
-type StatsPaginatedResult struct {
-	PaginatedResultNew
-	items []*Stats
-}
-
-// Next retrieves the next page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *StatsPaginatedResult) Next(ctx context.Context) bool {
-	p.items = nil // avoid mutating already returned items
-	return p.next(ctx, &p.items)
-}
-
-// Items returns the current page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *StatsPaginatedResult) Items() []*Stats {
-	return p.items
-}
-
-// Items returns a convenience iterator for single Stats, over an underlying
-// paginated iterator.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r StatsRequest) Items(ctx context.Context) (*StatsPaginatedItems, error) {
-	var res StatsPaginatedItems
-	var err error
-	res.next, err = res.loadItems(ctx, r.r, func() (interface{}, func() int) {
-		res.items = nil // avoid mutating already returned items
-		return &res.items, func() int { return len(res.items) }
-	})
-	return &res, err
-}
-
-type StatsPaginatedItems struct {
-	PaginatedResultNew
-	items []*Stats
-	item  *Stats
-	next  func(context.Context) (int, bool)
-}
-
-// Next retrieves the next result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *StatsPaginatedItems) Next(ctx context.Context) bool {
-	i, ok := p.next(ctx)
-	if !ok {
-		return false
-	}
-	p.item = p.items[i]
-	return true
-}
-
-// Item returns the current result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *StatsPaginatedItems) Item() *Stats {
-	return p.item
+// Stats gives the channel's metrics according to the given parameters.
+// The returned result can be inspected for the statistics via the Stats()
+// method.
+func (c *REST) Stats(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	return newPaginatedResult(ctx, nil, paginatedRequest{typ: statType, path: "/stats", params: params, query: query(c.get), logger: c.logger(), respCheck: checkValidHTTPResponse})
 }
 
 // request this contains fields necessary to compose http request that will be
@@ -677,7 +546,5 @@ func decodeResp(resp *http.Response, out interface{}) error {
 	if err != nil {
 		return err
 	}
-	b, _ := ioutil.ReadAll(resp.Body)
-
-	return decode(typ, bytes.NewReader(b), out)
+	return decode(typ, resp.Body, out)
 }
diff --git a/ably/rest_presence.go b/ably/rest_presence.go
index a67d67d..0adad4b 100644
--- a/ably/rest_presence.go
+++ b/ably/rest_presence.go
@@ -1,14 +1,6 @@
 package ably
 
-import (
-	"context"
-	"encoding/json"
-	"net/url"
-	"strconv"
-	"time"
-
-	"github.com/ugorji/go/codec"
-)
+import "context"
 
 type RESTPresence struct {
 	client  *REST
@@ -23,181 +15,14 @@ func (p *RESTPresence) Get(ctx context.Context, params *PaginateParams) (*Pagina
 	return newPaginatedResult(ctx, nil, paginatedRequest{typ: presMsgType, path: path, params: params, query: query(p.client.get), logger: p.logger(), respCheck: checkValidHTTPResponse})
 }
 
-func (p *RESTPresence) logger() *LoggerOptions {
-	return p.client.logger()
-}
-
-// History gives the channel's presence history.
-func (c *RESTPresence) History(o ...PresenceHistoryOption) PresenceHistoryRequest {
-	params := (&presenceHistoryOptions{}).apply(o...)
-	return PresenceHistoryRequest{
-		r:       c.client.newPaginatedRequest("/channels/"+c.channel.Name+"/presence/history", params),
-		channel: c.channel,
-	}
-}
-
-// A HistoryOption configures a call to RESTChannel.History or RealtimeChannel.History.
-type PresenceHistoryOption func(*presenceHistoryOptions)
-
-func PresenceHistoryWithStart(t time.Time) PresenceHistoryOption {
-	return func(o *presenceHistoryOptions) {
-		o.params.Set("start", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func PresenceHistoryWithEnd(t time.Time) PresenceHistoryOption {
-	return func(o *presenceHistoryOptions) {
-		o.params.Set("end", strconv.FormatInt(unixMilli(t), 10))
-	}
-}
-
-func PresenceHistoryWithLimit(limit int) PresenceHistoryOption {
-	return func(o *presenceHistoryOptions) {
-		o.params.Set("limit", strconv.Itoa(limit))
-	}
-}
-
-func PresenceHistoryWithDirection(d Direction) PresenceHistoryOption {
-	return func(o *presenceHistoryOptions) {
-		o.params.Set("direction", string(d))
-	}
-}
-
-type presenceHistoryOptions struct {
-	params url.Values
-}
-
-func (o *presenceHistoryOptions) apply(opts ...PresenceHistoryOption) url.Values {
-	o.params = make(url.Values)
-	for _, opt := range opts {
-		opt(o)
-	}
-	return o.params
-}
-
-// PresenceHistoryRequest represents a request prepared by the RESTPresence.History or
-// RealtimePresence.History method, ready to be performed by its Pages or Items methods.
-type PresenceHistoryRequest struct {
-	r       paginatedRequestNew
-	channel *RESTChannel
-}
-
-// Pages returns an iterator for whole pages of presence messages.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r PresenceHistoryRequest) Pages(ctx context.Context) (*PresencePaginatedResult, error) {
-	res := PresencePaginatedResult{decoder: r.channel.fullPresenceDecoder}
-	return &res, res.load(ctx, r.r)
-}
-
-// A PresencePaginatedResult is an iterator for the result of a PresenceHistory request.
-//
-// See "Paginated results" section in the package-level documentation.
-type PresencePaginatedResult struct {
-	PaginatedResultNew
-	items   []*PresenceMessage
-	decoder func(*[]*PresenceMessage) interface{}
-}
-
-// Next retrieves the next page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *PresencePaginatedResult) Next(ctx context.Context) bool {
-	p.items = nil // avoid mutating already returned items
-	return p.next(ctx, p.decoder(&p.items))
-}
-
-// Items returns the current page of results.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *PresencePaginatedResult) Items() []*PresenceMessage {
-	return p.items
-}
-
-// Items returns a convenience iterator for single PresenceHistory, over an underlying
-// paginated iterator.
-//
-// See "Paginated results" section in the package-level documentation.
-func (r PresenceHistoryRequest) Items(ctx context.Context) (*PresencePaginatedItems, error) {
-	var res PresencePaginatedItems
-	var err error
-	res.next, err = res.loadItems(ctx, r.r, func() (interface{}, func() int) {
-		return r.channel.fullPresenceDecoder(&res.items), func() int {
-			return len(res.items)
-		}
-	})
-	return &res, err
-}
-
-// fullPresenceDecoder wraps a destination slice of messages in a decoder value
-// that decodes both the message itself from the transport-level encoding and
-// the data field within from its message-specific encoding.
-func (c *RESTChannel) fullPresenceDecoder(dst *[]*PresenceMessage) interface{} {
-	return &fullPresenceDecoder{dst: dst, c: c}
-}
-
-type fullPresenceDecoder struct {
-	dst *[]*PresenceMessage
-	c   *RESTChannel
-}
-
-func (t *fullPresenceDecoder) UnmarshalJSON(b []byte) error {
-	err := json.Unmarshal(b, &t.dst)
-	if err != nil {
-		return err
-	}
-	t.decodeMessagesData()
-	return nil
-}
-
-func (t *fullPresenceDecoder) CodecEncodeSelf(*codec.Encoder) {
-	panic("presenceDecoderForChannel cannot be used as encoder")
-}
-
-func (t *fullPresenceDecoder) CodecDecodeSelf(decoder *codec.Decoder) {
-	decoder.MustDecode(&t.dst)
-	t.decodeMessagesData()
-}
-
-var _ interface {
-	json.Unmarshaler
-	codec.Selfer
-} = (*fullPresenceDecoder)(nil)
-
-func (t *fullPresenceDecoder) decodeMessagesData() {
-	cipher, _ := t.c.options.GetCipher()
-	for _, m := range *t.dst {
-		var err error
-		m.Message, err = m.Message.WithDecodedData(cipher)
-		if err != nil {
-			// RSL6b
-			t.c.logger().sugar().Errorf("Couldn't fully decode presence message data from channel %q: %w", t.c.Name, err)
-		}
-	}
-}
-
-type PresencePaginatedItems struct {
-	PaginatedResultNew
-	items []*PresenceMessage
-	item  *PresenceMessage
-	next  func(context.Context) (int, bool)
-}
-
-// Next retrieves the next result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *PresencePaginatedItems) Next(ctx context.Context) bool {
-	i, ok := p.next(ctx)
-	if !ok {
-		return false
-	}
-	p.item = p.items[i]
-	return true
+// History gives the channel's presence messages history according to the given
+// parameters. The returned result can be inspected for the presence messages
+// via the PresenceMessages() method.
+func (p *RESTPresence) History(ctx context.Context, params *PaginateParams) (*PaginatedResult, error) {
+	path := p.channel.baseURL + "/presence/history"
+	return newPaginatedResult(ctx, nil, paginatedRequest{typ: presMsgType, path: path, params: params, query: query(p.client.get), logger: p.logger(), respCheck: checkValidHTTPResponse})
 }
 
-// Item returns the current result.
-//
-// See the "Paginated results" section in the package-level documentation.
-func (p *PresencePaginatedItems) Item() *PresenceMessage {
-	return p.item
+func (p *RESTPresence) logger() *LoggerOptions {
+	return p.client.logger()
 }


Test patch:
diff --git a/ably/ablytest/cryptodata.go b/ably/ablytest/cryptodata.go
index b019b93..4146439 100644
--- a/ably/ablytest/cryptodata.go
+++ b/ably/ablytest/cryptodata.go
@@ -15,8 +15,8 @@ type CryptoData struct {
 	Key       string `json:"key"`
 	IV        string `json:"iv"`
 	Items     []struct {
-		Encoded   json.RawMessage `json:"encoded"`
-		Encrypted json.RawMessage `json:"encrypted"`
+		Encoded   map[string]interface{} `json:"encoded"`
+		Encrypted map[string]interface{} `json:"encrypted"`
 	} `json:"items"`
 }
 
diff --git a/ably/ablytest/pagination.go b/ably/ablytest/pagination.go
deleted file mode 100644
index e052157..0000000
--- a/ably/ablytest/pagination.go
+++ /dev/null
@@ -1,209 +0,0 @@
-package ablytest
-
-import (
-	"context"
-	"fmt"
-	"reflect"
-)
-
-// AllPages appends all items from all pages resulting from a paginated
-// request into the slice pointed to by dst, which must be a pointer to a slice
-// of the same type as the paginated response's items.
-func AllPages(dst, paginatedRequest interface{}) error {
-	_, getItems := generalizePagination(reflect.ValueOf(paginatedRequest))
-	items, err := getItems()
-	if err != nil {
-		return err
-	}
-	all := reflect.ValueOf(dst).Elem()
-	for items.next() {
-		all.Set(reflect.Append(all, reflect.ValueOf(items.item())))
-	}
-	return items.err()
-}
-
-type paginationOptions struct {
-	equal func(x, y interface{}) bool
-}
-
-type PaginationOption func(*paginationOptions)
-
-func PaginationWithEqual(equal func(x, y interface{}) bool) PaginationOption {
-	return func(o *paginationOptions) {
-		o.equal = equal
-	}
-}
-
-func TestPagination(expected, request interface{}, perPage int, options ...PaginationOption) error {
-	opts := paginationOptions{
-		equal: reflect.DeepEqual,
-	}
-	for _, o := range options {
-		o(&opts)
-	}
-
-	var items []interface{}
-	rexpected := reflect.ValueOf(expected)
-	for i := 0; i < reflect.ValueOf(expected).Len(); i++ {
-		items = append(items, rexpected.Index(i).Interface())
-	}
-	return testPagination(reflect.ValueOf(request), items, perPage, opts.equal)
-}
-
-func testPagination(request reflect.Value, expectedItems []interface{}, perPage int, equal func(x, y interface{}) bool) error {
-	getPages, getItems := generalizePagination(request)
-
-	var expectedPages [][]interface{}
-	var page []interface{}
-	for _, item := range expectedItems {
-		page = append(page, item)
-		if len(page) == perPage {
-			expectedPages = append(expectedPages, page)
-			page = nil
-		}
-	}
-	if len(page) > 0 {
-		expectedPages = append(expectedPages, page)
-	}
-
-	for i := 0; i < 2; i++ {
-		pages, err := getPages()
-		if err != nil {
-			return fmt.Errorf("calling Pages: %w", err)
-		}
-		var gotPages [][]interface{}
-		for pages.next() {
-			gotPages = append(gotPages, pages.items())
-		}
-		if err := pages.err(); err != nil {
-			return fmt.Errorf("iterating pages: %w", err)
-		}
-
-		if !PagesEqual(expectedPages, gotPages, equal) {
-			return fmt.Errorf("expected pages: %+v, got: %+v", expectedPages, gotPages)
-		}
-
-		if err := pages.first(); err != nil {
-			return fmt.Errorf("going back to first page: %w", err)
-		}
-	}
-
-	for i := 0; i < 2; i++ {
-		items, err := getItems()
-		if err != nil {
-			return fmt.Errorf("calling Items: %w", err)
-		}
-		var gotItems []interface{}
-		for items.next() {
-			gotItems = append(gotItems, items.item())
-		}
-		if err := items.err(); err != nil {
-			return fmt.Errorf("iterating items: %w", err)
-		}
-
-		if !ItemsEqual(expectedItems, gotItems, equal) {
-			return fmt.Errorf("expected items: %+v, got: %+v", expectedItems, gotItems)
-		}
-
-		if err := items.first(); err != nil {
-			return fmt.Errorf("going back to first page: %w", err)
-		}
-	}
-
-	return nil
-}
-
-type paginated struct {
-	next  func() bool
-	first func() error
-	err   func() error
-}
-
-type paginatedResult struct {
-	paginated
-	items func() []interface{}
-}
-
-type paginatedItems struct {
-	paginated
-	item func() interface{}
-}
-
-func generalizePagination(request reflect.Value) (func() (paginatedResult, error), func() (paginatedItems, error)) {
-	ctx := reflect.ValueOf(context.Background())
-
-	generalizeCommon := func(r reflect.Value) paginated {
-		return paginated{
-			next: func() bool {
-				return r.MethodByName("Next").Call([]reflect.Value{ctx})[0].Bool()
-			},
-			first: func() error {
-				err, _ := r.MethodByName("First").Call([]reflect.Value{ctx})[0].Interface().(error)
-				return err
-			},
-			err: func() error {
-				err, _ := r.MethodByName("Err").Call(nil)[0].Interface().(error)
-				return err
-			},
-		}
-	}
-
-	pages := func() (paginatedResult, error) {
-		ret := request.MethodByName("Pages").Call([]reflect.Value{ctx})
-		if err, ok := ret[1].Interface().(error); ok && err != nil {
-			return paginatedResult{}, err
-		}
-		r := ret[0]
-		return paginatedResult{
-			paginated: generalizeCommon(r),
-			items: func() []interface{} {
-				ritems := r.MethodByName("Items").Call(nil)[0]
-				var items []interface{}
-				for i := 0; i < ritems.Len(); i++ {
-					items = append(items, ritems.Index(i).Interface())
-				}
-				return items
-			},
-		}, nil
-	}
-
-	items := func() (paginatedItems, error) {
-		ret := request.MethodByName("Items").Call([]reflect.Value{ctx})
-		if err, ok := ret[1].Interface().(error); ok && err != nil {
-			return paginatedItems{}, err
-		}
-		r := ret[0]
-		return paginatedItems{
-			paginated: generalizeCommon(r),
-			item: func() interface{} {
-				return r.MethodByName("Item").Call(nil)[0].Interface()
-			},
-		}, nil
-	}
-
-	return pages, items
-}
-
-func PagesEqual(expected, got [][]interface{}, equal func(x, y interface{}) bool) bool {
-	if len(expected) != len(got) {
-		return false
-	}
-	for i := range expected {
-		if !ItemsEqual(expected[i], got[i], equal) {
-			return false
-		}
-	}
-	return true
-}
-
-func ItemsEqual(expected, got []interface{}, equal func(x, y interface{}) bool) bool {
-	if len(expected) != len(got) {
-		return false
-	}
-	for i := range expected {
-		if !equal(expected[i], got[i]) {
-			return false
-		}
-	}
-	return true
-}
diff --git a/ably/auth_test.go b/ably/auth_test.go
index a9de5b7..d00a4b3 100644
--- a/ably/auth_test.go
+++ b/ably/auth_test.go
@@ -55,7 +55,7 @@ func TestAuth_BasicAuth(t *testing.T) {
 	if _, err := client.Time(context.Background()); err != nil {
 		t.Fatalf("client.Time()=%v", err)
 	}
-	if _, err := client.Stats().Pages(context.Background()); err != nil {
+	if _, err := client.Stats(context.Background(), single()); err != nil {
 		t.Fatalf("client.Stats()=%v", err)
 	}
 	if n := rec.Len(); n != 2 {
@@ -107,7 +107,7 @@ func TestAuth_TokenAuth(t *testing.T) {
 	if _, err := client.Time(context.Background()); err != nil {
 		t.Fatalf("client.Time()=%v", err)
 	}
-	if _, err := client.Stats().Pages(context.Background()); err != nil {
+	if _, err := client.Stats(context.Background(), single()); err != nil {
 		t.Fatalf("client.Stats()=%v", err)
 	}
 	// At this points there should be two requests recorded:
@@ -262,7 +262,7 @@ func TestAuth_TokenAuth_Renew(t *testing.T) {
 		t.Fatalf("want ttl=1s; got %v", ttl)
 	}
 	time.Sleep(2 * time.Second) // wait till expires
-	_, err = client.Stats().Pages(context.Background())
+	_, err = client.Stats(context.Background(), single())
 	if err != nil {
 		t.Fatalf("Stats()=%v", err)
 	}
@@ -296,7 +296,7 @@ func TestAuth_TokenAuth_Renew(t *testing.T) {
 	if err != nil {
 		t.Fatalf("NewREST()=%v", err)
 	}
-	if _, err := client.Stats().Pages(context.Background()); err == nil {
+	if _, err := client.Stats(context.Background(), single()); err == nil {
 		t.Fatal("want err!=nil")
 	}
 	// Ensure no requests were made to Ably servers.
@@ -460,7 +460,7 @@ func TestAuth_RequestToken(t *testing.T) {
 			t.Errorf("NewRealtime()=%v", err)
 			continue
 		}
-		if _, err = c.Stats().Pages(context.Background()); err != nil {
+		if _, err = c.Stats(context.Background(), single()); err != nil {
 			t.Errorf("c.Stats()=%v (method=%s)", err, method)
 		}
 	}
diff --git a/ably/doc_test.go b/ably/doc_test.go
deleted file mode 100644
index 7a7670b..0000000
--- a/ably/doc_test.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package ably_test
-
-import "fmt"
-
-func Example_paginatedResults() {
-	fmt.Println("TODO once the Requests method is adapted")
-	// Output: TODO once the Requests method is adapted
-}
diff --git a/ably/http_paginated_response_test.go b/ably/http_paginated_response_test.go
index 7ce0e14..02e08e2 100644
--- a/ably/http_paginated_response_test.go
+++ b/ably/http_paginated_response_test.go
@@ -133,11 +133,11 @@ func TestHTTPPaginatedResponse(t *testing.T) {
 
 		})
 
-		var m []*ably.Message
-		err := ablytest.AllPages(&m, client.Channels.Get(channelName).History())
+		res, err := client.Channels.Get(channelName).History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
+		m := res.Messages()
 		sort.Slice(m, func(i, j int) bool {
 			return m[i].Name < m[j].Name
 		})
diff --git a/ably/proto/message_test.go b/ably/proto/message_test.go
index 27386a6..39e70e6 100644
--- a/ably/proto/message_test.go
+++ b/ably/proto/message_test.go
@@ -13,7 +13,7 @@ import (
 type custom struct{}
 
 func (custom) MarshalJSON() ([]byte, error) {
-	return json.Marshal([]string{"custom"})
+	return json.Marshal("custom")
 }
 
 func TestMessage(t *testing.T) {
@@ -63,8 +63,8 @@ func TestMessage(t *testing.T) {
 		{
 			desc:        "with a json encoding RSL4d3 json.Marshaler data",
 			data:        custom{},
-			encodedJSON: `{"data":"[\"custom\"]","encoding":"json"}`,
-			decoded:     []interface{}{"custom"},
+			encodedJSON: `{"data":"\"custom\"","encoding":"json"}`,
+			decoded:     "custom",
 		},
 		{
 			desc:        "with a base64 encoding RSL4d3 binary data",
@@ -88,34 +88,29 @@ func TestMessage(t *testing.T) {
 	for _, v := range sample {
 		// pin
 		v := v
-		t.Run(v.desc, func(t *testing.T) {
-			cipher, _ := v.opts.GetCipher()
-			msg, err := proto.Message{
-				Data: v.data,
-			}.WithEncodedData(cipher)
-			if err != nil {
-				t.Fatal(err)
+		t.Run(v.desc, func(ts *testing.T) {
+			msg := &proto.Message{
+				Data:           v.data,
+				ChannelOptions: v.opts,
 			}
 			b, err := json.Marshal(msg)
 			if err != nil {
-				t.Fatal(err)
+				ts.Fatal(err)
 			}
 			got := string(b)
 			if got != v.encodedJSON {
-				t.Errorf("expected %s got %s", v.encodedJSON, got)
+				ts.Errorf("expected %s got %s", v.encodedJSON, got)
 			}
 
-			var encoded proto.Message
-			err = json.Unmarshal(b, &encoded)
-			if err != nil {
-				t.Fatal(err)
+			decoded := &proto.Message{
+				ChannelOptions: v.opts,
 			}
-			decoded, err := encoded.WithDecodedData(cipher)
+			err = decoded.UnmarshalJSON(b)
 			if err != nil {
-				t.Fatal(err)
+				ts.Fatal(err)
 			}
 			if !reflect.DeepEqual(decoded.Data, v.decoded) {
-				t.Errorf("expected %#v got %#v", v.decoded, decoded.Data)
+				ts.Errorf("expected %#v got %v", v.decoded, decoded.Data)
 			}
 		})
 	}
@@ -133,10 +128,10 @@ func TestMessage_CryptoDataFixtures_RSL6a1_RSL5b_RSL5c(t *testing.T) {
 	for _, fixture := range fixtures {
 		// pin
 		fixture := fixture
-		t.Run(fixture.desc, func(t *testing.T) {
+		t.Run(fixture.desc, func(ts *testing.T) {
 			test, key, iv, err := ablytest.LoadCryptoData(fixture.file)
 			if err != nil {
-				t.Fatal(err)
+				ts.Fatal(err)
 			}
 			opts := &proto.ChannelOptions{
 				Cipher: proto.CipherParams{
@@ -145,35 +140,26 @@ func TestMessage_CryptoDataFixtures_RSL6a1_RSL5b_RSL5c(t *testing.T) {
 					IV:        iv,
 				},
 			}
-			t.Run("fixture encode", func(t *testing.T) {
+			ts.Run("fixture encode", func(ts *testing.T) {
 				for _, item := range test.Items {
-					cipher, _ := opts.GetCipher()
-
-					var encoded proto.Message
-					err := json.Unmarshal(item.Encoded, &encoded)
+					// All test-cases from the common fixtures files are encoded
+					// for binary transports. Decode the input message first,
+					// to ensure we're not encrypting base64d payloads.
+					encoded := &proto.Message{ChannelOptions: opts}
+					err := encoded.FromMap(item.Encoded)
 					if err != nil {
-						t.Fatal(err)
+						ts.Fatal(err)
 					}
-					encoded, err = encoded.WithDecodedData(cipher)
+					encrypted := &proto.Message{ChannelOptions: opts}
+					err = encrypted.FromMap(item.Encrypted)
 					if err != nil {
-						t.Fatal(err)
+						ts.Fatal(err)
 					}
-
-					var encrypted proto.Message
-					err = json.Unmarshal(item.Encoded, &encrypted)
-					if err != nil {
-						t.Fatal(err)
-					}
-					encrypted, err = encrypted.WithDecodedData(cipher)
-					if err != nil {
-						t.Fatal(err)
-					}
-
 					if encoded.Name != encrypted.Name {
-						t.Errorf("expected %s got %s", encoded.Name, encrypted.Name)
+						ts.Errorf("expected %s got %s", encoded.Name, encrypted.Name)
 					}
 					if !reflect.DeepEqual(encoded.Data, encrypted.Data) {
-						t.Errorf("expected %s got %s :encoding %s",
+						ts.Errorf("expected %s got %s :encoding %s",
 							encoded.Data, encrypted.Data, encoded.Encoding)
 					}
 				}
diff --git a/ably/readme_examples_test.go b/ably/readme_examples_test.go
index 33b30cf..5caad05 100644
--- a/ably/readme_examples_test.go
+++ b/ably/readme_examples_test.go
@@ -170,78 +170,54 @@ func TestReadmeExamples(t *testing.T) {
 			/* README.md:240 */ panic(err)
 			/* README.md:241 */
 		}
-		/* README.md:247 */ {
-			/* README.md:251 */ pages, err := channel.History().Pages(ctx)
-			/* README.md:252 */ if err != nil {
-				/* README.md:253 */ panic(err)
-				/* README.md:254 */
+		/* README.md:247 */ page, err := channel.History(ctx, nil)
+		/* README.md:248 */ for ; err == nil && page != nil; page, err = page.Next(ctx) {
+			/* README.md:249 */ for _, message := range page.Messages() {
+				/* README.md:250 */ fmt.Println(message)
+				/* README.md:251 */
 			}
-			/* README.md:255 */ for pages.Next(ctx) {
-				/* README.md:256 */ for _, message := range pages.Items() {
-					/* README.md:257 */ fmt.Println(message)
-					/* README.md:258 */
-				}
-				/* README.md:259 */
-			}
-			/* README.md:260 */ if err := pages.Err(); err != nil {
-				/* README.md:261 */ panic(err)
-				/* README.md:262 */
-			}
-			/* README.md:267 */
+			/* README.md:252 */
 		}
-		/* README.md:273 */ {
-			/* README.md:277 */ page, err := channel.Presence.Get(ctx, nil)
-			/* README.md:278 */ for ; err == nil && page != nil; page, err = page.Next(ctx) {
-				/* README.md:279 */ for _, presence := range page.PresenceMessages() {
-					/* README.md:280 */ fmt.Println(presence)
-					/* README.md:281 */
-				}
-				/* README.md:282 */
-			}
-			/* README.md:283 */ if err != nil {
-				/* README.md:284 */ panic(err)
-				/* README.md:285 */
-			}
-			/* README.md:289 */
+		/* README.md:253 */ if err != nil {
+			/* README.md:254 */ panic(err)
+			/* README.md:255 */
 		}
-		/* README.md:295 */ {
-			/* README.md:299 */ pages, err := channel.Presence.History().Pages(ctx)
-			/* README.md:300 */ if err != nil {
-				/* README.md:301 */ panic(err)
-				/* README.md:302 */
-			}
-			/* README.md:303 */ for pages.Next(ctx) {
-				/* README.md:304 */ for _, presence := range pages.Items() {
-					/* README.md:305 */ fmt.Println(presence)
-					/* README.md:306 */
-				}
-				/* README.md:307 */
-			}
-			/* README.md:308 */ if err := pages.Err(); err != nil {
-				/* README.md:309 */ panic(err)
-				/* README.md:310 */
+		/* README.md:261 */ page, err = channel.Presence.Get(ctx, nil)
+		/* README.md:262 */ for ; err == nil && page != nil; page, err = page.Next(ctx) {
+			/* README.md:263 */ for _, presence := range page.PresenceMessages() {
+				/* README.md:264 */ fmt.Println(presence)
+				/* README.md:265 */
 			}
-			/* README.md:314 */
+			/* README.md:266 */
 		}
-		/* README.md:320 */ {
-			/* README.md:324 */ pages, err := client.Stats().Pages(ctx)
-			/* README.md:325 */ if err != nil {
-				/* README.md:326 */ panic(err)
-				/* README.md:327 */
-			}
-			/* README.md:328 */ for pages.Next(ctx) {
-				/* README.md:329 */ for _, stat := range pages.Items() {
-					/* README.md:330 */ fmt.Println(stat)
-					/* README.md:331 */
-				}
-				/* README.md:332 */
+		/* README.md:267 */ if err != nil {
+			/* README.md:268 */ panic(err)
+			/* README.md:269 */
+		}
+		/* README.md:275 */ page, err = channel.Presence.History(ctx, nil)
+		/* README.md:276 */ for ; err == nil && page != nil; page, err = page.Next(ctx) {
+			/* README.md:277 */ for _, presence := range page.PresenceMessages() {
+				/* README.md:278 */ fmt.Println(presence)
+				/* README.md:279 */
 			}
-			/* README.md:333 */ if err := pages.Err(); err != nil {
-				/* README.md:334 */ panic(err)
-				/* README.md:335 */
+			/* README.md:280 */
+		}
+		/* README.md:281 */ if err != nil {
+			/* README.md:282 */ panic(err)
+			/* README.md:283 */
+		}
+		/* README.md:289 */ page, err = client.Stats(ctx, &ably.PaginateParams{})
+		/* README.md:290 */ for ; err == nil && page != nil; page, err = page.Next(ctx) {
+			/* README.md:291 */ for _, stat := range page.Stats() {
+				/* README.md:292 */ fmt.Println(stat)
+				/* README.md:293 */
 			}
-			/* README.md:339 */
+			/* README.md:294 */
+		}
+		/* README.md:295 */ if err != nil {
+			/* README.md:296 */ panic(err)
+			/* README.md:297 */
 		}
-		/* README.md:343 */
+		/* README.md:301 */
 	}
 }
diff --git a/ably/rest_channel_test.go b/ably/rest_channel_test.go
index c280b0c..e77a673 100644
--- a/ably/rest_channel_test.go
+++ b/ably/rest_channel_test.go
@@ -44,8 +44,7 @@ func TestRESTChannel(t *testing.T) {
 		m := make(map[string]dataSample)
 		if ablytest.NoBinaryProtocol {
 			m["string"] = dataSample{
-				encoding: proto.UTF8,
-				data:     "string",
+				data: "string",
 			}
 			m["binary"] = dataSample{
 				encoding: proto.Base64,
@@ -59,12 +58,10 @@ func TestRESTChannel(t *testing.T) {
 			}
 		} else {
 			m["string"] = dataSample{
-				encoding: proto.UTF8,
-				data:     "string",
+				data: "string",
 			}
 			m["binary"] = dataSample{
-				encoding: proto.UTF8,
-				data:     "string",
+				data: "string",
 			}
 			m["json"] = dataSample{
 				encoding: proto.JSON,
@@ -80,17 +77,52 @@ func TestRESTChannel(t *testing.T) {
 			}
 		}
 		ts.Run("is available in the history", func(ts *testing.T) {
-			var messages []*ably.Message
-			err := ablytest.AllPages(&messages, channel.History())
+			page, err := channel.History(context.Background(), nil)
 			if err != nil {
 				ts.Fatal(err)
 			}
+			messages := page.Messages()
 			if len(messages) == 0 {
 				ts.Fatal("expected messages")
 			}
+			for _, v := range messages {
+				e := m[v.Name]
+				if v.Encoding != e.encoding {
+					ts.Errorf("expected %s got %s ", e.encoding, v.Encoding)
+				}
+			}
 		})
 	})
 
+	t.Run("History", func(ts *testing.T) {
+		historyRESTChannel := client.Channels.Get("channelhistory")
+		for i := 0; i < 2; i++ {
+			historyRESTChannel.Publish(context.Background(), "breakingnews", "Another Shark attack!!")
+		}
+
+		page1, err := historyRESTChannel.History(context.Background(), &ably.PaginateParams{Limit: 1})
+		if err != nil {
+			ts.Fatal(err)
+		}
+		if len(page1.Messages()) != 1 {
+			ts.Errorf("expected 1 message got %d", len(page1.Messages()))
+		}
+		if len(page1.Items()) != 1 {
+			ts.Errorf("expected 1 item got %d", len(page1.Items()))
+		}
+
+		page2, err := page1.Next(context.Background())
+		if err != nil {
+			ts.Fatal(err)
+		}
+		if len(page2.Messages()) != 1 {
+			ts.Errorf("expected 1 message got %d", len(page2.Messages()))
+		}
+		if len(page2.Items()) != 1 {
+			ts.Errorf("expected 1 item got %d", len(page2.Items()))
+		}
+	})
+
 	t.Run("PublishBatch", func(ts *testing.T) {
 		encodingRESTChannel := client.Channels.Get("this?is#an?encoding#channel")
 		messages := []*ably.Message{
@@ -101,24 +133,26 @@ func TestRESTChannel(t *testing.T) {
 		if err != nil {
 			ts.Fatal(err)
 		}
-		var history []*ably.Message
-		err = ablytest.AllPages(&history, encodingRESTChannel.History(ably.HistoryWithLimit(2)))
+		page, err := encodingRESTChannel.History(context.Background(), &ably.PaginateParams{Limit: 2})
 		if err != nil {
 			ts.Fatal(err)
 		}
-		if len(history) != 2 {
-			ts.Errorf("expected 2 messages got %d", len(history))
+		if len(page.Messages()) != 2 {
+			ts.Errorf("expected 2 messages got %d", len(page.Messages()))
+		}
+		if len(page.Items()) != 2 {
+			ts.Errorf("expected 2 items got %d", len(page.Items()))
 		}
 	})
 
-	t.Run("encryption", func(t *testing.T) {
+	t.Run("encryption", func(ts *testing.T) {
 		key, err := base64.StdEncoding.DecodeString("WUP6u0K7MXI5Zeo0VppPwg==")
 		if err != nil {
-			t.Fatal(err)
+			ts.Fatal(err)
 		}
 		iv, err := base64.StdEncoding.DecodeString("HO4cYSP8LybPYBPZPHQOtg==")
 		if err != nil {
-			t.Fatal(err)
+			ts.Fatal(err)
 		}
 		opts := []ably.ChannelOption{ably.ChannelWithCipher(proto.CipherParams{
 			Key:       key,
@@ -137,14 +171,17 @@ func TestRESTChannel(t *testing.T) {
 		for _, v := range sample {
 			err := channel.Publish(context.Background(), v.event, v.message)
 			if err != nil {
-				t.Error(err)
+				ts.Error(err)
 			}
 		}
 
-		var msg []*ably.Message
-		err = ablytest.AllPages(&msg, channel.History())
+		rst, err := channel.History(context.Background(), nil)
 		if err != nil {
-			t.Fatal(err)
+			ts.Fatal(err)
+		}
+		msg := rst.Messages()
+		if len(msg) != len(sample) {
+			t.Errorf("expected %d messages got %d", len(sample), len(msg))
 		}
 		sort.Slice(msg, func(i, j int) bool {
 			return msg[i].Name < msg[j].Name
@@ -186,12 +223,11 @@ func TestIdempotentPublishing(t *testing.T) {
 				ts.Fatal(err)
 			}
 		}
-		var history []*ably.Message
-		err := ablytest.AllPages(&history, channel.History())
+		res, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		n := len(history)
+		n := len(res.Items())
 		if n != 3 {
 			ts.Errorf("expected %d got %d", 3, n)
 		}
@@ -210,17 +246,16 @@ func TestIdempotentPublishing(t *testing.T) {
 				ts.Fatal(err)
 			}
 		}
-		var history []*ably.Message
-		err := ablytest.AllPages(&history, channel.History())
+		res, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		n := len(history)
+		n := len(res.Items())
 		if n != 1 {
 			// three REST publishes result in only one message being published
 			ts.Errorf("expected %d got %d", 1, n)
 		}
-		msg := history[0]
+		msg := res.Messages()[0]
 		if msg.ID != randomStr {
 			ts.Errorf("expected id to be %s got %s", randomStr, msg.ID)
 		}
@@ -263,15 +298,15 @@ func TestIdempotentPublishing(t *testing.T) {
 		if err != nil {
 			ts.Fatal(err)
 		}
-		var messages []*ably.Message
-		err = ablytest.AllPages(&messages, channel.History())
+		res, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		n := len(messages)
+		n := len(res.Items())
 		if n != 3 {
 			ts.Errorf("expected %d got %d", 3, n)
 		}
+		messages := res.Messages()
 
 		// we need to sort so we we can easily test the serial in order.
 		sort.Slice(messages, func(i, j int) bool {
@@ -305,11 +340,11 @@ func TestIdempotentPublishing(t *testing.T) {
 		if err != nil {
 			ts.Fatal(err)
 		}
-		var m []*ably.Message
-		err = ablytest.AllPages(&m, channel.History())
+		res, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
+		m := res.Messages()
 		if len(m) != 1 {
 			ts.Fatalf("expected %d got %d", 1, len(m))
 		}
@@ -343,11 +378,11 @@ func TestIdempotentPublishing(t *testing.T) {
 		if err != nil {
 			ts.Fatal(err)
 		}
-		var m []*ably.Message
-		err = ablytest.AllPages(&m, channel.History())
+		res, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
+		m := res.Messages()
 		if len(m) != 3 {
 			ts.Fatalf("expected %d got %d", 1, len(m))
 		}
@@ -442,11 +477,11 @@ func TestIdempotent_retry(t *testing.T) {
 			if retryCount != 2 {
 				ts.Errorf("expected %d retry attempts got %d", 2, retryCount)
 			}
-			var m []*ably.Message
-			err = ablytest.AllPages(&m, channel.History())
+			res, err := channel.History(context.Background(), nil)
 			if err != nil {
 				ts.Fatal(err)
 			}
+			m := res.Messages()
 			if len(m) != 1 {
 				ts.Errorf("expected %d messages got %d", 1, len(m))
 			}
@@ -462,11 +497,11 @@ func TestIdempotent_retry(t *testing.T) {
 			for range msgs {
 				channel.PublishBatch(context.Background(), msgs)
 			}
-			var m []*ably.Message
-			err := ablytest.AllPages(&m, channel.History())
+			res, err := channel.History(context.Background(), nil)
 			if err != nil {
 				ts.Fatal(err)
 			}
+			m := res.Messages()
 			if len(m) != len(msgs) {
 				ts.Errorf("expected %d messages got %d", len(msgs), len(m))
 			}
@@ -502,11 +537,11 @@ func TestRSL1f1(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	var m []*ably.Message
-	err = ablytest.AllPages(&m, channel.History())
+	res, err := channel.History(context.Background(), nil)
 	if err != nil {
 		t.Fatal(err)
 	}
+	m := res.Messages()
 	n := len(m)
 	if n != size {
 		t.Errorf("expected %d messages got %d", size, n)
@@ -544,12 +579,15 @@ func TestRSL1g(t *testing.T) {
 		if err != nil {
 			ts.Fatal(err)
 		}
-		var history []*ably.Message
-		err = ablytest.AllPages(&history, channel.History())
+		pages, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		for _, m := range history {
+		msg := pages.Messages()
+		if len(msg) != 3 {
+			ts.Fatalf("expected 3 messages got %d", len(msg))
+		}
+		for _, m := range msg {
 			if m.ClientID != clientID {
 				ts.Errorf("expected %s got %s", clientID, m.ClientID)
 			}
@@ -565,12 +603,15 @@ func TestRSL1g(t *testing.T) {
 		if err != nil {
 			ts.Fatal(err)
 		}
-		var history []*ably.Message
-		err = ablytest.AllPages(&history, channel.History())
+		pages, err := channel.History(context.Background(), nil)
 		if err != nil {
 			ts.Fatal(err)
 		}
-		for _, m := range history {
+		msg := pages.Messages()
+		if len(msg) != 3 {
+			ts.Fatalf("expected 3 messages got %d", len(msg))
+		}
+		for _, m := range msg {
 			if m.ClientID != clientID {
 				ts.Errorf("expected %s got %s", clientID, m.ClientID)
 			}
@@ -588,88 +629,3 @@ func TestRSL1g(t *testing.T) {
 		}
 	})
 }
-
-func TestHistory_RSL2_RSL2b3(t *testing.T) {
-	t.Parallel()
-
-	for _, limit := range []int{2, 3, 20} {
-		t.Run(fmt.Sprintf("limit=%d", limit), func(t *testing.T) {
-			t.Parallel()
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-			channel := rest.Channels.Get("test")
-
-			fixtures := historyFixtures()
-			channel.PublishBatch(context.Background(), fixtures)
-
-			err := ablytest.TestPagination(
-				reverseMessages(fixtures),
-				channel.History(ably.HistoryWithLimit(limit)),
-				limit,
-				ablytest.PaginationWithEqual(messagesEqual),
-			)
-			if err != nil {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func TestHistory_Direction_RSL2b2(t *testing.T) {
-	t.Parallel()
-
-	for _, c := range []struct {
-		direction ably.Direction
-		expected  []*ably.Message
-	}{
-		{
-			direction: ably.Backwards,
-			expected:  reverseMessages(historyFixtures()),
-		},
-		{
-			direction: ably.Forwards,
-			expected:  historyFixtures(),
-		},
-	} {
-		c := c
-		t.Run(fmt.Sprintf("direction=%v", c.direction), func(t *testing.T) {
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-			channel := rest.Channels.Get("test")
-
-			fixtures := historyFixtures()
-			channel.PublishBatch(context.Background(), fixtures)
-
-			expected := c.expected
-
-			err := ablytest.TestPagination(expected, channel.History(
-				ably.HistoryWithLimit(len(expected)),
-				ably.HistoryWithDirection(c.direction),
-			), 100, ablytest.PaginationWithEqual(messagesEqual))
-			if err != nil {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func historyFixtures() []*ably.Message {
-	var fixtures []*ably.Message
-	for i := 0; i < 10; i++ {
-		fixtures = append(fixtures, &ably.Message{Name: fmt.Sprintf("msg%d", i)})
-	}
-	return fixtures
-}
-
-func reverseMessages(msgs []*ably.Message) []*ably.Message {
-	var reversed []*ably.Message
-	for i := len(msgs) - 1; i >= 0; i-- {
-		reversed = append(reversed, msgs[i])
-	}
-	return reversed
-}
-
-func messagesEqual(x, y interface{}) bool {
-	mx, my := x.(*ably.Message), y.(*ably.Message)
-	return mx.Name == my.Name && reflect.DeepEqual(mx.Data, my.Data)
-}
diff --git a/ably/rest_client_test.go b/ably/rest_client_test.go
index 175c5c2..f490bdf 100644
--- a/ably/rest_client_test.go
+++ b/ably/rest_client_test.go
@@ -1,7 +1,6 @@
 package ably_test
 
 import (
-	"bytes"
 	"context"
 	"crypto/tls"
 	"encoding/json"
@@ -12,7 +11,6 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"net/url"
-	"reflect"
 	"regexp"
 	"strings"
 	"sync/atomic"
@@ -203,20 +201,19 @@ func TestRestClient(t *testing.T) {
 					errCh <- errors.New("timeout waiting for client.Stats to return nonempty value")
 					return
 				case <-tick:
-					page, err := client.Stats(
-						ably.StatsWithLimit(1),
-						ably.StatsWithStart(longAgo),
-						ably.StatsWithUnit(ably.PeriodMinute),
-					).Pages(context.Background())
+					page, err := client.Stats(context.Background(), &ably.PaginateParams{
+						Limit: 1,
+						ScopeParams: ably.ScopeParams{
+							Start: longAgo,
+							Unit:  proto.StatGranularityMinute,
+						},
+					})
 					if err != nil {
 						errCh <- err
 						return
 					}
-					if !page.Next(context.Background()) {
-						errCh <- page.Err()
-						return
-					}
-					if stats := page.Items(); len(stats) != 0 {
+
+					if stats := page.Stats(); len(stats) != 0 {
 						statsCh <- stats
 						return
 					}
@@ -598,202 +595,3 @@ func (c *connCloseTracker) Close() error {
 	atomic.StoreUintptr(&c.closed, 1)
 	return c.Conn.Close()
 }
-
-func TestStatsPagination_RSC6a_RSCb3(t *testing.T) {
-	t.Parallel()
-
-	for _, limit := range []int{2, 3, 20} {
-		t.Run(fmt.Sprintf("limit=%d", limit), func(t *testing.T) {
-			t.Parallel()
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-
-			fixtures := statsFixtures()
-			postStats(app, fixtures)
-
-			err := ablytest.TestPagination(
-				reverseStats(fixtures),
-				rest.Stats(ably.StatsWithLimit(limit)),
-				limit,
-			)
-			if err != nil {
-				t.Fatal(err)
-			}
-		})
-	}
-}
-
-func TestStats_StartEnd_RSC6b1(t *testing.T) {
-	t.Parallel()
-
-	ctx := context.Background()
-
-	app, rest := ablytest.NewREST()
-	defer app.Close()
-
-	fixtures := statsFixtures()
-	postStats(app, fixtures)
-
-	expected := reverseStats(fixtures[1:3])
-
-	pages, err := rest.Stats(
-		ably.StatsWithStart(time.Date(2020, time.January, 28, 14, 1, 0, 0, time.UTC)),
-		ably.StatsWithEnd(time.Date(2020, time.January, 28, 14, 2, 30, 0, time.UTC)),
-	).Pages(ctx)
-	if err != nil {
-		t.Fatal(err)
-	}
-	var got []*ably.Stats
-	for pages.Next(ctx) {
-		got = append(got, pages.Items()...)
-	}
-	if err := pages.Err(); err != nil {
-		t.Fatal(err)
-	}
-
-	if !reflect.DeepEqual(expected, got) {
-		t.Fatalf("expected: %+v; got: %+v", expected, got)
-	}
-}
-
-func TestStats_Direction_RSC6b2(t *testing.T) {
-	t.Parallel()
-
-	for _, c := range []struct {
-		direction ably.Direction
-		expected  []*ably.Stats
-	}{
-		{
-			direction: ably.Backwards,
-			expected:  reverseStats(statsFixtures()),
-		},
-		{
-			direction: ably.Forwards,
-			expected:  statsFixtures(),
-		},
-	} {
-		c := c
-		t.Run(fmt.Sprintf("direction=%v", c.direction), func(t *testing.T) {
-			t.Parallel()
-			ctx := context.Background()
-
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-
-			fixtures := statsFixtures()
-			postStats(app, fixtures)
-
-			expected := c.expected
-
-			pages, err := rest.Stats(
-				ably.StatsWithLimit(len(expected)),
-				ably.StatsWithDirection(c.direction),
-			).Pages(ctx)
-			if err != nil {
-				t.Fatal(err)
-			}
-			var got []*ably.Stats
-			for pages.Next(ctx) {
-				got = append(got, pages.Items()...)
-			}
-			if err := pages.Err(); err != nil {
-				t.Fatal(err)
-			}
-
-			if !reflect.DeepEqual(expected, got) {
-				t.Fatalf("expected: %+v; got: %+v", expected, got)
-			}
-		})
-	}
-}
-
-func TestStats_Unit_RSC6b4(t *testing.T) {
-	t.Parallel()
-
-	ctx := context.Background()
-
-	app, rest := ablytest.NewREST()
-	defer app.Close()
-
-	fixtures := statsFixtures()
-	postStats(app, fixtures)
-
-	pages, err := rest.Stats(
-		ably.StatsWithUnit(ably.PeriodMonth),
-	).Pages(ctx)
-	if err != nil {
-		t.Fatal(err)
-	}
-	var got []*ably.Stats
-	for pages.Next(ctx) {
-		got = append(got, pages.Items()...)
-	}
-	if err := pages.Err(); err != nil {
-		t.Fatal(err)
-	}
-
-	if expected, got := 1, len(got); expected != got {
-		t.Fatalf("expected: %v; got: %v", expected, got)
-	}
-
-	stats := got[0]
-	if expected, got := "month", stats.Unit; expected != got {
-		t.Fatalf("expected: %v; got: %v", expected, got)
-	}
-}
-
-func statsFixtures() []*ably.Stats {
-	var fixtures []*ably.Stats
-	baseDate := time.Date(2020, time.January, 28, 14, 0, 0, 0, time.UTC)
-	msgCounts := ably.StatsMessageCount{
-		Count: 50,
-		Data:  5000,
-	}
-	msgTypes := ably.StatsMessageTypes{
-		All:      msgCounts,
-		Messages: msgCounts,
-	}
-	for i := time.Duration(0); i < 10; i++ {
-		fixtures = append(fixtures, &ably.Stats{
-			IntervalID: baseDate.Add(i * time.Minute).Format("2006-01-02:15:04"),
-			Unit:       "minute",
-			All:        msgTypes,
-			Inbound: ably.StatsMessageTraffic{
-				All:      msgTypes,
-				RealTime: msgTypes,
-			},
-		})
-	}
-	return fixtures
-}
-
-func postStats(app *ablytest.Sandbox, stats []*ably.Stats) error {
-	ctx, cancel := context.WithTimeout(context.Background(), ablytest.Timeout)
-	defer cancel()
-
-	statsJSON, err := json.Marshal(stats)
-	if err != nil {
-		return fmt.Errorf("marshaling stats: %w", err)
-	}
-
-	req, err := http.NewRequest("POST", "https://sandbox-rest.ably.io/stats", bytes.NewReader(statsJSON))
-	if err != nil {
-		return fmt.Errorf("creating request: %w", err)
-	}
-	req = req.WithContext(ctx)
-	req.SetBasicAuth(app.KeyParts())
-	resp, err := http.DefaultClient.Do(req)
-	if err != nil {
-		return fmt.Errorf("performing request: %w", err)
-	}
-	resp.Body.Close()
-	return nil
-}
-
-func reverseStats(stats []*ably.Stats) []*ably.Stats {
-	var reversed []*ably.Stats
-	for i := len(stats) - 1; i >= 0; i-- {
-		reversed = append(reversed, stats[i])
-	}
-	return reversed
-}
diff --git a/ably/rest_presence_test.go b/ably/rest_presence_test.go
index 022b87b..a368db8 100644
--- a/ably/rest_presence_test.go
+++ b/ably/rest_presence_test.go
@@ -2,14 +2,11 @@ package ably_test
 
 import (
 	"context"
-	"fmt"
-	"io"
-	"reflect"
 	"testing"
+	"time"
 
 	"github.com/ably/ably-go/ably"
 	"github.com/ably/ably-go/ably/ablytest"
-	"github.com/ably/ably-go/ably/proto"
 )
 
 func TestChannel_Presence(t *testing.T) {
@@ -74,133 +71,35 @@ func TestChannel_Presence(t *testing.T) {
 			}
 		})
 	})
-}
-
-func TestPresenceHistory_RSP4_RSP4b3(t *testing.T) {
-	t.Parallel()
 
-	for _, limit := range []int{2, 3, 20} {
-		t.Run(fmt.Sprintf("limit=%d", limit), func(t *testing.T) {
-			t.Parallel()
-
-			app, rest := ablytest.NewREST()
-			defer app.Close()
-			channel := rest.Channels.Get("test")
-
-			fixtures := presenceHistoryFixtures()
-			realtime := postPresenceHistoryFixtures(context.Background(), app, "test", fixtures)
-			defer safeclose(t, realtime, app)
+	t.Run("History", func(ts *testing.T) {
+		page, err := presence.History(context.Background(), nil)
+		if err != nil {
+			ts.Fatal(err)
+		}
+		n := len(page.PresenceMessages())
+		expect := len(app.Config.Channels[0].Presence)
+		if n != expect {
+			ts.Errorf("expected %d got %d", expect, n)
+		}
 
-			var err error
-			if !ablytest.Soon.IsTrue(func() bool {
-				err = ablytest.TestPagination(
-					reversePresence(fixtures),
-					channel.Presence.History(ably.PresenceHistoryWithLimit(limit)),
-					limit,
-					ablytest.PaginationWithEqual(presenceEqual),
-				)
-				return err == nil
-			}) {
-				t.Fatal(err)
+		ts.Run("with start and end time", func(ts *testing.T) {
+			params := &ably.PaginateParams{
+				ScopeParams: ably.ScopeParams{
+					Start: time.Now().Add(-24 * time.Hour),
+					End:   time.Now(),
+				},
 			}
-		})
-	}
-}
-
-func TestPresenceHistory_Direction_RSP4b2(t *testing.T) {
-	t.Parallel()
-
-	for _, c := range []struct {
-		direction ably.Direction
-		expected  []*ably.PresenceMessage
-	}{
-		{
-			direction: ably.Backwards,
-			expected:  reversePresence(presenceHistoryFixtures()),
-		},
-		{
-			direction: ably.Forwards,
-			expected:  presenceHistoryFixtures(),
-		},
-	} {
-		c := c
-		t.Run(fmt.Sprintf("direction=%v", c.direction), func(t *testing.T) {
-			t.Parallel()
-
-			app, rest := ablytest.NewREST()
-			channel := rest.Channels.Get("test")
-
-			fixtures := presenceHistoryFixtures()
-			realtime := postPresenceHistoryFixtures(context.Background(), app, "test", fixtures)
-			defer safeclose(t, realtime, app)
-
-			expected := c.expected
-
-			var err error
-			if !ablytest.Soon.IsTrue(func() bool {
-				err = ablytest.TestPagination(expected, channel.Presence.History(
-					ably.PresenceHistoryWithLimit(len(expected)),
-					ably.PresenceHistoryWithDirection(c.direction),
-				), len(expected), ablytest.PaginationWithEqual(presenceEqual))
-				return err == nil
-			}) {
-				t.Fatal(err)
+			page, err := presence.History(context.Background(), params)
+			if err != nil {
+				ts.Fatal(err)
+			}
+			n := len(page.PresenceMessages())
+			expect := len(app.Config.Channels[0].Presence)
+			if n != expect {
+				ts.Errorf("expected %d got %d", expect, n)
 			}
 		})
-	}
-}
-
-func presenceHistoryFixtures() []*ably.PresenceMessage {
-	actions := []proto.PresenceAction{
-		proto.PresenceEnter,
-		proto.PresenceUpdate,
-		proto.PresenceLeave,
-	}
-	var fixtures []*ably.PresenceMessage
-	for i := 0; i < 10; i++ {
-		for j, action := range actions[:i%3+1] {
-			fixtures = append(fixtures, &ably.PresenceMessage{
-				Action: action,
-				Message: ably.Message{
-					Data:     fmt.Sprintf("msg%d.%d", i, j),
-					ClientID: fmt.Sprintf("client%d", i),
-				},
-			})
-		}
-	}
-	return fixtures
-}
-
-func postPresenceHistoryFixtures(ctx context.Context, app *ablytest.Sandbox, channel string, fixtures []*ably.PresenceMessage) io.Closer {
-	realtime := app.NewRealtime()
-	p := realtime.Channels.Get(channel).Presence
-
-	for _, m := range fixtures {
-		switch m.Action {
-		case proto.PresenceEnter:
-			p.EnterClient(ctx, m.ClientID, m.Data)
-		case proto.PresenceUpdate:
-			p.UpdateClient(ctx, m.ClientID, m.Data)
-		case proto.PresenceLeave:
-			p.LeaveClient(ctx, m.ClientID, m.Data)
-		}
-	}
-
-	return ablytest.FullRealtimeCloser(realtime)
-}
-
-func reversePresence(msgs []*ably.PresenceMessage) []*ably.PresenceMessage {
-	var reversed []*ably.PresenceMessage
-	for i := len(msgs) - 1; i >= 0; i-- {
-		reversed = append(reversed, msgs[i])
-	}
-	return reversed
-}
+	})
 
-func presenceEqual(x, y interface{}) bool {
-	mx, my := x.(*ably.PresenceMessage), y.(*ably.PresenceMessage)
-	return mx.Action == my.Action &&
-		mx.ClientID == my.ClientID &&
-		mx.Name == my.Name &&
-		reflect.DeepEqual(mx.Data, my.Data)
 }