Repository: prebid/prebid-server
Pull request #1413 (2020-07-23 09:35:34)

New bid adapter for Smaato
### Type of change
 

- [X] New bidder adapter added for Smaato
- [X] Doc smaato.md added
- [X] ./validate.sh --nofmt --cov --race 10 .... passed
- [X] code coverage 92.7%

### Description of change

Add a new bidder adapter for Smaato (https://www.smaato.com/)

 ###    test parameters for validating bids

```
"imp":[
      {
         "id":“1C86242D-9535-47D6-9576-7B1FE87F282C,
         "banner":{
            "format":[
               {
                  "w":300,
                  "h":50
               },
               {
                  "w":300,
                  "h":250
               }
            ]
         },
         "ext":{
            "smaato":{
               "publisherId":"100042525",
               "adspaceId":"130563103"
            }
         }
      }
   ]
```

- contact email of the adapter’s maintainer: prebid@smaato.com

- official adapter submission [X]

### Other information

- The adapter requires setup and approval from the Smaato team. The configured endpoint is reachable, but will probably return a no-bid (HTTP 204).

- This adapter supports configured first-party data (value will be copied to the user/site nodes of the outgoing bid requests)

- Video and user sync support is planned but not available yet.



Patch:
diff --git a/adapters/openx/openx.go b/adapters/openx/openx.go
index ca88b18b..c2a42adf 100644
--- a/adapters/openx/openx.go
+++ b/adapters/openx/openx.go
@@ -22,7 +22,8 @@ type openxImpExt struct {
 }
 
 type openxReqExt struct {
-	DelDomain    string `json:"delDomain"`
+	DelDomain    string `json:"delDomain,omitempty"`
+	Platform     string `json:"platform,omitempty"`
 	BidderConfig string `json:"bc"`
 }
 
@@ -125,6 +126,7 @@ func preprocess(imp *openrtb.Imp, reqExt *openxReqExt) error {
 	}
 
 	reqExt.DelDomain = openxExt.DelDomain
+	reqExt.Platform = openxExt.Platform
 
 	imp.TagID = openxExt.Unit
 	imp.BidFloor = openxExt.CustomFloor
diff --git a/adapters/smaato/image.go b/adapters/smaato/image.go
new file mode 100644
index 00000000..582206cc
--- /dev/null
+++ b/adapters/smaato/image.go
@@ -0,0 +1,53 @@
+package smaato
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strings"
+)
+
+type imageAd struct {
+	Image image `json:"image"`
+}
+type image struct {
+	Img                img      `json:"img"`
+	Impressiontrackers []string `json:"impressiontrackers"`
+	Clicktrackers      []string `json:"clicktrackers"`
+}
+type img struct {
+	URL    string `json:"url"`
+	W      int    `json:"w"`
+	H      int    `json:"h"`
+	Ctaurl string `json:"ctaurl"`
+}
+
+func extractAdmImage(adapterResponseAdm string) (string, error) {
+	var imgMarkup string
+	var err error
+
+	var imageAd imageAd
+	err = json.Unmarshal([]byte(adapterResponseAdm), &imageAd)
+	var image = imageAd.Image
+
+	if err == nil {
+		var clickEvent strings.Builder
+		var impressionTracker strings.Builder
+
+		for _, clicktracker := range image.Clicktrackers {
+			clickEvent.WriteString("fetch(decodeURIComponent('" + url.QueryEscape(clicktracker) + "'.replace(/\\+/g, ' ')), " +
+				"{cache: 'no-cache'});")
+		}
+
+		for _, impression := range image.Impressiontrackers {
+
+			impressionTracker.WriteString(fmt.Sprintf(`<img src="%s" alt="" width="0" height="0"/>`, impression))
+		}
+
+		imgMarkup = fmt.Sprintf(`<div style="cursor:pointer" onclick="%s;window.open(decodeURIComponent('%s'.replace(/\+/g, ' ')));"><img src="%s" width="%d" height="%d"/>%s</div>`,
+			&clickEvent, url.QueryEscape(image.Img.Ctaurl), image.
+				Img.URL, image.Img.W, image.Img.
+				H, &impressionTracker)
+	}
+	return imgMarkup, err
+}
diff --git a/adapters/smaato/richmedia.go b/adapters/smaato/richmedia.go
new file mode 100644
index 00000000..1c94a355
--- /dev/null
+++ b/adapters/smaato/richmedia.go
@@ -0,0 +1,52 @@
+package smaato
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strings"
+)
+
+type richMediaAd struct {
+	RichMedia richmedia `json:"richmedia"`
+}
+type mediadata struct {
+	Content string `json:"content"`
+	W       int    `json:"w"`
+	H       int    `json:"h"`
+}
+
+type richmedia struct {
+	MediaData          mediadata `json:"mediadata"`
+	Impressiontrackers []string  `json:"impressiontrackers"`
+	Clicktrackers      []string  `json:"clicktrackers"`
+}
+
+func extractAdmRichMedia(adapterResponseAdm string) (string, error) {
+	var richMediaMarkup string
+	var err error
+
+	var richMediaAd richMediaAd
+	err = json.Unmarshal([]byte(adapterResponseAdm), &richMediaAd)
+	var richMedia = richMediaAd.RichMedia
+
+	if err == nil {
+		var clickEvent strings.Builder
+		var impressionTracker strings.Builder
+
+		for _, clicktracker := range richMedia.Clicktrackers {
+			clickEvent.WriteString("fetch(decodeURIComponent('" + url.QueryEscape(clicktracker) + "'), " +
+				"{cache: 'no-cache'});")
+		}
+		for _, impression := range richMedia.Impressiontrackers {
+
+			impressionTracker.WriteString(fmt.Sprintf(`<img src="%s" alt="" width="0" height="0"/>`, impression))
+		}
+
+		richMediaMarkup = fmt.Sprintf(`<div onclick="%s">%s%s</div>`,
+			&clickEvent,
+			richMedia.MediaData.Content,
+			&impressionTracker)
+	}
+	return richMediaMarkup, err
+}
diff --git a/adapters/smaato/smaato.go b/adapters/smaato/smaato.go
new file mode 100644
index 00000000..06678d77
--- /dev/null
+++ b/adapters/smaato/smaato.go
@@ -0,0 +1,276 @@
+package smaato
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/buger/jsonparser"
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+const clientVersion = "prebid_server_0.1"
+
+type adMarkupType string
+
+const (
+	smtAdTypeImg       adMarkupType = "Img"
+	smtAdTypeRichmedia adMarkupType = "Richmedia"
+)
+
+// SmaatoAdapter describes a Smaato prebid server adapter.
+type SmaatoAdapter struct {
+	URI string
+}
+
+//userExt defines User.Ext object for Smaato
+type userExt struct {
+	Data userExtData `json:"data"`
+}
+
+type userExtData struct {
+	Keywords string `json:"keywords"`
+	Gender   string `json:"gender"`
+	Yob      int64  `json:"yob"`
+}
+
+//userExt defines Site.Ext object for Smaato
+type siteExt struct {
+	Data siteExtData `json:"data"`
+}
+
+type siteExtData struct {
+	Keywords string `json:"keywords"`
+}
+
+// NewSmaatoBidder creates a Smaato bid adapter.
+func NewSmaatoBidder(uri string) *SmaatoAdapter {
+	return &SmaatoAdapter{
+		URI: uri,
+	}
+}
+
+// MakeRequests makes the HTTP requests which should be made to fetch bids.
+func (a *SmaatoAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	errs := make([]error, 0, len(request.Imp))
+	if len(request.Imp) == 0 {
+		errs = append(errs, &errortypes.BadInput{Message: "no impressions in bid request"})
+		return nil, errs
+	}
+
+	// Use bidRequestExt of first imp to retrieve params which are valid for all imps, e.g. publisherId
+	publisherId, err := jsonparser.GetString(request.Imp[0].Ext, "bidder", "publisherId")
+	if err != nil {
+		errs = append(errs, err)
+		return nil, errs
+	}
+
+	for i := 0; i < len(request.Imp); i++ {
+		err := parseImpressionObject(&request.Imp[i])
+		// If the parsing is failed, remove imp and add the error.
+		if err != nil {
+			errs = append(errs, err)
+			request.Imp = append(request.Imp[:i], request.Imp[i+1:]...)
+			i--
+		}
+	}
+	if request.Site != nil {
+		siteCopy := *request.Site
+		siteCopy.Publisher = &openrtb.Publisher{ID: publisherId}
+
+		if request.Site.Ext != nil {
+			var siteExt siteExt
+			err := json.Unmarshal([]byte(request.Site.Ext), &siteExt)
+			if err != nil {
+				errs = append(errs, err)
+				return nil, errs
+			}
+			siteCopy.Keywords = siteExt.Data.Keywords
+			siteCopy.Ext = nil
+		}
+		request.Site = &siteCopy
+	}
+
+	if request.User != nil && request.User.Ext != nil {
+		var userExt userExt
+		var userExtRaw map[string]json.RawMessage
+
+		rawExtErr := json.Unmarshal(request.User.Ext, &userExtRaw)
+		if rawExtErr != nil {
+			errs = append(errs, rawExtErr)
+			return nil, errs
+		}
+
+		userExtErr := json.Unmarshal([]byte(request.User.Ext), &userExt)
+		if userExtErr != nil {
+			errs = append(errs, userExtErr)
+			return nil, errs
+		}
+
+		userCopy := *request.User
+		extractUserExtAttributes(userExt, &userCopy)
+		delete(userExtRaw, "data")
+		userCopy.Ext, err = json.Marshal(userExtRaw)
+		if err != nil {
+			errs = append(errs, err)
+			return nil, errs
+		}
+		request.User = &userCopy
+	}
+
+	// Setting ext client info
+	type bidRequestExt struct {
+		Client string `json:"client"`
+	}
+	request.Ext, err = json.Marshal(bidRequestExt{Client: clientVersion})
+	if err != nil {
+		errs = append(errs, err)
+		return nil, errs
+	}
+	reqJSON, err := json.Marshal(request)
+	if err != nil {
+		errs = append(errs, err)
+		return nil, errs
+	}
+
+	uri := a.URI
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	return []*adapters.RequestData{{
+		Method:  "POST",
+		Uri:     uri,
+		Body:    reqJSON,
+		Headers: headers,
+	}}, errs
+}
+
+// MakeBids unpacks the server's response into Bids.
+func (a *SmaatoAdapter) MakeBids(internalRequest *openrtb.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if response.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if response.StatusCode == http.StatusBadRequest {
+		return nil, []error{&errortypes.BadInput{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
+		}}
+	}
+
+	if response.StatusCode != http.StatusOK {
+		return nil, []error{fmt.Errorf("unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode)}
+	}
+
+	var bidResp openrtb.BidResponse
+	if err := json.Unmarshal(response.Body, &bidResp); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(5)
+
+	for _, sb := range bidResp.SeatBid {
+		for i := 0; i < len(sb.Bid); i++ {
+			bid := sb.Bid[i]
+
+			var markupError error
+			bid.AdM, markupError = renderAdMarkup(getAdMarkupType(response, bid.AdM), bid.AdM)
+			if markupError != nil {
+				fmt.Println(markupError)
+				continue // no bid when broken ad markup
+			}
+
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+				Bid:     &bid,
+				BidType: openrtb_ext.BidTypeBanner,
+			})
+		}
+	}
+	return bidResponse, nil
+}
+
+func renderAdMarkup(adMarkupType adMarkupType, adMarkup string) (string, error) {
+	var markupError error
+	var adm string
+	switch adMarkupType {
+	case smtAdTypeImg:
+		adm, markupError = extractAdmImage(adMarkup)
+	case smtAdTypeRichmedia:
+		adm, markupError = extractAdmRichMedia(adMarkup)
+	default:
+		return "", fmt.Errorf("Unknown markup type %s", adMarkupType)
+	}
+	return adm, markupError
+}
+
+func getAdMarkupType(response *adapters.ResponseData, adMarkup string) adMarkupType {
+	if admType := adMarkupType(response.Headers.Get("X-SMT-ADTYPE")); admType != "" {
+		return admType
+	}
+	if strings.HasPrefix(adMarkup, `{"image":`) {
+		return smtAdTypeImg
+	}
+	if strings.HasPrefix(adMarkup, `{"richmedia":`) {
+		return smtAdTypeRichmedia
+	}
+	return ""
+}
+
+func assignBannerSize(banner *openrtb.Banner) (*openrtb.Banner, error) {
+	if banner.W != nil && banner.H != nil {
+		return banner, nil
+	}
+	if len(banner.Format) == 0 {
+		return banner, fmt.Errorf("No sizes provided for Banner %v", banner.Format)
+	}
+	bannerCopy := *banner
+	bannerCopy.W = new(uint64)
+	*bannerCopy.W = banner.Format[0].W
+	bannerCopy.H = new(uint64)
+	*bannerCopy.H = banner.Format[0].H
+
+	return &bannerCopy, nil
+}
+
+// parseImpressionObject parse the imp to get it ready to send to smaato
+func parseImpressionObject(imp *openrtb.Imp) error {
+	adSpaceID, err := jsonparser.GetString(imp.Ext, "bidder", "adspaceId")
+	if err != nil {
+		return err
+	}
+
+	// SMAATO supports banner impressions.
+	if imp.Banner != nil {
+		bannerCopy, err := assignBannerSize(imp.Banner)
+		if err != nil {
+			return err
+		}
+		imp.Banner = bannerCopy
+		imp.TagID = adSpaceID
+		imp.Ext = nil
+		return nil
+	}
+	return fmt.Errorf("invalid MediaType. SMAATO only supports Banner. Ignoring ImpID=%s", imp.ID)
+}
+
+func extractUserExtAttributes(userExt userExt, userCopy *openrtb.User) {
+	gender := userExt.Data.Gender
+	if gender != "" {
+		userCopy.Gender = gender
+	}
+
+	yob := userExt.Data.Yob
+	if yob != 0 {
+		userCopy.Yob = yob
+	}
+
+	keywords := userExt.Data.Keywords
+	if keywords != "" {
+		userCopy.Keywords = keywords
+	}
+}
diff --git a/analytics/clients/http.go b/analytics/clients/http.go
new file mode 100644
index 00000000..bc7f863e
--- /dev/null
+++ b/analytics/clients/http.go
@@ -0,0 +1,12 @@
+package clients
+
+import (
+	"net/http"
+)
+
+var defaultHttpInstance = http.DefaultClient
+
+func GetDefaultHttpInstance() *http.Client {
+	// TODO 2020-06-22 @see https://github.com/prebid/prebid-server/pull/1331#discussion_r436110097
+	return defaultHttpInstance
+}
diff --git a/analytics/config/config.go b/analytics/config/config.go
index 7be7c8ec..7f7ded0f 100644
--- a/analytics/config/config.go
+++ b/analytics/config/config.go
@@ -3,7 +3,9 @@ package config
 import (
 	"github.com/golang/glog"
 	"github.com/prebid/prebid-server/analytics"
+	"github.com/prebid/prebid-server/analytics/clients"
 	"github.com/prebid/prebid-server/analytics/filesystem"
+	"github.com/prebid/prebid-server/analytics/pubstack"
 	"github.com/prebid/prebid-server/config"
 )
 
@@ -17,6 +19,21 @@ func NewPBSAnalytics(analytics *config.Analytics) analytics.PBSAnalyticsModule {
 			glog.Fatalf("Could not initialize FileLogger for file %v :%v", analytics.File.Filename, err)
 		}
 	}
+	if analytics.Pubstack.Enabled {
+		pubstackModule, err := pubstack.NewPubstackModule(
+			clients.GetDefaultHttpInstance(),
+			analytics.Pubstack.ScopeId,
+			analytics.Pubstack.IntakeUrl,
+			analytics.Pubstack.ConfRefresh,
+			analytics.Pubstack.Buffers.EventCount,
+			analytics.Pubstack.Buffers.BufferSize,
+			analytics.Pubstack.Buffers.Timeout)
+		if err == nil {
+			modules = append(modules, pubstackModule)
+		} else {
+			glog.Errorf("Could not initialize PubstackModule: %v", err)
+		}
+	}
 	return modules
 }
 
diff --git a/analytics/pubstack/README.md b/analytics/pubstack/README.md
new file mode 100644
index 00000000..51c5fdb6
--- /dev/null
+++ b/analytics/pubstack/README.md
@@ -0,0 +1,28 @@
+# Pubstack Analytics
+
+In order to use the pubstack analytics module, it needs to be configured by the host.
+
+You can configure the server using the following environment variables:
+
+```bash
+export PBS_ANALYTICS_PUBSTACK_ENABLED="true"
+export PBS_ANALYTICS_PUBSTACK_ENDPOINT="https://openrtb.preview.pubstack.io/v1/openrtb2"
+export PBS_ANALYTICS_PUBSTACK_SCOPEID=<your scopeId here> # should be an UUIDv4
+```
+
+Or using the pbs configuration file and by appending the following block:
+
+```yaml
+analytics:
+    pubstack:
+      # Required properties
+      enabled: true
+      endpoint: "https://openrtb.preview.pubstack.io/v1/openrtb2"
+      scopeid: "<scopeId>" # The scopeId provided by the Pubstack Support Team
+      # Optional properties (advanced configuration)
+      configuration_refresh_delay: "2h" # Dynamic configuration delay
+      buffers: # Flush events to Pubstack when (first condition reached)
+        size: "2MB" # greater than 2MB
+        count : 100 # greater than 100 events
+        timeout: "15m" # greater than 15 minutes
+```
\ No newline at end of file
diff --git a/analytics/pubstack/config.go b/analytics/pubstack/config.go
new file mode 100644
index 00000000..472acf68
--- /dev/null
+++ b/analytics/pubstack/config.go
@@ -0,0 +1,51 @@
+package pubstack
+
+import (
+	"encoding/json"
+	"github.com/docker/go-units"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+func fetchConfig(client *http.Client, endpoint *url.URL) (*Configuration, error) {
+
+	res, err := client.Get(endpoint.String())
+	if err != nil {
+		return nil, err
+	}
+
+	defer res.Body.Close()
+	c := Configuration{}
+	err = json.NewDecoder(res.Body).Decode(&c)
+	if err != nil {
+		return nil, err
+	}
+	return &c, nil
+}
+
+func newBufferConfig(count int, size, duration string) (*bufferConfig, error) {
+	pDuration, err := time.ParseDuration(duration)
+	if err != nil {
+		return nil, err
+	}
+	pSize, err := units.FromHumanSize(size)
+	if err != nil {
+		return nil, err
+	}
+	return &bufferConfig{
+		pDuration,
+		int64(count),
+		pSize,
+	}, nil
+}
+
+func (a *Configuration) isSameAs(b *Configuration) bool {
+	sameEndpoint := a.Endpoint == b.Endpoint
+	sameScopeID := a.ScopeID == b.ScopeID
+	sameFeature := len(a.Features) == len(b.Features)
+	for key := range a.Features {
+		sameFeature = sameFeature && a.Features[key] == b.Features[key]
+	}
+	return sameFeature && sameEndpoint && sameScopeID
+}
diff --git a/analytics/pubstack/eventchannel/eventchannel.go b/analytics/pubstack/eventchannel/eventchannel.go
new file mode 100644
index 00000000..b8dc4dd8
--- /dev/null
+++ b/analytics/pubstack/eventchannel/eventchannel.go
@@ -0,0 +1,137 @@
+package eventchannel
+
+import (
+	"bytes"
+	"compress/gzip"
+	"sync"
+	"time"
+
+	"github.com/golang/glog"
+)
+
+type Metrics struct {
+	bufferSize int64
+	eventCount int64
+}
+type Limit struct {
+	maxByteSize   int64
+	maxEventCount int64
+	maxTime       time.Duration
+}
+type EventChannel struct {
+	gz   *gzip.Writer
+	buff *bytes.Buffer
+
+	ch          chan []byte
+	endCh       chan int
+	metrics     Metrics
+	muxGzBuffer sync.RWMutex
+	send        Sender
+	limit       Limit
+}
+
+func NewEventChannel(sender Sender, maxByteSize, maxEventCount int64, maxTime time.Duration) *EventChannel {
+	b := &bytes.Buffer{}
+	gzw := gzip.NewWriter(b)
+
+	c := EventChannel{
+		gz:      gzw,
+		buff:    b,
+		ch:      make(chan []byte),
+		endCh:   make(chan int),
+		metrics: Metrics{},
+		send:    sender,
+		limit:   Limit{maxByteSize, maxEventCount, maxTime},
+	}
+	go c.start()
+	return &c
+}
+
+func (c *EventChannel) Push(event []byte) {
+	c.ch <- event
+}
+
+func (c *EventChannel) Close() {
+	c.endCh <- 1
+}
+
+func (c *EventChannel) buffer(event []byte) {
+	c.muxGzBuffer.Lock()
+	defer c.muxGzBuffer.Unlock()
+
+	_, err := c.gz.Write(event)
+	if err != nil {
+		glog.Warning("[pubstack] fail to compress, skip the event")
+		return
+	}
+
+	c.metrics.eventCount++
+	c.metrics.bufferSize += int64(len(event))
+}
+
+func (c *EventChannel) isBufferFull() bool {
+	c.muxGzBuffer.RLock()
+	defer c.muxGzBuffer.RUnlock()
+	return c.metrics.eventCount >= c.limit.maxEventCount || c.metrics.bufferSize >= c.limit.maxByteSize
+}
+
+func (c *EventChannel) reset() {
+	// reset buffer
+	c.gz.Reset(c.buff)
+	c.buff.Reset()
+
+	// reset metrics
+	c.metrics.eventCount = 0
+	c.metrics.bufferSize = 0
+}
+
+func (c *EventChannel) flush() {
+	c.muxGzBuffer.Lock()
+	defer c.muxGzBuffer.Unlock()
+
+	if c.metrics.eventCount == 0 || c.metrics.bufferSize == 0 {
+		return
+	}
+
+	// finish writing gzip header
+	err := c.gz.Flush()
+	if err != nil {
+		glog.Warning("[pubstack] fail to flush gzipped buffer")
+		return
+	}
+
+	// copy the current buffer to send the payload in a new thread
+	payload := make([]byte, c.buff.Len())
+	_, err = c.buff.Read(payload)
+	if err != nil {
+		glog.Warning("[pubstack] fail to copy the buffer")
+		return
+	}
+
+	// reset buffers and writers
+	c.reset()
+
+	// send events (async)
+	go c.send(payload)
+}
+
+func (c *EventChannel) start() {
+	ticker := time.NewTicker(c.limit.maxTime)
+
+	for {
+		select {
+		case <-c.endCh:
+			c.flush()
+			return
+		// event is received
+		case event := <-c.ch:
+			c.buffer(event)
+			if c.isBufferFull() {
+				c.flush()
+			}
+		// time between 2 flushes has passed
+		case <-ticker.C:
+			c.flush()
+		}
+	}
+}
diff --git a/analytics/pubstack/eventchannel/sender.go b/analytics/pubstack/eventchannel/sender.go
new file mode 100644
index 00000000..951de4d4
--- /dev/null
+++ b/analytics/pubstack/eventchannel/sender.go
@@ -0,0 +1,45 @@
+package eventchannel
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/golang/glog"
+	"net/http"
+	"net/url"
+	"path"
+)
+
+type Sender = func(payload []byte) error
+
+func NewHttpSender(client *http.Client, endpoint string) Sender {
+	return func(payload []byte) error {
+		req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(payload))
+		if err != nil {
+			glog.Error(err)
+			return err
+		}
+
+		req.Header.Set("Content-Type", "application/octet-stream")
+		req.Header.Set("Content-Encoding", "gzip")
+
+		resp, err := client.Do(req)
+		if err != nil {
+			return err
+		}
+
+		if resp.StatusCode != http.StatusOK {
+			glog.Errorf("[pubstack] Wrong code received %d instead of %d", resp.StatusCode, http.StatusOK)
+			return fmt.Errorf("wrong code received %d instead of %d", resp.StatusCode, http.StatusOK)
+		}
+		return nil
+	}
+}
+
+func BuildEndpointSender(client *http.Client, baseUrl string, module string) Sender {
+	endpoint, err := url.Parse(baseUrl)
+	if err != nil {
+		glog.Error(err)
+	}
+	endpoint.Path = path.Join(endpoint.Path, "intake", module)
+	return NewHttpSender(client, endpoint.String())
+}
diff --git a/analytics/pubstack/helpers/json.go b/analytics/pubstack/helpers/json.go
new file mode 100644
index 00000000..f02f1120
--- /dev/null
+++ b/analytics/pubstack/helpers/json.go
@@ -0,0 +1,88 @@
+package helpers
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/prebid/prebid-server/analytics"
+)
+
+func JsonifyAuctionObject(ao *analytics.AuctionObject, scope string) ([]byte, error) {
+	b, err := json.Marshal(&struct {
+		Scope string `json:"scope"`
+		*analytics.AuctionObject
+	}{
+		Scope:         scope,
+		AuctionObject: ao,
+	})
+
+	if err == nil {
+		b = append(b, byte('\n'))
+		return b, nil
+	}
+	return nil, fmt.Errorf("auction object badly formed %v", err)
+}
+
+func JsonifyVideoObject(vo *analytics.VideoObject, scope string) ([]byte, error) {
+	b, err := json.Marshal(&struct {
+		Scope string `json:"scope"`
+		*analytics.VideoObject
+	}{
+		Scope:       scope,
+		VideoObject: vo,
+	})
+
+	if err == nil {
+		b = append(b, byte('\n'))
+		return b, nil
+	}
+	return nil, fmt.Errorf("video object badly formed %v", err)
+}
+
+func JsonifyCookieSync(cso *analytics.CookieSyncObject, scope string) ([]byte, error) {
+	b, err := json.Marshal(&struct {
+		Scope string `json:"scope"`
+		*analytics.CookieSyncObject
+	}{
+		Scope:            scope,
+		CookieSyncObject: cso,
+	})
+
+	if err == nil {
+		b = append(b, byte('\n'))
+		return b, nil
+	}
+	return nil, fmt.Errorf("cookie sync object badly formed %v", err)
+}
+
+func JsonifySetUIDObject(so *analytics.SetUIDObject, scope string) ([]byte, error) {
+	b, err := json.Marshal(&struct {
+		Scope string `json:"scope"`
+		*analytics.SetUIDObject
+	}{
+		Scope:        scope,
+		SetUIDObject: so,
+	})
+
+	if err == nil {
+		b = append(b, byte('\n'))
+		return b, nil
+	}
+	return nil, fmt.Errorf("set UID object badly formed %v", err)
+}
+
+func JsonifyAmpObject(ao *analytics.AmpObject, scope string) ([]byte, error) {
+	b, err := json.Marshal(&struct {
+		Scope string `json:"scope"`
+		*analytics.AmpObject
+	}{
+		Scope:     scope,
+		AmpObject: ao,
+	})
+
+	if err == nil {
+		b = append(b, byte('\n'))
+		return b, nil
+	}
+	return nil, fmt.Errorf("amp object badly formed %v", err)
+}
diff --git a/analytics/pubstack/mocks/mock_openrtb_request.json b/analytics/pubstack/mocks/mock_openrtb_request.json
new file mode 100644
index 00000000..03b9665b
--- /dev/null
+++ b/analytics/pubstack/mocks/mock_openrtb_request.json
@@ -0,0 +1,64 @@
+{
+  "id": "19c2eeb8-824c-4604-af41-a59b2b7bb895",
+  "site": {
+    "page": "https%3A%2F%2Fdebug.mediasquare.fr%2Fdebug%2Fprebid%2Fmsq_desktop.html%3Fpbjs_debug%3Dtrue"
+  },
+  "user": {
+    "ext": {}
+  },
+  "regs": {
+    "ext": {}
+  },
+  "test": 1,
+  "imp": [
+    {
+      "id": "0341252e-b3b0-4dff-a0ef-1ced63369bd5",
+      "ext": {
+        "appnexus": {
+          "placementId": 5724999
+        }
+      },
+      "secure": 1,
+      "banner": {
+        "format": [
+          {
+            "w": 970,
+            "h": 250
+          }
+        ]
+      }
+    },
+    {
+      "id": "3ac0ffa3-01de-44d2-9baf-1fee79026624",
+      "ext": {
+        "msqClassic": {
+          "placementId": 10471298
+        }
+      },
+      "secure": 1,
+      "banner": {
+        "format": [
+          {
+            "w": 300,
+            "h": 250
+          },
+          {
+            "w": 300,
+            "h": 600
+          }
+        ]
+      }
+    }
+  ],
+  "tmax": 500,
+  "ext": {
+    "prebid": {
+      "bidadjustmentfactors": {
+        "msqClassic": 0.8,
+        "msqBrand": 0.8,
+        "msqMax": 0.8,
+        "msqMaxView": 0.8
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/analytics/pubstack/mocks/mock_openrtb_response.json b/analytics/pubstack/mocks/mock_openrtb_response.json
new file mode 100644
index 00000000..6f4d1965
--- /dev/null
+++ b/analytics/pubstack/mocks/mock_openrtb_response.json
@@ -0,0 +1,91 @@
+{
+  "id": "19c2eeb8-824c-4604-af41-a59b2b7bb895",
+  "seatbid": [{
+    "seat": "958",
+    "bid": [
+      {
+      "id": "7706636740145184841",
+      "impid": "0341252e-b3b0-4dff-a0ef-1ced63369bd5",
+      "price": 0.500000,
+      "adid": "29681110",
+      "adm": "some-test-ad",
+      "adomain": ["appnexus.com"],
+      "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+      "cid": "958",
+      "crid": "29681110",
+      "h": 970,
+      "w": 250,
+      "ext": {
+        "appnexus": {
+          "brand_id": 1,
+          "brand_category_id": 1,
+          "auction_id": 8189378542222915032,
+          "bid_ad_type": 0,
+          "bidder_id": 2,
+          "ranking_price": 0.000000
+        }
+      }
+    },
+      {
+        "id": "7706636740145184842",
+        "impid": "0341252e-b3b0-4dff-a0ef-1ced63369bd5",
+        "price": 0.0,
+        "adid": "29681114",
+        "adm": "some-test-ad2",
+        "adomain": ["appnexus.com"],
+        "iurl": "http://nym1-ib.adnxs.com/cr?id=29681114",
+        "cid": "959",
+        "crid": "29681114",
+        "h": 970,
+        "w": 250,
+        "ext": {
+          "appnexus": {
+            "brand_id": 1,
+            "brand_category_id": 1,
+            "auction_id": 8189378542222915032,
+            "bid_ad_type": 0,
+            "bidder_id": 2,
+            "ranking_price": 0.000000
+          }
+        }
+      },{
+        "id": "7706636740145184842",
+        "impid": "3ac0ffa3-01de-44d2-9baf-1fee79026624",
+        "price": 0.5234,
+        "adid": "29681113",
+        "adm": "some-test-ad2",
+        "adomain": ["appnexus.com"],
+        "iurl": "http://nym1-ib.adnxs.com/cr?id=29681113",
+        "cid": "959",
+        "crid": "29681113",
+        "h": 970,
+        "w": 250,
+        "ext": {
+          "appnexus": {
+            "brand_id": 1,
+            "brand_category_id": 1,
+            "auction_id": 8189378542222915032,
+            "bid_ad_type": 0,
+            "bidder_id": 2,
+            "ranking_price": 0.000000
+          }
+        }
+      }]
+  },      {
+    "seat": "improvedigital",
+    "bid": [{
+      "id": "randomid",
+      "impid": "0341252e-b3b0-4dff-a0ef-1ced63369bd5",
+      "price": 0.510000,
+      "adid": "12345678",
+      "adm": "some-test-ad",
+      "cid": "987",
+      "crid": "12345678",
+      "h": 250,
+      "w": 300
+    }]
+  }
+  ],
+  "bidid": "5778926625248726496",
+  "cur": "USD"
+}
diff --git a/analytics/pubstack/pubstack_module.go b/analytics/pubstack/pubstack_module.go
new file mode 100644
index 00000000..9f1a81c7
--- /dev/null
+++ b/analytics/pubstack/pubstack_module.go
@@ -0,0 +1,273 @@
+package pubstack
+
+import (
+	"fmt"
+	"github.com/prebid/prebid-server/analytics/pubstack/eventchannel"
+	"net/http"
+	"net/url"
+	"os"
+	"os/signal"
+	"sync"
+	"syscall"
+	"time"
+
+	"github.com/golang/glog"
+	"github.com/prebid/prebid-server/analytics/pubstack/helpers"
+
+	"github.com/prebid/prebid-server/analytics"
+)
+
+type Configuration struct {
+	ScopeID  string          `json:"scopeId"`
+	Endpoint string          `json:"endpoint"`
+	Features map[string]bool `json:"features"`
+}
+
+// routes for events
+const (
+	auction    = "auction"
+	cookieSync = "cookiesync"
+	amp        = "amp"
+	setUID     = "setuid"
+	video      = "video"
+)
+
+type bufferConfig struct {
+	timeout time.Duration
+	count   int64
+	size    int64
+}
+
+type PubstackModule struct {
+	eventChannels map[string]*eventchannel.EventChannel
+	httpClient    *http.Client
+	configCh      chan *Configuration
+	sigTermCh     chan os.Signal
+	scope         string
+	cfg           *Configuration
+	buffsCfg      *bufferConfig
+	muxConfig     sync.RWMutex
+}
+
+func NewPubstackModule(client *http.Client, scope, endpoint, configRefreshDelay string, maxEventCount int, maxByteSize, maxTime string) (analytics.PBSAnalyticsModule, error) {
+	glog.Infof("[pubstack] Initializing module scope=%s endpoint=%s\n", scope, endpoint)
+
+	// parse args
+
+	refreshDelay, err := time.ParseDuration(configRefreshDelay)
+	if err != nil {
+		return nil, fmt.Errorf("fail to parse the module args, arg=analytics.pubstack.configuration_refresh_delay, :%v", err)
+	}
+
+	bufferCfg, err := newBufferConfig(maxEventCount, maxByteSize, maxTime)
+	if err != nil {
+		return nil, fmt.Errorf("fail to parse the module args, arg=analytics.pubstack.buffers, :%v", err)
+	}
+
+	defaultFeatures := map[string]bool{
+		auction:    false,
+		video:      false,
+		amp:        false,
+		cookieSync: false,
+		setUID:     false,
+	}
+
+	defaultConfig := &Configuration{
+		ScopeID:  scope,
+		Endpoint: endpoint,
+		Features: defaultFeatures,
+	}
+
+	pb := PubstackModule{
+		scope:         scope,
+		httpClient:    client,
+		cfg:           defaultConfig,
+		buffsCfg:      bufferCfg,
+		sigTermCh:     make(chan os.Signal),
+		configCh:      make(chan *Configuration),
+		eventChannels: make(map[string]*eventchannel.EventChannel),
+		muxConfig:     sync.RWMutex{},
+	}
+	signal.Notify(pb.sigTermCh, os.Interrupt, syscall.SIGTERM)
+
+	configUrl, err := url.Parse(pb.cfg.Endpoint + "/bootstrap?scopeId=" + pb.cfg.ScopeID)
+	if err != nil {
+		glog.Error(err)
+		return nil, err
+	}
+	go pb.start(configUrl, refreshDelay)
+	go func() {
+		err = pb.reloadConfig(configUrl)
+		if err != nil {
+			glog.Errorf("[pubstack] Fail to fetch remote configuration: %v", err)
+		}
+	}()
+
+	glog.Info("[pubstack] Pubstack analytics configured and ready")
+	return &pb, nil
+}
+
+func (p *PubstackModule) LogAuctionObject(ao *analytics.AuctionObject) {
+	p.muxConfig.RLock()
+	defer p.muxConfig.RUnlock()
+
+	if !p.isFeatureEnable(auction) {
+		return
+	}
+
+	// serialize event
+	payload, err := helpers.JsonifyAuctionObject(ao, p.scope)
+	if err != nil {
+		glog.Warning("[pubstack] Cannot serialize auction")
+		return
+	}
+
+	p.eventChannels[auction].Push(payload)
+}
+
+func (p *PubstackModule) LogVideoObject(vo *analytics.VideoObject) {
+	p.muxConfig.RLock()
+	defer p.muxConfig.RUnlock()
+
+	if !p.isFeatureEnable(video) {
+		return
+	}
+
+	// serialize event
+	payload, err := helpers.JsonifyVideoObject(vo, p.scope)
+	if err != nil {
+		glog.Warning("[pubstack] Cannot serialize video")
+		return
+	}
+
+	p.eventChannels[video].Push(payload)
+}
+
+func (p *PubstackModule) LogSetUIDObject(so *analytics.SetUIDObject) {
+	p.muxConfig.RLock()
+	defer p.muxConfig.RUnlock()
+
+	if !p.isFeatureEnable(setUID) {
+		return
+	}
+
+	// serialize event
+	payload, err := helpers.JsonifySetUIDObject(so, p.scope)
+	if err != nil {
+		glog.Warning("[pubstack] Cannot serialize video")
+		return
+	}
+
+	p.eventChannels[setUID].Push(payload)
+}
+
+func (p *PubstackModule) LogCookieSyncObject(cso *analytics.CookieSyncObject) {
+	p.muxConfig.RLock()
+	defer p.muxConfig.RUnlock()
+
+	if !p.isFeatureEnable(cookieSync) {
+		return
+	}
+
+	// serialize event
+	payload, err := helpers.JsonifyCookieSync(cso, p.scope)
+	if err != nil {
+		glog.Warning("[pubstack] Cannot serialize video")
+		return
+	}
+
+	p.eventChannels[cookieSync].Push(payload)
+
+}
+
+func (p *PubstackModule) LogAmpObject(ao *analytics.AmpObject) {
+	p.muxConfig.RLock()
+	defer p.muxConfig.RUnlock()
+
+	if !p.isFeatureEnable(amp) {
+		return
+	}
+
+	// serialize event
+	payload, err := helpers.JsonifyAmpObject(ao, p.scope)
+	if err != nil {
+		glog.Warning("[pubstack] Cannot serialize video")
+		return
+	}
+
+	p.eventChannels[amp].Push(payload)
+
+}
+
+func (p *PubstackModule) reloadConfig(configUrl *url.URL) error {
+	config, err := fetchConfig(p.httpClient, configUrl)
+	if err != nil {
+		return err
+	}
+	p.configCh <- config
+	return nil
+}
+
+func (p *PubstackModule) start(configUrl *url.URL, refreshDelay time.Duration) {
+
+	tick := time.NewTicker(refreshDelay)
+
+	for {
+		select {
+		case <-p.sigTermCh:
+			p.closeAllEventChannels()
+			return
+		case config := <-p.configCh:
+			p.updateConfig(config)
+			glog.Infof("[pubstack] Updating config: %v", p.cfg)
+		case <-tick.C:
+			go func() {
+				err := p.reloadConfig(configUrl)
+				if err != nil {
+					glog.Errorf("[pubstack] Fail to fetch remote configuration: %v", err)
+				}
+			}()
+		}
+	}
+
+}
+
+func (p *PubstackModule) updateConfig(config *Configuration) {
+	p.muxConfig.Lock()
+	defer p.muxConfig.Unlock()
+
+	if p.cfg.isSameAs(config) {
+		return
+	}
+
+	p.cfg = config
+	p.closeAllEventChannels()
+
+	if p.isFeatureEnable(amp) {
+		p.eventChannels[amp] = eventchannel.NewEventChannel(eventchannel.BuildEndpointSender(p.httpClient, p.cfg.Endpoint, amp), p.buffsCfg.size, p.buffsCfg.count, p.buffsCfg.timeout)
+	}
+	if p.isFeatureEnable(auction) {
+		p.eventChannels[auction] = eventchannel.NewEventChannel(eventchannel.BuildEndpointSender(p.httpClient, p.cfg.Endpoint, auction), p.buffsCfg.size, p.buffsCfg.count, p.buffsCfg.timeout)
+	}
+	if p.isFeatureEnable(cookieSync) {
+		p.eventChannels[cookieSync] = eventchannel.NewEventChannel(eventchannel.BuildEndpointSender(p.httpClient, p.cfg.Endpoint, cookieSync), p.buffsCfg.size, p.buffsCfg.count, p.buffsCfg.timeout)
+	}
+	if p.isFeatureEnable(video) {
+		p.eventChannels[video] = eventchannel.NewEventChannel(eventchannel.BuildEndpointSender(p.httpClient, p.cfg.Endpoint, video), p.buffsCfg.size, p.buffsCfg.count, p.buffsCfg.timeout)
+	}
+	if p.isFeatureEnable(setUID) {
+		p.eventChannels[setUID] = eventchannel.NewEventChannel(eventchannel.BuildEndpointSender(p.httpClient, p.cfg.Endpoint, setUID), p.buffsCfg.size, p.buffsCfg.count, p.buffsCfg.timeout)
+	}
+}
+
+func (p *PubstackModule) closeAllEventChannels() {
+	for key, ch := range p.eventChannels {
+		ch.Close()
+		delete(p.eventChannels, key)
+	}
+}
+
+func (p *PubstackModule) isFeatureEnable(feature string) bool {
+	val, ok := p.cfg.Features[feature]
+	return ok && val
+}
diff --git a/config/config.go b/config/config.go
index 8545523d..fb060764 100755
--- a/config/config.go
+++ b/config/config.go
@@ -209,7 +209,8 @@ type LMT struct {
 }
 
 type Analytics struct {
-	File FileLogs `mapstructure:"file"`
+	File     FileLogs `mapstructure:"file"`
+	Pubstack Pubstack `mapstructure:"pubstack"`
 }
 
 type CurrencyConverter struct {
@@ -230,6 +231,20 @@ type FileLogs struct {
 	Filename string `mapstructure:"filename"`
 }
 
+type Pubstack struct {
+	Enabled     bool           `mapstructure:"enabled"`
+	ScopeId     string         `mapstructure:"scopeid"`
+	IntakeUrl   string         `mapstructure:"endpoint"`
+	Buffers     PubstackBuffer `mapstructure:"buffers"`
+	ConfRefresh string         `mapstructure:"configuration_refresh_delay"`
+}
+
+type PubstackBuffer struct {
+	BufferSize string `mapstructure:"size"`
+	EventCount int    `mapstructure:"count"`
+	Timeout    string `mapstructure:"timeout"`
+}
+
 type HostCookie struct {
 	Domain             string `mapstructure:"domain"`
 	Family             string `mapstructure:"family"`
@@ -830,6 +845,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.rubicon.disabled", true)
 	v.SetDefault("adapters.rubicon.endpoint", "http://exapi-us-east.rubiconproject.com/a/api/exchange.json")
 	v.SetDefault("adapters.sharethrough.endpoint", "http://btlr.sharethrough.com/FGMrCMMc/v1")
+	v.SetDefault("adapters.smaato.endpoint", "https://prebid.ad.smaato.net/oapi/prebid")
 	v.SetDefault("adapters.smartadserver.endpoint", "https://ssb.smartadserver.com")
 	v.SetDefault("adapters.smartrtb.endpoint", "http://market-east.smrtb.com/json/publisher/rtb?pubid={{.PublisherID}}")
 	v.SetDefault("adapters.somoaudience.endpoint", "http://publisher-east.mobileadtrading.com/rtb/bid")
@@ -855,6 +871,13 @@ func SetupViper(v *viper.Viper, filename string) {
 
 	v.SetDefault("max_request_size", 1024*256)
 	v.SetDefault("analytics.file.filename", "")
+	v.SetDefault("analytics.pubstack.endpoint", "https://s2s.pbstck.com/v1")
+	v.SetDefault("analytics.pubstack.scopeid", "change-me")
+	v.SetDefault("analytics.pubstack.enabled", false)
+	v.SetDefault("analytics.pubstack.configuration_refresh_delay", "2h")
+	v.SetDefault("analytics.pubstack.buffers.size", "2MB")
+	v.SetDefault("analytics.pubstack.buffers.count", 100)
+	v.SetDefault("analytics.pubstack.buffers.timeout", "900s")
 	v.SetDefault("amp_timeout_adjustment_ms", 0)
 	v.SetDefault("gdpr.host_vendor_id", 0)
 	v.SetDefault("gdpr.usersync_if_ambiguous", false)
diff --git a/docs/bidders/openx.md b/docs/bidders/openx.md
index c366db3a..54a0a5b1 100644
--- a/docs/bidders/openx.md
+++ b/docs/bidders/openx.md
@@ -5,10 +5,13 @@ OpenX supports the following parameters:
 | property | type | required? | description | example |
 |----------|------|-----------|-------------|---------|
 | unit | string | required | The ad unit id | "10092842" |
-| delDomain | string | required | The delivery domain for the customer | "sademo-d.openx.net" |
+| delDomain | string | required\* | The delivery domain for the customer | "sademo-d.openx.net" |
+| platform | uuid | required\* | The platform id for the customer | "a3aece0c-9e80-4316-8deb-faf804779bd1" |
 | customFloor | number | optional | The minimum CPM price in USD | 1.50 - sets a $1.50 floor |
 | customParams | object | optional | User-defined targeting key-value pairs | {key1: "v1", key2: ["v2","v3"]} |
 
+\* At least one of `delDomain` or `platform` parameters is required.
+
 If you have any questions regarding setting up, please reach out to your account manager or 
 <support@openx.com>
 
@@ -59,4 +62,4 @@ If you have any questions regarding setting up, please reach out to your account
     },
   }
 }
-```
\ No newline at end of file
+```
diff --git a/docs/bidders/smaato.md b/docs/bidders/smaato.md
new file mode 100644
index 00000000..881f8f2a
--- /dev/null
+++ b/docs/bidders/smaato.md
@@ -0,0 +1,42 @@
+
+# Smaato Bidder
+
+```
+Module Name: Smaato Bidder Adapter
+Module Type: Bidder Adapter
+Maintainer: prebid@smaato.com
+```
+
+### Description
+
+Please contact Smaato Support or prebid@smaato.com to get set up with a publisherId and adspaceId.
+
+### Test Parameters:  
+
+Following example includes sample `imp` object with publisherId and adSlot which can be used to test Smaato Adapter
+
+```
+"imp":[
+      {
+         "id":“1C86242D-9535-47D6-9576-7B1FE87F282C,
+         "banner":{
+            "format":[
+               {
+                  "w":300,
+                  "h":50
+               },
+               {
+                  "w":300,
+                  "h":250
+               }
+            ]
+         },
+         "ext":{
+            "smaato":{
+               "publisherId":"100042525",
+               "adspaceId":"130563103"
+            }
+         }
+      }
+   ]
+```
diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md
index d0921618..b532923e 100644
--- a/docs/endpoints/openrtb2/auction.md
+++ b/docs/endpoints/openrtb2/auction.md
@@ -173,6 +173,7 @@ to set these params on the response at `response.seatbid[i].bid[j].ext.prebid.ta
         },
         "includewinners": false, // Optional param defaulting to true
         "includebidderkeys": false // Optional param defaulting to true
+        "includeformat": false // Optional param defaulting to false
       }
     }
   }
@@ -184,6 +185,8 @@ For backwards compatibility the following strings will also be allowed as price
 
 One of "includewinners" or "includebidderkeys" must be true (both default to true if unset). If both were false, then no targeting keys would be set, which is better configured by omitting targeting altogether.
 
+The parameter "includeformat" indicates the type of the bid (banner, video, etc) for multiformat requests. It will add the key `hb_format` and/or `hb_format_{bidderName}` as per "includewinners" and "includebidderkeys" above.
+
 MediaType PriceGranularity (PBS-Java only) - when a single OpenRTB request contains multiple impressions with different mediatypes, or a single impression supports multiple formats, the different mediatypes may need different price granularities. If `mediatypepricegranularity` is present, `pricegranularity` would only be used for any mediatypes not specified. 
 
 ```
diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go
index 2ecddb83..207a7a9b 100755
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -64,6 +64,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/rtbhouse"
 	"github.com/prebid/prebid-server/adapters/rubicon"
 	"github.com/prebid/prebid-server/adapters/sharethrough"
+	"github.com/prebid/prebid-server/adapters/smaato"
 	"github.com/prebid/prebid-server/adapters/smartadserver"
 	"github.com/prebid/prebid-server/adapters/smartrtb"
 	"github.com/prebid/prebid-server/adapters/somoaudience"
@@ -154,6 +155,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 			cfg.Adapters[string(openrtb_ext.BidderRubicon)].XAPI.Tracker),
 
 		openrtb_ext.BidderSharethrough:     sharethrough.NewSharethroughBidder(cfg.Adapters[string(openrtb_ext.BidderSharethrough)].Endpoint),
+		openrtb_ext.BidderSmaato:           smaato.NewSmaatoBidder(cfg.Adapters[string(openrtb_ext.BidderSmaato)].Endpoint),
 		openrtb_ext.BidderSmartadserver:    smartadserver.NewSmartadserverBidder(cfg.Adapters[string(openrtb_ext.BidderSmartadserver)].Endpoint),
 		openrtb_ext.BidderSmartRTB:         smartrtb.NewSmartRTBBidder(cfg.Adapters[string(openrtb_ext.BidderSmartRTB)].Endpoint),
 		openrtb_ext.BidderSomoaudience:     somoaudience.NewSomoaudienceBidder(cfg.Adapters[string(openrtb_ext.BidderSomoaudience)].Endpoint),
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 3f0258dd..5001e495 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -135,6 +135,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 				includeBidderKeys: requestExt.Prebid.Targeting.IncludeBidderKeys,
 				includeCacheBids:  shouldCacheBids,
 				includeCacheVast:  shouldCacheVAST,
+				includeFormat:     requestExt.Prebid.Targeting.IncludeFormat,
 			}
 			targData.cacheHost, targData.cachePath = e.cache.GetExtCacheData()
 		}
diff --git a/exchange/targeting.go b/exchange/targeting.go
index 994ad9e7..dca57653 100644
--- a/exchange/targeting.go
+++ b/exchange/targeting.go
@@ -22,6 +22,7 @@ type targetData struct {
 	includeBidderKeys bool
 	includeCacheBids  bool
 	includeCacheVast  bool
+	includeFormat     bool
 	// cacheHost and cachePath exist to supply cache host and path as targeting parameters
 	cacheHost string
 	cachePath string
@@ -53,6 +54,9 @@ func (targData *targetData) setTargeting(auc *auction, isApp bool, categoryMappi
 			if vastID, ok := auc.vastCacheIds[topBidPerBidder.bid]; ok {
 				targData.addKeys(targets, openrtb_ext.HbVastCacheKey, vastID, bidderName, isOverallWinner)
 			}
+			if targData.includeFormat {
+				targData.addKeys(targets, openrtb_ext.HbFormatKey, string(topBidPerBidder.bidType), bidderName, isOverallWinner)
+			}
 
 			if targData.cacheHost != "" {
 				targData.addKeys(targets, openrtb_ext.HbConstantCacheHostKey, targData.cacheHost, bidderName, isOverallWinner)
diff --git a/go.mod b/go.mod
index 00cadd31..a5b5a161 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
 	github.com/cespare/xxhash v1.0.0 // indirect
 	github.com/chasex/glog v0.0.0-20160217080310-c62392af379c
 	github.com/coocood/freecache v1.0.1
+	github.com/docker/go-units v0.4.0
 	github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5
 	github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd
 	github.com/gofrs/uuid v3.2.0+incompatible
diff --git a/go.sum b/go.sum
index 5eaf37ca..1ddab713 100644
--- a/go.sum
+++ b/go.sum
@@ -23,6 +23,8 @@ github.com/coocood/freecache v1.0.1/go.mod h1:ePwxCDzOYvARfHdr1pByNct1at3CoKnsip
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
 github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
 github.com/evanphx/json-patch v0.0.0-20180720181644-f195058310bd h1:biTJQdqouE5by89AAffXG8++TY+9Fsdrg5rinbt3tHk=
diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go
index 3b297c7a..09ee375b 100644
--- a/openrtb_ext/bid.go
+++ b/openrtb_ext/bid.go
@@ -99,6 +99,9 @@ const (
 	HbSizeConstantKey   TargetingKey = "hb_size"
 	HbDealIDConstantKey TargetingKey = "hb_deal"
 
+	// HbFormatKey is the format of the bid. For example, "video", "banner"
+	HbFormatKey TargetingKey = "hb_format"
+
 	// HbCacheKey and HbVastCacheKey store UUIDs which can be used to fetch things from prebid cache.
 	// Callers should *never* assume that either of these exist, since the call to the cache may always fail.
 	//
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 62fb9750..ee0f4090 100755
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -80,6 +80,7 @@ const (
 	BidderRTBHouse         BidderName = "rtbhouse"
 	BidderRubicon          BidderName = "rubicon"
 	BidderSharethrough     BidderName = "sharethrough"
+	BidderSmaato           BidderName = "smaato"
 	BidderSmartadserver    BidderName = "smartadserver"
 	BidderSmartRTB         BidderName = "smartrtb"
 	BidderSomoaudience     BidderName = "somoaudience"
@@ -162,6 +163,7 @@ var BidderMap = map[string]BidderName{
 	"rtbhouse":          BidderRTBHouse,
 	"rubicon":           BidderRubicon,
 	"sharethrough":      BidderSharethrough,
+	"smaato":            BidderSmaato,
 	"smartadserver":     BidderSmartadserver,
 	"smartrtb":          BidderSmartRTB,
 	"somoaudience":      BidderSomoaudience,
diff --git a/openrtb_ext/imp_openx.go b/openrtb_ext/imp_openx.go
index e63595b0..2625cb38 100644
--- a/openrtb_ext/imp_openx.go
+++ b/openrtb_ext/imp_openx.go
@@ -3,6 +3,7 @@ package openrtb_ext
 // ExtImpOpenx defines the contract for bidrequest.imp[i].ext.openx
 type ExtImpOpenx struct {
 	Unit         string                 `json:"unit"`
+	Platform     string                 `json:"platform"`
 	DelDomain    string                 `json:"delDomain"`
 	CustomFloor  float64                `json:"customFloor"`
 	CustomParams map[string]interface{} `json:"customParams"`
diff --git a/openrtb_ext/imp_smaato.go b/openrtb_ext/imp_smaato.go
new file mode 100644
index 00000000..10de97fb
--- /dev/null
+++ b/openrtb_ext/imp_smaato.go
@@ -0,0 +1,9 @@
+package openrtb_ext
+
+// ExtImpSmaato defines the contract for bidrequest.imp[i].ext.smaato
+// PublisherId and AdSpaceId are mandatory parameters, others are optional parameters
+// AdSpaceId is identifier for specific ad placement or ad tag
+type ExtImpSmaato struct {
+	PublisherID string `json:"publisherId"`
+	AdSpaceID   string `json:"adspaceId"`
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 86388f60..acfd4a1e 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -85,6 +85,7 @@ type ExtRequestTargeting struct {
 	IncludeWinners       bool                     `json:"includewinners"`
 	IncludeBidderKeys    bool                     `json:"includebidderkeys"`
 	IncludeBrandCategory *ExtIncludeBrandCategory `json:"includebrandcategory"`
+	IncludeFormat        bool                     `json:"includeformat"`
 	DurationRangeSec     []int                    `json:"durationrangesec"`
 }
 
diff --git a/static/bidder-info/smaato.yaml b/static/bidder-info/smaato.yaml
new file mode 100644
index 00000000..662603fe
--- /dev/null
+++ b/static/bidder-info/smaato.yaml
@@ -0,0 +1,9 @@
+maintainer:
+  email: "prebid@smaato.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+  site:
+    mediaTypes:
+      - banner
\ No newline at end of file
diff --git a/static/bidder-params/openx.json b/static/bidder-params/openx.json
index 93a672ed..6dbd1017 100644
--- a/static/bidder-params/openx.json
+++ b/static/bidder-params/openx.json
@@ -16,6 +16,11 @@
       "pattern": "\\.[a-zA-Z]{2,3}$",
       "format": "hostname"
     },
+    "platform": {
+      "type": "string",
+      "description": "The platform id for the customer.",
+      "format": "uuid"
+    },
     "customFloor": {
       "type": "number",
       "description": "The minimum CPM price in USD.",
@@ -26,6 +31,19 @@
       "description": "User-defined targeting key-value pairs."
     }
   },
-
-  "required": ["unit", "delDomain"]
+  "required": [
+    "unit"
+  ],
+  "anyOf": [
+    {
+      "required": [
+        "delDomain"
+      ]
+    },
+    {
+      "required": [
+        "platform"
+      ]
+    }
+  ]
 }
diff --git a/static/bidder-params/smaato.json b/static/bidder-params/smaato.json
new file mode 100644
index 00000000..aa91c4ba
--- /dev/null
+++ b/static/bidder-params/smaato.json
@@ -0,0 +1,17 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Smaato Adapter Params",
+  "description": "A schema which validates params accepted by the Smaato adapter",
+  "type": "object",
+  "properties": {
+    "publisherId": {
+      "type": "string",
+      "description": "A unique identifier for this impression within the context of the bid request"
+    },
+    "adspaceId": {
+      "type": "string",
+      "description": "Identifier for specific ad placement is SOMA `adspaceId`"
+    }
+  },
+  "required": ["publisherId","adspaceId"]
+}
\ No newline at end of file


Test patch:
diff --git a/adapters/openx/openxtest/exemplary/optional-params.json b/adapters/openx/openxtest/exemplary/optional-params.json
index 22555987..e8fcf394 100644
--- a/adapters/openx/openxtest/exemplary/optional-params.json
+++ b/adapters/openx/openxtest/exemplary/optional-params.json
@@ -11,6 +11,7 @@
           "bidder": {
             "unit": "539439964",
             "delDomain": "se-demo-d.openx.net",
+            "platform": "PLATFORM",
             "customFloor": 0.1,
             "customParams": {"foo": "bar"}
           }
@@ -40,7 +41,8 @@
           ],
           "ext": {
             "bc": "hb_pbs_1.0.0",
-            "delDomain": "se-demo-d.openx.net"
+            "delDomain": "se-demo-d.openx.net",
+            "platform": "PLATFORM"
           }
         }
       },
diff --git a/adapters/smaato/image_test.go b/adapters/smaato/image_test.go
new file mode 100644
index 00000000..5f39c857
--- /dev/null
+++ b/adapters/smaato/image_test.go
@@ -0,0 +1,44 @@
+package smaato
+
+import (
+	"testing"
+)
+
+func TestRenderAdMarkup(t *testing.T) {
+	type args struct {
+		adType             adMarkupType
+		adapterResponseAdm string
+	}
+	expectedResult := `<div style="cursor:pointer"` +
+		` onclick="fetch(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fclick%2F1'.replace(/\+/g, ' ')),` +
+		` {cache: 'no-cache'});fetch(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fclick%2F2'.replace(/\+/g, ' ')),` +
+		` {cache: 'no-cache'});;window.open(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fctaurl%2F1'.replace(/\+/g, ' ')));">` +
+		`<img src="//prebid-test.smaatolabs.net/img/320x50.jpg" width="350" height="50"/>` +
+		`<img src="//prebid-test.smaatolabs.net/track/imp/1" alt="" width="0" height="0"/>` +
+		`<img src="//prebid-test.smaatolabs.net/track/imp/2" alt="" width="0" height="0"/></div>`
+
+	tests := []struct {
+		testName string
+		args     args
+		result   string
+	}{
+		{"imageTest", args{"Img",
+			"{\"image\":{\"img\":{\"url\":\"//prebid-test.smaatolabs.net/img/320x50.jpg\"," +
+				"\"w\":350,\"h\":50,\"ctaurl\":\"//prebid-test.smaatolabs.net/track/ctaurl/1\"}," +
+				"\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"]," +
+				"\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}"},
+			expectedResult,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.testName, func(t *testing.T) {
+			got, err := renderAdMarkup(tt.args.adType, tt.args.adapterResponseAdm)
+			if err != nil {
+				t.Errorf("error rendering ad markup: %v", err)
+			}
+			if got != tt.result {
+				t.Errorf("renderAdMarkup() got = %v, result %v", got, tt.result)
+			}
+		})
+	}
+}
diff --git a/adapters/smaato/params_test.go b/adapters/smaato/params_test.go
new file mode 100644
index 00000000..6c71cbe7
--- /dev/null
+++ b/adapters/smaato/params_test.go
@@ -0,0 +1,65 @@
+package smaato
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// This file intends to test static/bidder-params/smaato.json
+
+// These also validate the format of the external API: request.imp[i].bidRequestExt.smaato
+
+// TestValidParams makes sure that the Smaato schema accepts all imp.bidRequestExt fields which Smaato supports.
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, validParam := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderSmaato, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected smaato params: %s \n Error: %s", validParam, err)
+		}
+	}
+}
+
+// TestInvalidParams makes sure that the Smaato schema rejects all the imp.bidRequestExt fields which are not support.
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, invalidParam := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderSmaato, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"publisherId":"test-id-1234-smaato","adspaceId": "1123581321"}`,
+}
+
+var invalidParams = []string{
+	``,
+	`null`,
+	`true`,
+	`5`,
+	`4.2`,
+	`[]`,
+	`{}`,
+	`{"publisherId":"test-id-1234-smaato"}`,
+	`{"adspaceId": "1123581321"}`,
+	`{"publisherId":false}`,
+	`{"adspaceId":false}`,
+	`{"publisherId":0,"adspaceId": 1123581321}`,
+	`{"publisherId":false,"adspaceId": true}`,
+	`{"instl": 0}`,
+	`{"secure": 0}`,
+	`{"adspaceId": "1123581321","instl": 0,"secure": 0}`,
+	`{"instl": 0,"secure": 0}`,
+	`{"publisherId":"test-id-1234-smaato","instl": 0,"secure": 0}`,
+}
diff --git a/adapters/smaato/richmedia_test.go b/adapters/smaato/richmedia_test.go
new file mode 100644
index 00000000..20fa1ba3
--- /dev/null
+++ b/adapters/smaato/richmedia_test.go
@@ -0,0 +1,39 @@
+package smaato
+
+import (
+	"testing"
+)
+
+func TestExtractAdmRichMedia(t *testing.T) {
+	type args struct {
+		adType             adMarkupType
+		adapterResponseAdm string
+	}
+	expectedResult := `<div onclick="fetch(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fclick%2F1'),` +
+		` {cache: 'no-cache'});fetch(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fclick%2F2'),` +
+		` {cache: 'no-cache'});"><div>hello</div><img src="//prebid-test.smaatolabs.net/track/imp/1" alt="" width="0" height="0"/>` +
+		`<img src="//prebid-test.smaatolabs.net/track/imp/2" alt="" width="0" height="0"/></div>`
+	tests := []struct {
+		testName string
+		args     args
+		result   string
+	}{
+		{"richmediaTest", args{"Richmedia", "{\"richmedia\":{\"mediadata\":{\"content\":\"<div>hello</div>\"," +
+			"" + "\"w\":350," +
+			"\"h\":50},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"]," +
+			"\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}"},
+			expectedResult,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.testName, func(t *testing.T) {
+			got, err := renderAdMarkup(tt.args.adType, tt.args.adapterResponseAdm)
+			if err != nil {
+				t.Errorf("error rendering ad markup: %v", err)
+			}
+			if got != tt.result {
+				t.Errorf("renderAdMarkup() got = %v, result %v", got, tt.result)
+			}
+		})
+	}
+}
diff --git a/adapters/smaato/smaato_test.go b/adapters/smaato/smaato_test.go
new file mode 100644
index 00000000..cf76d58d
--- /dev/null
+++ b/adapters/smaato/smaato_test.go
@@ -0,0 +1,11 @@
+package smaato
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+)
+
+func TestJsonSamples(t *testing.T) {
+	adapterstest.RunJSONBidderTest(t, "smaatotest", NewSmaatoBidder("https://prebid/bidder"))
+}
diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json b/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json
new file mode 100644
index 00000000..7b662e88
--- /dev/null
+++ b/adapters/smaato/smaatotest/exemplary/simple-banner-richMedia.json
@@ -0,0 +1,194 @@
+{
+  "mockBidRequest": {
+    "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+    "site": {
+      "publisher": {
+        "id": "1100042525"
+      },
+      "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder",
+      "ext": {
+        "data": {
+          "keywords": "power tools",
+          "search": "drill",
+          "content": {
+            "userrating": 4
+          }
+        }
+      }
+    },
+    "imp": [
+      {
+        "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 50
+            },
+            {
+              "w": 320,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "1100042525",
+            "adspaceId": "130563103"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "user": {
+      "ext": {
+        "consent": "gdprConsentString",
+        "data": {
+          "keywords": "a,b",
+          "gender": "M",
+          "yob": 1984,
+          "geo": {
+            "country": "ca"
+          }
+        }
+      }
+    },
+    "regs": {
+      "coppa": 1,
+      "ext": {
+        "gdpr": 1,
+        "us_privacy": "uspConsentString"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "uri": "https://prebid/bidder",
+        "body": {
+          "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+          "imp": [
+            {
+              "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+              "tagid": "130563103",
+              "banner": {
+                "h": 50,
+                "w": 320,
+                "format": [
+                  {
+                    "w": 320,
+                    "h": 50
+                  },
+                  {
+                    "w": 320,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ],
+          "user": {
+            "gender": "M",
+            "keywords": "a,b",
+            "yob": 1984,
+            "ext": {
+              "consent": "gdprConsentString"
+            }
+          },
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "regs": {
+            "coppa": 1,
+            "ext": {
+              "gdpr": 1,
+              "us_privacy": "uspConsentString"
+            }
+          },
+          "site": {
+            "publisher": {
+              "id": "1100042525"
+            },
+            "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder",
+            "keywords": "power tools"
+          },
+          "ext": {
+            "client": "prebid_server_0.1"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "5ebea288-f13a-4754-be6d-4ade66c68877",
+          "seatbid": [
+            {
+              "seat": "CM6523",
+              "bid": [
+                {
+                  "adm": "{\"richmedia\":{\"mediadata\":{\"content\":\"<div>hello</div>\", \"w\":350,\"h\":50},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}",
+                  "adomain": [
+                    "smaato.com"
+                  ],
+                  "bidderName": "smaato",
+                  "cid": "CM6523",
+                  "crid": "CR69381",
+                  "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd",
+                  "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+                  "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png",
+                  "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D",
+                  "price": 0.01,
+                  "w": 350,
+                  "h": 50
+                }
+              ]
+            }
+          ],
+          "bidid": "04db8629-179d-4bcd-acce-e54722969006",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "adm": "<div onclick=\"fetch(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fclick%2F1'), {cache: 'no-cache'});fetch(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fclick%2F2'), {cache: 'no-cache'});\"><div>hello</div><img src=\"//prebid-test.smaatolabs.net/track/imp/1\" alt=\"\" width=\"0\" height=\"0\"/><img src=\"//prebid-test.smaatolabs.net/track/imp/2\" alt=\"\" width=\"0\" height=\"0\"/></div>",
+            "adomain": [
+              "smaato.com"
+            ],
+            "cid": "CM6523",
+            "crid": "CR69381",
+            "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd",
+            "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+            "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png",
+            "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D",
+            "price": 0.01,
+            "w": 350,
+            "h": 50
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner.json b/adapters/smaato/smaatotest/exemplary/simple-banner.json
new file mode 100644
index 00000000..a50fd928
--- /dev/null
+++ b/adapters/smaato/smaatotest/exemplary/simple-banner.json
@@ -0,0 +1,190 @@
+{
+  "mockBidRequest": {
+    "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+    "site": {
+      "publisher": {
+        "id": "1100042525"
+      },
+      "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder",
+      "ext": {
+        "data": {
+          "keywords": "power tools",
+          "search": "drill",
+          "content": {
+            "userrating": 4
+          }
+        }
+      }
+    },
+    "imp": [
+      {
+        "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 50
+            },
+            {
+              "w": 320,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "1100042525",
+            "adspaceId": "130563103"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "user": {
+      "ext": {
+        "consent": "gdprConsentString",
+        "data": {
+          "keywords": "a,b",
+          "gender": "M",
+          "yob": 1984,
+          "geo": {
+            "country": "ca"
+          }
+        }
+      }
+    },
+    "regs": {
+      "coppa": 1,
+      "ext": {
+        "gdpr": 1,
+        "us_privacy": "uspConsentString"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": ["application/json;charset=utf-8"],
+          "Accept": ["application/json"]
+        },
+        "uri": "https://prebid/bidder",
+        "body": {
+          "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+          "imp": [
+            {
+              "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+              "tagid": "130563103",
+              "banner": {
+                "h": 50,
+                "w": 320,
+                "format": [
+                  {
+                    "w": 320,
+                    "h": 50
+                  },
+                  {
+                    "w": 320,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ],
+          "user": {
+            "ext": {
+              "consent": "gdprConsentString"
+            },
+            "gender": "M",
+            "keywords": "a,b",
+            "yob": 1984
+          },
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "regs": {
+            "coppa": 1,
+            "ext": {
+              "gdpr": 1,
+              "us_privacy": "uspConsentString"
+            }
+          },
+          "site": {
+            "publisher": {
+              "id": "1100042525"
+            },
+            "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder",
+            "keywords": "power tools"
+          },
+          "ext": {
+            "client": "prebid_server_0.1"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "5ebea288-f13a-4754-be6d-4ade66c68877",
+          "seatbid": [
+            {
+              "seat": "CM6523",
+              "bid": [
+                {
+                  "adm": "{\"image\":{\"img\":{\"url\":\"//prebid-test.smaatolabs.net/img/320x50.jpg\",\"w\":350,\"h\":50,\"ctaurl\":\"//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}",
+                  "adomain": [
+                    "smaato.com"
+                  ],
+                  "bidderName": "smaato",
+                  "cid": "CM6523",
+                  "crid": "CR69381",
+                  "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd",
+                  "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+                  "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png",
+                  "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D",
+                  "price": 0.01,
+                  "w": 350,
+                  "h": 50
+                }
+              ]
+            }
+          ],
+          "bidid": "04db8629-179d-4bcd-acce-e54722969006",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "adm": "<div style=\"cursor:pointer\" onclick=\"fetch(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fclick%2F1'.replace(/\\+/g, ' ')), {cache: 'no-cache'});fetch(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fclick%2F2'.replace(/\\+/g, ' ')), {cache: 'no-cache'});;window.open(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fctaurl%2F1'.replace(/\\+/g, ' ')));\"><img src=\"//prebid-test.smaatolabs.net/img/320x50.jpg\" width=\"350\" height=\"50\"/><img src=\"//prebid-test.smaatolabs.net/track/imp/1\" alt=\"\" width=\"0\" height=\"0\"/><img src=\"//prebid-test.smaatolabs.net/track/imp/2\" alt=\"\" width=\"0\" height=\"0\"/></div>",
+            "adomain": [
+              "smaato.com"
+            ],
+            "cid": "CM6523",
+            "crid": "CR69381",
+            "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd",
+            "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+            "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png",
+            "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D",
+            "price": 0.01,
+            "w": 350,
+            "h": 50
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/smaato/smaatotest/params/banner.json b/adapters/smaato/smaatotest/params/banner.json
new file mode 100644
index 00000000..a84c44d4
--- /dev/null
+++ b/adapters/smaato/smaatotest/params/banner.json
@@ -0,0 +1,4 @@
+{
+  "publisherId": "1100042525",
+  "adspaceId": "130563103"
+}
\ No newline at end of file
diff --git a/adapters/smaato/smaatotest/supplemental/bad-adm-response.json b/adapters/smaato/smaatotest/supplemental/bad-adm-response.json
new file mode 100644
index 00000000..6d4990e9
--- /dev/null
+++ b/adapters/smaato/smaatotest/supplemental/bad-adm-response.json
@@ -0,0 +1,166 @@
+{
+  "mockBidRequest": {
+    "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+    "site": {
+      "publisher": {
+        "id": "1100042525"
+      },
+      "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder",
+      "ext": {
+        "data": {
+          "keywords": "power tools",
+          "search": "drill",
+          "content": {
+            "userrating": 4
+          }
+        }
+      }
+    },
+    "imp": [
+      {
+        "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 50
+            },
+            {
+              "w": 320,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "1100042525",
+            "adspaceId": "130563103"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "user": {
+      "ext": {
+        "consent": "gdprConsentString",
+        "data": {
+          "keywords": "a,b",
+          "gender": "M",
+          "yob": 1984,
+          "geo": {
+            "country": "ca"
+          }
+        }
+      }
+    },
+    "regs": {
+      "coppa": 1,
+      "ext": {
+        "gdpr": 1,
+        "us_privacy": "uspConsentString"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": ["application/json;charset=utf-8"],
+          "Accept": ["application/json"]
+        },
+        "uri": "https://prebid/bidder",
+        "body": {
+          "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+          "imp": [
+            {
+              "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+              "tagid": "130563103",
+              "banner": {
+                "h": 50,
+                "w": 320,
+                "format": [
+                  {
+                    "w": 320,
+                    "h": 50
+                  },
+                  {
+                    "w": 320,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ],
+          "user": {
+            "ext": {
+              "consent": "gdprConsentString"
+            },
+            "gender": "M",
+            "keywords": "a,b",
+            "yob": 1984
+          },
+          "device": {
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "language": "en",
+            "dnt": 0
+          },
+          "regs": {
+            "coppa": 1,
+            "ext": {
+              "gdpr": 1,
+              "us_privacy": "uspConsentString"
+            }
+          },
+          "site": {
+            "publisher": {
+              "id": "1100042525"
+            },
+            "page": "http://localhost:3000/server.html?pbjs_debug=true&endpoint=http://localhost:3000/bidder",
+            "keywords": "power tools"
+          },
+          "ext": {
+            "client": "prebid_server_0.1"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "5ebea288-f13a-4754-be6d-4ade66c68877",
+          "seatbid": [
+            {
+              "seat": "CM6523",
+              "bid": [
+                {
+                  "adm": "{\"badmedia\":{\"mediadata\":{\"content\":\"<div>hello</div>\", \"w\":350,\"h\":50},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}",
+                  "adomain": [
+                    "smaato.com"
+                  ],
+                  "bidderName": "smaato",
+                  "cid": "CM6523",
+                  "crid": "CR69381",
+                  "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd",
+                  "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+                  "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png",
+                  "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D",
+                  "price": 0.01,
+                  "w": 350,
+                  "h": 50
+                }
+              ]
+            }
+          ],
+          "bidid": "04db8629-179d-4bcd-acce-e54722969006",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": []
+}
\ No newline at end of file
diff --git a/adapters/smaato/smaatotest/supplemental/bad-ext-req.json b/adapters/smaato/smaatotest/supplemental/bad-ext-req.json
new file mode 100644
index 00000000..0c970fc5
--- /dev/null
+++ b/adapters/smaato/smaatotest/supplemental/bad-ext-req.json
@@ -0,0 +1,54 @@
+{
+  "mockBidRequest": {
+    "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "imp": [
+      {
+        "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 50
+            },
+            {
+              "w": 320,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+        }
+      }
+    ],
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "user": {
+      "ext": {
+        "consent": "gdprConsentString"
+      }
+    },
+    "regs": {
+      "coppa": 1,
+      "ext": {
+        "gdpr": 1,
+        "us_privacy": "uspConsentString"
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Key path not found",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/smaato/smaatotest/supplemental/bad-imp-banner-format-req.json b/adapters/smaato/smaatotest/supplemental/bad-imp-banner-format-req.json
new file mode 100644
index 00000000..b9560f0f
--- /dev/null
+++ b/adapters/smaato/smaatotest/supplemental/bad-imp-banner-format-req.json
@@ -0,0 +1,61 @@
+{
+  "mockBidRequest": {
+    "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+    "imp": [
+      {
+        "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+        "banner": {
+          "format": []
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "1100042525",
+            "adspaceId": "130563103"
+          }
+        }
+      }
+    ],
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "1"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": ["application/json;charset=utf-8"],
+          "Accept": ["application/json"]
+        },
+        "uri": "https://prebid/bidder",
+        "body": {
+          "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+          "imp": [],
+          "site": {
+            "page": "prebid.org",
+            "publisher": {
+              "id": "1100042525"
+            }
+          },
+          "ext": {
+            "client": "prebid_server_0.1"
+          }
+        }
+      }
+    }
+  ],
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "No sizes provided for Banner []",
+      "comparison": "literal"
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "unexpected status code: 0. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/smaato/smaatotest/supplemental/bad-user-ext-data-req.json b/adapters/smaato/smaatotest/supplemental/bad-user-ext-data-req.json
new file mode 100644
index 00000000..9e65fce1
--- /dev/null
+++ b/adapters/smaato/smaatotest/supplemental/bad-user-ext-data-req.json
@@ -0,0 +1,67 @@
+{
+  "mockBidRequest": {
+    "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "imp": [
+      {
+        "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 50
+            },
+            {
+              "w": 320,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "1100042525",
+            "adspaceId": "130563103"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "user": {
+      "gender": "M",
+      "ext": {
+        "data": {
+          "keywords":"a,b",
+          "gender": "M",
+          "yob": "",
+          "geo": {
+            "country": "ca"
+          }
+        },
+        "consent":"yes"
+      }
+    },
+    "regs": {
+      "coppa": 1,
+      "ext": {
+        "gdpr": 1,
+        "us_privacy": "uspConsentString"
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "json: cannot unmarshal string into Go struct field userExtData.data.yob of type int64",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/smaato/smaatotest/supplemental/bad-user-ext-req.json b/adapters/smaato/smaatotest/supplemental/bad-user-ext-req.json
new file mode 100644
index 00000000..7f05b2df
--- /dev/null
+++ b/adapters/smaato/smaatotest/supplemental/bad-user-ext-req.json
@@ -0,0 +1,57 @@
+{
+  "mockBidRequest": {
+    "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "imp": [
+      {
+        "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 50
+            },
+            {
+              "w": 320,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "1100042525",
+            "adspaceId": "130563103"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "language": "en",
+      "dnt": 0
+    },
+    "user": {
+      "gender": "M",
+      "ext": 99
+    },
+    "regs": {
+      "coppa": 1,
+      "ext": {
+        "gdpr": 1,
+        "us_privacy": "uspConsentString"
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "json: cannot unmarshal number into Go value of type map[string]json.RawMessage",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/smaato/smaatotest/supplemental/no-consent-info.json b/adapters/smaato/smaatotest/supplemental/no-consent-info.json
new file mode 100644
index 00000000..9e0ccfdc
--- /dev/null
+++ b/adapters/smaato/smaatotest/supplemental/no-consent-info.json
@@ -0,0 +1,137 @@
+{
+  "mockBidRequest": {
+    "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "1"
+      }
+    },
+    "imp": [
+      {
+        "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+        "banner": {
+          "format": [
+            {
+              "w": 320,
+              "h": 50
+            },
+            {
+              "w": 320,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "publisherId": "1100042525",
+            "adspaceId": "130563103"
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "headers": {
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ],
+          "Accept": [
+            "application/json"
+          ]
+        },
+        "uri": "https://prebid/bidder",
+        "body": {
+          "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+          "imp": [
+            {
+              "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+              "tagid": "130563103",
+              "banner": {
+                "h": 50,
+                "w": 320,
+                "format": [
+                  {
+                    "w": 320,
+                    "h": 50
+                  },
+                  {
+                    "w": 320,
+                    "h": 250
+                  }
+                ]
+              }
+            }
+          ],
+          "site": {
+            "page": "prebid.org",
+            "publisher": {
+              "id": "1100042525"
+            }
+          },
+          "ext": {
+            "client": "prebid_server_0.1"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "5ebea288-f13a-4754-be6d-4ade66c68877",
+          "seatbid": [
+            {
+              "seat": "CM6523",
+              "bid": [
+                {
+                  "adm": "{\"image\":{\"img\":{\"url\":\"//prebid-test.smaatolabs.net/img/320x50.jpg\",\"w\":350,\"h\":50,\"ctaurl\":\"//prebid-test.smaatolabs.net/track/ctaurl/1\"},\"impressiontrackers\":[\"//prebid-test.smaatolabs.net/track/imp/1\",\"//prebid-test.smaatolabs.net/track/imp/2\"],\"clicktrackers\":[\"//prebid-test.smaatolabs.net/track/click/1\",\"//prebid-test.smaatolabs.net/track/click/2\"]}}",
+                  "adomain": [
+                    "smaato.com"
+                  ],
+                  "bidderName": "smaato",
+                  "cid": "CM6523",
+                  "crid": "CR69381",
+                  "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd",
+                  "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+                  "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png",
+                  "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D",
+                  "price": 0.01,
+                  "w": 350,
+                  "h": 50
+                }
+              ]
+            }
+          ],
+          "bidid": "04db8629-179d-4bcd-acce-e54722969006",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "adm": "<div style=\"cursor:pointer\" onclick=\"fetch(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fclick%2F1'.replace(/\\+/g, ' ')), {cache: 'no-cache'});fetch(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fclick%2F2'.replace(/\\+/g, ' ')), {cache: 'no-cache'});;window.open(decodeURIComponent('%2F%2Fprebid-test.smaatolabs.net%2Ftrack%2Fctaurl%2F1'.replace(/\\+/g, ' ')));\"><img src=\"//prebid-test.smaatolabs.net/img/320x50.jpg\" width=\"350\" height=\"50\"/><img src=\"//prebid-test.smaatolabs.net/track/imp/1\" alt=\"\" width=\"0\" height=\"0\"/><img src=\"//prebid-test.smaatolabs.net/track/imp/2\" alt=\"\" width=\"0\" height=\"0\"/></div>",
+            "adomain": [
+              "smaato.com"
+            ],
+            "cid": "CM6523",
+            "crid": "CR69381",
+            "id": "6906aae8-7f74-4edd-9a4f-f49379a3cadd",
+            "impid": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+            "iurl": "https://bidstalkcreatives.s3.amazonaws.com/1x1.png",
+            "nurl": "https://ets-eu-west-1.track.smaato.net/v1/view?sessionId=e4e17adb-9599-42b1-bb5f-a1f1b3bee572&adSourceId=6906aae8-7f74-4edd-9a4f-f49379a3cadd&originalRequestTime=1552310449698&expires=1552311350698&winurl=ama8JbpJVpFWxvEja5viE3cLXFu58qRI8dGUh23xtsOn3N2-5UU0IwkgNEmR82pI37fcMXejL5IWTNAoW6Cnsjf-Dxl_vx2dUqMrVEevX-Vdx2VVnf-D5f73gZhvi4t36iPL8Dsw4aACekoLvVOV7-eXDjz7GHy60QFqcwKf5g2AlKPOInyZ6vJg_fn4qA9argvCRgwVybXE9Ndm2W0v8La4uFYWpJBOUveDDUrSQfzal7RsYvLb_OyaMlPHdrd_bwA9qqZWuyJXd-L9lxr7RQ%3D%3D%7CMw3kt91KJR0Uy5L-oNztAg%3D%3D&dpid=4XVofb_lH-__hr2JNGhKfg%3D%3D%7Cr9ciCU1cx3zmHXihItKO0g%3D%3D",
+            "price": 0.01,
+            "w": 350,
+            "h": 50
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/smaato/smaatotest/supplemental/no-imp-req.json b/adapters/smaato/smaatotest/supplemental/no-imp-req.json
new file mode 100644
index 00000000..bfaf51e6
--- /dev/null
+++ b/adapters/smaato/smaatotest/supplemental/no-imp-req.json
@@ -0,0 +1,17 @@
+{
+  "mockBidRequest": {
+    "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
+    "site": {
+      "page": "prebid.org",
+      "publisher": {
+        "id": "1"
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "no impressions in bid request",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/analytics/config/config_test.go b/analytics/config/config_test.go
index 7d97fb5f..583d475e 100644
--- a/analytics/config/config_test.go
+++ b/analytics/config/config_test.go
@@ -1,6 +1,7 @@
 package config
 
 import (
+	"github.com/stretchr/testify/assert"
 	"net/http"
 	"os"
 	"testing"
@@ -73,6 +74,13 @@ func initAnalytics(count *int) analytics.PBSAnalyticsModule {
 }
 
 func TestNewPBSAnalytics(t *testing.T) {
+	pbsAnalytics := NewPBSAnalytics(&config.Analytics{})
+	instance := pbsAnalytics.(enabledAnalytics)
+
+	assert.Equal(t, len(instance), 0)
+}
+
+func TestNewPBSAnalytics_FileLogger(t *testing.T) {
 	if _, err := os.Stat(TEST_DIR); os.IsNotExist(err) {
 		if err = os.MkdirAll(TEST_DIR, 0755); err != nil {
 			t.Fatalf("Could not create test directory for FileLogger")
@@ -88,4 +96,37 @@ func TestNewPBSAnalytics(t *testing.T) {
 	default:
 		t.Fatalf("Failed to initialize analytics module")
 	}
+
+	pbsAnalytics := NewPBSAnalytics(&config.Analytics{File: config.FileLogs{Filename: TEST_DIR + "/test"}})
+	instance := pbsAnalytics.(enabledAnalytics)
+
+	assert.Equal(t, len(instance), 1)
+}
+
+func TestNewPBSAnalytics_Pubstack(t *testing.T) {
+
+	pbsAnalyticsWithoutError := NewPBSAnalytics(&config.Analytics{
+		Pubstack: config.Pubstack{
+			Enabled:   true,
+			ScopeId:   "scopeId",
+			IntakeUrl: "https://pubstack.io/intake",
+			Buffers: config.PubstackBuffer{
+				BufferSize: "100KB",
+				EventCount: 0,
+				Timeout:    "30s",
+			},
+			ConfRefresh: "2h",
+		},
+	})
+	instanceWithoutError := pbsAnalyticsWithoutError.(enabledAnalytics)
+
+	assert.Equal(t, len(instanceWithoutError), 1)
+
+	pbsAnalyticsWithError := NewPBSAnalytics(&config.Analytics{
+		Pubstack: config.Pubstack{
+			Enabled: true,
+		},
+	})
+	instanceWithError := pbsAnalyticsWithError.(enabledAnalytics)
+	assert.Equal(t, len(instanceWithError), 0)
 }
diff --git a/analytics/pubstack/config_test.go b/analytics/pubstack/config_test.go
new file mode 100644
index 00000000..bb6fd0bd
--- /dev/null
+++ b/analytics/pubstack/config_test.go
@@ -0,0 +1,102 @@
+package pubstack
+
+import (
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+)
+
+func TestFetchConfig(t *testing.T) {
+	configResponse := `{
+		"scopeId":  "scopeId",
+		"endpoint": "https://pubstack.io",
+		"features": {
+			"auction":    true,
+			"cookiesync": true,
+			"amp":        true,
+			"setuid":     false,
+			"video":      false
+		}
+	}`
+
+	server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		defer req.Body.Close()
+		res.Write([]byte(configResponse))
+		res.WriteHeader(200)
+	}))
+
+	defer server.Close()
+
+	endpoint, _ := url.Parse(server.URL)
+	cfg, _ := fetchConfig(server.Client(), endpoint)
+
+	assert.Equal(t, cfg.ScopeID, "scopeId")
+	assert.Equal(t, cfg.Endpoint, "https://pubstack.io")
+	assert.Equal(t, cfg.Features[auction], true)
+	assert.Equal(t, cfg.Features[cookieSync], true)
+	assert.Equal(t, cfg.Features[amp], true)
+	assert.Equal(t, cfg.Features[setUID], false)
+	assert.Equal(t, cfg.Features[video], false)
+}
+
+func TestFetchConfig_Error(t *testing.T) {
+	configResponse := `{
+		"error":  "scopeId",
+	}`
+
+	server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		defer req.Body.Close()
+		res.Write([]byte(configResponse))
+		res.WriteHeader(200)
+	}))
+
+	defer server.Close()
+
+	endpoint, _ := url.Parse(server.URL)
+	cfg, err := fetchConfig(server.Client(), endpoint)
+
+	assert.Nil(t, cfg)
+	assert.NotNil(t, err)
+}
+
+func TestIsSameAs(t *testing.T) {
+	copyConfig := func(conf Configuration) *Configuration {
+		newConfig := conf
+		newConfig.Features = make(map[string]bool)
+		for k := range conf.Features {
+			newConfig.Features[k] = conf.Features[k]
+		}
+		return &newConfig
+	}
+
+	a := &Configuration{
+		ScopeID:  "scopeId",
+		Endpoint: "endpoint",
+		Features: map[string]bool{
+			"auction":    true,
+			"cookiesync": true,
+			"amp":        true,
+			"setuid":     false,
+			"video":      false,
+		},
+	}
+
+	assert.True(t, a.isSameAs(copyConfig(*a)))
+
+	b := copyConfig(*a)
+	b.ScopeID = "anotherId"
+	assert.False(t, a.isSameAs(b))
+
+	b = copyConfig(*a)
+	b.Endpoint = "anotherEndpoint"
+	assert.False(t, a.isSameAs(b))
+
+	b = copyConfig(*a)
+	b.Features["auction"] = true
+	assert.True(t, a.isSameAs(b))
+	b.Features["auction"] = false
+	assert.False(t, a.isSameAs(b))
+
+}
diff --git a/analytics/pubstack/eventchannel/eventchannel_test.go b/analytics/pubstack/eventchannel/eventchannel_test.go
new file mode 100644
index 00000000..9fdcfe97
--- /dev/null
+++ b/analytics/pubstack/eventchannel/eventchannel_test.go
@@ -0,0 +1,136 @@
+package eventchannel
+
+import (
+	"bytes"
+	"compress/gzip"
+	"github.com/stretchr/testify/assert"
+	"io/ioutil"
+	"sync"
+	"testing"
+	"time"
+)
+
+var maxByteSize = int64(15)
+var maxEventCount = int64(3)
+var maxTime = 2 * time.Hour
+
+func readGz(encoded bytes.Buffer) string {
+	gr, _ := gzip.NewReader(bytes.NewBuffer(encoded.Bytes()))
+	defer gr.Close()
+
+	decoded, _ := ioutil.ReadAll(gr)
+	return string(decoded)
+}
+
+func newSender(data *[]byte) Sender {
+	mux := &sync.Mutex{}
+	return func(payload []byte) error {
+		mux.Lock()
+		defer mux.Unlock()
+		event := bytes.Buffer{}
+		event.Write(payload)
+		*data = append(*data, readGz(event)...)
+		return nil
+	}
+}
+
+func TestEventChannel_isBufferFull(t *testing.T) {
+
+	send := func(_ []byte) error { return nil }
+
+	eventChannel := NewEventChannel(send, maxByteSize, maxEventCount, maxTime)
+	defer eventChannel.Close()
+
+	eventChannel.buffer([]byte("one"))
+	eventChannel.buffer([]byte("two"))
+
+	assert.Equal(t, eventChannel.isBufferFull(), false)
+
+	eventChannel.buffer([]byte("three"))
+
+	assert.Equal(t, eventChannel.isBufferFull(), true)
+
+	eventChannel.reset()
+
+	assert.Equal(t, eventChannel.isBufferFull(), false)
+
+	eventChannel.buffer([]byte("big-event-abcdefghijklmnopqrstuvwxyz"))
+
+	assert.Equal(t, eventChannel.isBufferFull(), true)
+
+}
+
+func TestEventChannel_reset(t *testing.T) {
+	send := func(_ []byte) error { return nil }
+
+	eventChannel := NewEventChannel(send, maxByteSize, maxEventCount, maxTime)
+	defer eventChannel.Close()
+
+	assert.Equal(t, eventChannel.metrics.eventCount, int64(0))
+	assert.Equal(t, eventChannel.metrics.bufferSize, int64(0))
+
+	eventChannel.buffer([]byte("one"))
+	eventChannel.buffer([]byte("two"))
+
+	assert.NotEqual(t, eventChannel.metrics.eventCount, int64(0))
+	assert.NotEqual(t, eventChannel.metrics.bufferSize, int64(0))
+
+	eventChannel.reset()
+
+	assert.Equal(t, eventChannel.buff.Len(), 0)
+	assert.Equal(t, eventChannel.metrics.eventCount, int64(0))
+	assert.Equal(t, eventChannel.metrics.bufferSize, int64(0))
+}
+
+func TestEventChannel_flush(t *testing.T) {
+	data := make([]byte, 0)
+	send := newSender(&data)
+
+	eventChannel := NewEventChannel(send, maxByteSize, maxEventCount, maxTime)
+	defer eventChannel.Close()
+
+	eventChannel.buffer([]byte("one"))
+	eventChannel.buffer([]byte("two"))
+	eventChannel.buffer([]byte("three"))
+	eventChannel.flush()
+	time.Sleep(10 * time.Millisecond)
+
+	assert.Equal(t, string(data), "onetwothree")
+}
+
+func TestEventChannel_close(t *testing.T) {
+	data := make([]byte, 0)
+	send := newSender(&data)
+
+	eventChannel := NewEventChannel(send, 15000, 15000, 2*time.Hour)
+
+	eventChannel.buffer([]byte("one"))
+	eventChannel.buffer([]byte("two"))
+	eventChannel.buffer([]byte("three"))
+	eventChannel.Close()
+
+	time.Sleep(10 * time.Millisecond)
+
+	assert.Equal(t, string(data), "onetwothree")
+}
+
+func TestEventChannel_Push(t *testing.T) {
+	data := make([]byte, 0)
+	send := newSender(&data)
+
+	eventChannel := NewEventChannel(send, 15000, 5, 5*time.Millisecond)
+	defer eventChannel.Close()
+
+	eventChannel.Push([]byte("one"))
+	eventChannel.Push([]byte("two"))
+	eventChannel.Push([]byte("three"))
+	eventChannel.Push([]byte("four"))
+	eventChannel.Push([]byte("five"))
+	eventChannel.Push([]byte("six"))
+	eventChannel.Push([]byte("seven"))
+
+	time.Sleep(10 * time.Millisecond)
+
+	assert.Equal(t, string(data), "onetwothreefourfivesixseven")
+
+}
diff --git a/analytics/pubstack/eventchannel/sender_test.go b/analytics/pubstack/eventchannel/sender_test.go
new file mode 100644
index 00000000..1185435e
--- /dev/null
+++ b/analytics/pubstack/eventchannel/sender_test.go
@@ -0,0 +1,40 @@
+package eventchannel
+
+import (
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBuildEndpointSender(t *testing.T) {
+	requestBody := make([]byte, 10)
+	server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		defer req.Body.Close()
+		requestBody, _ = ioutil.ReadAll(req.Body)
+		res.WriteHeader(200)
+	}))
+
+	defer server.Close()
+
+	sender := BuildEndpointSender(server.Client(), server.URL, "module")
+	err := sender([]byte("message"))
+
+	assert.Equal(t, requestBody, []byte("message"))
+	assert.Nil(t, err)
+}
+
+func TestBuildEndpointSender_Error(t *testing.T) {
+	server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		res.WriteHeader(400)
+	}))
+
+	defer server.Close()
+
+	sender := BuildEndpointSender(server.Client(), server.URL, "module")
+	err := sender([]byte("message"))
+
+	assert.NotNil(t, err)
+}
diff --git a/analytics/pubstack/helpers/json_test.go b/analytics/pubstack/helpers/json_test.go
new file mode 100644
index 00000000..4e36e8db
--- /dev/null
+++ b/analytics/pubstack/helpers/json_test.go
@@ -0,0 +1,61 @@
+package helpers
+
+import (
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/analytics"
+	"github.com/prebid/prebid-server/usersync"
+	"net/http"
+	"testing"
+)
+
+func TestJsonifyAuctionObject(t *testing.T) {
+	ao := &analytics.AuctionObject{
+		Status: http.StatusOK,
+	}
+	if _, err := JsonifyAuctionObject(ao, "scopeId"); err != nil {
+		t.Fail()
+	}
+
+}
+
+func TestJsonifyVideoObject(t *testing.T) {
+	vo := &analytics.VideoObject{
+		Status: http.StatusOK,
+	}
+	if _, err := JsonifyVideoObject(vo, "scopeId"); err != nil {
+		t.Fail()
+	}
+}
+
+func TestJsonifyCookieSync(t *testing.T) {
+	cso := &analytics.CookieSyncObject{
+		Status:       http.StatusOK,
+		BidderStatus: []*usersync.CookieSyncBidders{},
+	}
+	if _, err := JsonifyCookieSync(cso, "scopeId"); err != nil {
+		t.Fail()
+	}
+}
+
+func TestJsonifySetUIDObject(t *testing.T) {
+	so := &analytics.SetUIDObject{
+		Status: http.StatusOK,
+		Bidder: "any-bidder",
+		UID:    "uid string",
+	}
+	if _, err := JsonifySetUIDObject(so, "scopeId"); err != nil {
+		t.Fail()
+	}
+}
+
+func TestJsonifyAmpObject(t *testing.T) {
+	ao := &analytics.AmpObject{
+		Status:             http.StatusOK,
+		Errors:             make([]error, 0),
+		AuctionResponse:    &openrtb.BidResponse{},
+		AmpTargetingValues: map[string]string{},
+	}
+	if _, err := JsonifyAmpObject(ao, "scopeId"); err != nil {
+		t.Fail()
+	}
+}
diff --git a/analytics/pubstack/pubstack_module_test.go b/analytics/pubstack/pubstack_module_test.go
new file mode 100644
index 00000000..8d4dfdd6
--- /dev/null
+++ b/analytics/pubstack/pubstack_module_test.go
@@ -0,0 +1,186 @@
+package pubstack
+
+import (
+	"encoding/json"
+	"github.com/prebid/prebid-server/analytics/pubstack/eventchannel"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/analytics"
+	"github.com/stretchr/testify/assert"
+)
+
+func loadJsonFromFile() (*analytics.AuctionObject, error) {
+	req, err := os.Open("mocks/mock_openrtb_request.json")
+	if err != nil {
+		return nil, err
+	}
+	defer req.Close()
+
+	reqCtn := openrtb.BidRequest{}
+	reqPayload, err := ioutil.ReadAll(req)
+	if err != nil {
+		return nil, err
+	}
+
+	err = json.Unmarshal(reqPayload, &reqCtn)
+	if err != nil {
+		return nil, err
+	}
+
+	res, err := os.Open("mocks/mock_openrtb_response.json")
+	if err != nil {
+		return nil, err
+	}
+	defer res.Close()
+
+	resCtn := openrtb.BidResponse{}
+	resPayload, err := ioutil.ReadAll(res)
+	if err != nil {
+		return nil, err
+	}
+
+	err = json.Unmarshal(resPayload, &resCtn)
+	if err != nil {
+		return nil, err
+	}
+
+	return &analytics.AuctionObject{
+		Request:  &reqCtn,
+		Response: &resCtn,
+	}, nil
+}
+
+func TestPubstackModule(t *testing.T) {
+
+	remoteConfig := &Configuration{}
+	server := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
+		defer req.Body.Close()
+		data, _ := json.Marshal(remoteConfig)
+		res.Write(data)
+	}))
+	client := server.Client()
+
+	defer server.Close()
+
+	// Loading Issues
+	_, err := NewPubstackModule(client, "scope", server.URL, "1z", 100, "90MB", "15m")
+	assert.NotNil(t, err) // should raise an error since  we can't parse args // configRefreshDelay
+
+	_, err = NewPubstackModule(client, "scope", server.URL, "1h", 100, "90z", "15m")
+	assert.NotNil(t, err) // should raise an error since  we can't parse args // maxByte
+
+	_, err = NewPubstackModule(client, "scope", server.URL, "1h", 100, "90MB", "15z")
+	assert.NotNil(t, err) // should raise an error since  we can't parse args // maxTime
+
+	// Loading OK
+	module, err := NewPubstackModule(client, "scope", server.URL, "10ms", 100, "90MB", "15m")
+	assert.Nil(t, err)
+
+	// Default Configuration
+	pubstack, ok := module.(*PubstackModule)
+	assert.Equal(t, ok, true) //PBSAnalyticsModule is also a PubstackModule
+	assert.Equal(t, len(pubstack.cfg.Features), 5)
+	assert.Equal(t, pubstack.cfg.Features[auction], false)
+	assert.Equal(t, pubstack.cfg.Features[video], false)
+	assert.Equal(t, pubstack.cfg.Features[amp], false)
+	assert.Equal(t, pubstack.cfg.Features[setUID], false)
+	assert.Equal(t, pubstack.cfg.Features[cookieSync], false)
+
+	assert.Equal(t, len(pubstack.eventChannels), 0)
+
+	// Process Auction Event
+	counter := 0
+	send := func(_ []byte) error {
+		counter++
+		return nil
+	}
+	mockedEvent, err := loadJsonFromFile()
+	if err != nil {
+		t.Fail()
+	}
+
+	pubstack.eventChannels[auction] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second)
+	pubstack.eventChannels[video] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second)
+	pubstack.eventChannels[amp] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second)
+	pubstack.eventChannels[setUID] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second)
+	pubstack.eventChannels[cookieSync] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second)
+
+	pubstack.LogAuctionObject(mockedEvent)
+	pubstack.LogAmpObject(&analytics.AmpObject{
+		Status: http.StatusOK,
+	})
+	pubstack.LogCookieSyncObject(&analytics.CookieSyncObject{
+		Status: http.StatusOK,
+	})
+	pubstack.LogVideoObject(&analytics.VideoObject{
+		Status: http.StatusOK,
+	})
+	pubstack.LogSetUIDObject(&analytics.SetUIDObject{
+		Status: http.StatusOK,
+	})
+
+	pubstack.closeAllEventChannels()
+	time.Sleep(10 * time.Millisecond) // process channel
+	assert.Equal(t, counter, 0)
+
+	// Hot-Reload config
+	newFeatures := make(map[string]bool)
+	newFeatures[auction] = true
+	newFeatures[video] = true
+	newFeatures[amp] = true
+	newFeatures[cookieSync] = true
+	newFeatures[setUID] = true
+
+	remoteConfig = &Configuration{
+		ScopeID:  "new-scope",
+		Endpoint: "new-endpoint",
+		Features: newFeatures,
+	}
+
+	endpoint, _ := url.Parse(server.URL)
+	pubstack.reloadConfig(endpoint)
+
+	time.Sleep(2 * time.Millisecond) // process channel
+	assert.Equal(t, len(pubstack.cfg.Features), 5)
+	assert.Equal(t, pubstack.cfg.Features[auction], true)
+	assert.Equal(t, pubstack.cfg.Features[video], true)
+	assert.Equal(t, pubstack.cfg.Features[amp], true)
+	assert.Equal(t, pubstack.cfg.Features[setUID], true)
+	assert.Equal(t, pubstack.cfg.Features[cookieSync], true)
+	assert.Equal(t, pubstack.cfg.ScopeID, "new-scope")
+	assert.Equal(t, pubstack.cfg.Endpoint, "new-endpoint")
+	assert.Equal(t, len(pubstack.eventChannels), 5)
+
+	counter = 0
+	pubstack.eventChannels[auction] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second)
+	pubstack.eventChannels[video] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second)
+	pubstack.eventChannels[amp] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second)
+	pubstack.eventChannels[setUID] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second)
+	pubstack.eventChannels[cookieSync] = eventchannel.NewEventChannel(send, 2000, 1, 10*time.Second)
+
+	pubstack.LogAuctionObject(mockedEvent)
+	pubstack.LogAmpObject(&analytics.AmpObject{
+		Status: http.StatusOK,
+	})
+	pubstack.LogCookieSyncObject(&analytics.CookieSyncObject{
+		Status: http.StatusOK,
+	})
+	pubstack.LogVideoObject(&analytics.VideoObject{
+		Status: http.StatusOK,
+	})
+	pubstack.LogSetUIDObject(&analytics.SetUIDObject{
+		Status: http.StatusOK,
+	})
+	pubstack.closeAllEventChannels()
+	time.Sleep(10 * time.Millisecond)
+
+	assert.Equal(t, counter, 5)
+
+}
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index 16955e97..11b60db0 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -247,3 +247,212 @@ func (f *mockFetcher) GetId(bidder openrtb_ext.BidderName) (string, bool) {
 func mockServer(w http.ResponseWriter, req *http.Request) {
 	w.Write([]byte("{}"))
 }
+
+type TargetingTestData struct {
+	Description                string
+	TargetData                 targetData
+	Auction                    auction
+	IsApp                      bool
+	CategoryMapping            map[string]string
+	ExpectedBidTargetsByBidder map[string]map[openrtb_ext.BidderName]map[string]string
+}
+
+var bid123 *openrtb.Bid = &openrtb.Bid{
+	Price: 1.23,
+}
+
+var bid111 *openrtb.Bid = &openrtb.Bid{
+	Price:  1.11,
+	DealID: "mydeal",
+}
+var bid084 *openrtb.Bid = &openrtb.Bid{
+	Price: 0.84,
+}
+
+var TargetingTests []TargetingTestData = []TargetingTestData{
+	{
+		Description: "Targeting winners only (most basic targeting example)",
+		TargetData: targetData{
+			priceGranularity: openrtb_ext.PriceGranularityFromString("med"),
+			includeWinners:   true,
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid084,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder": "appnexus",
+					"hb_pb":     "1.20",
+				},
+				openrtb_ext.BidderRubicon: {},
+			},
+		},
+	},
+	{
+		Description: "Targeting on bidders only",
+		TargetData: targetData{
+			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			includeBidderKeys: true,
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid084,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder_appnexus": "appnexus",
+					"hb_pb_appnexus":     "1.20",
+				},
+				openrtb_ext.BidderRubicon: {
+					"hb_bidder_rubicon": "rubicon",
+					"hb_pb_rubicon":     "0.80",
+				},
+			},
+		},
+	},
+	{
+		Description: "Full basic targeting with hd_format",
+		TargetData: targetData{
+			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			includeWinners:    true,
+			includeBidderKeys: true,
+			includeFormat:     true,
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid084,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder":          "appnexus",
+					"hb_bidder_appnexus": "appnexus",
+					"hb_pb":              "1.20",
+					"hb_pb_appnexus":     "1.20",
+					"hb_format":          "banner",
+					"hb_format_appnexus": "banner",
+				},
+				openrtb_ext.BidderRubicon: {
+					"hb_bidder_rubicon": "rubicon",
+					"hb_pb_rubicon":     "0.80",
+					"hb_format_rubicon": "banner",
+				},
+			},
+		},
+	},
+	{
+		Description: "Cache and deal targeting test",
+		TargetData: targetData{
+			priceGranularity:  openrtb_ext.PriceGranularityFromString("med"),
+			includeBidderKeys: true,
+			cacheHost:         "cache.prebid.com",
+			cachePath:         "cache",
+		},
+		Auction: auction{
+			winningBidsByBidder: map[string]map[openrtb_ext.BidderName]*pbsOrtbBid{
+				"ImpId-1": {
+					openrtb_ext.BidderAppnexus: {
+						bid:     bid123,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+					openrtb_ext.BidderRubicon: {
+						bid:     bid111,
+						bidType: openrtb_ext.BidTypeBanner,
+					},
+				},
+			},
+			cacheIds: map[*openrtb.Bid]string{
+				bid123: "55555",
+				bid111: "cacheme",
+			},
+		},
+		ExpectedBidTargetsByBidder: map[string]map[openrtb_ext.BidderName]map[string]string{
+			"ImpId-1": {
+				openrtb_ext.BidderAppnexus: {
+					"hb_bidder_appnexus":   "appnexus",
+					"hb_pb_appnexus":       "1.20",
+					"hb_cache_id_appnexus": "55555",
+					"hb_cache_host_appnex": "cache.prebid.com",
+					"hb_cache_path_appnex": "cache",
+				},
+				openrtb_ext.BidderRubicon: {
+					"hb_bidder_rubicon":    "rubicon",
+					"hb_pb_rubicon":        "1.10",
+					"hb_cache_id_rubicon":  "cacheme",
+					"hb_deal_rubicon":      "mydeal",
+					"hb_cache_host_rubico": "cache.prebid.com",
+					"hb_cache_path_rubico": "cache",
+				},
+			},
+		},
+	},
+}
+
+func TestSetTargeting(t *testing.T) {
+	for _, test := range TargetingTests {
+		auc := &test.Auction
+		// Set rounded prices from the auction data
+		auc.setRoundedPrices(test.TargetData.priceGranularity)
+		winningBids := make(map[string]*pbsOrtbBid)
+		// Set winning bids from the auction data
+		for imp, bidsByBidder := range auc.winningBidsByBidder {
+			for _, bid := range bidsByBidder {
+				if winningBid, ok := winningBids[imp]; ok {
+					if winningBid.bid.Price < bid.bid.Price {
+						winningBids[imp] = bid
+					}
+				} else {
+					winningBids[imp] = bid
+				}
+			}
+		}
+		auc.winningBids = winningBids
+		targData := test.TargetData
+		targData.setTargeting(auc, test.IsApp, test.CategoryMapping)
+		for imp, targetsByBidder := range test.ExpectedBidTargetsByBidder {
+			for bidder, expected := range targetsByBidder {
+				assert.Equal(t,
+					expected,
+					auc.winningBidsByBidder[imp][bidder].bidTargets,
+					"Test: %s\nTargeting failed for bidder %s on imp %s.",
+					test.Description,
+					string(bidder),
+					imp)
+			}
+		}
+	}
+
+}
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index 32ab2e73..22b215c3 100755
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -93,6 +93,7 @@ func TestNewSyncerMap(t *testing.T) {
 		openrtb_ext.BidderMobileFuse:   true,
 		openrtb_ext.BidderOrbidder:     true,
 		openrtb_ext.BidderPubnative:    true,
+		openrtb_ext.BidderSmaato:       true,
 		openrtb_ext.BidderTappx:        true,
 		openrtb_ext.BidderYeahmobi:     true,
 	}