Repository: prebid/prebid-server
Pull request #1525 (2020-10-07 10:28:56)

Fix missing Request parameter for Adgeneration Adapter
 ## Type of change
fix: add UA to RequestHeader.
fix: fix sizes query.

　　　　　　　　　　　
## Description of change
Change currency of ad-generation's bidResponse according to bidRequest

Maintainer: ssp-ope@supership.jp

AdGeneration: Added test items for the added parameters.
Also, there was a problem with some test code, so I have fixed it as well.

Patch:
diff --git a/adapters/adgeneration/adgeneration.go b/adapters/adgeneration/adgeneration.go
index 054fa7f6..08a7be08 100644
--- a/adapters/adgeneration/adgeneration.go
+++ b/adapters/adgeneration/adgeneration.go
@@ -54,6 +54,9 @@ func (adg *AdgenerationAdapter) MakeRequests(request *openrtb.BidRequest, reqInf
 	headers := http.Header{}
 	headers.Add("Content-Type", "application/json;charset=utf-8")
 	headers.Add("Accept", "application/json")
+	if request.Device != nil && len(request.Device.UA) > 0 {
+		headers.Add("User-Agent", request.Device.UA)
+	}
 
 	bidRequestArray := make([]*adapters.RequestData, 0, numRequests)
 
@@ -106,11 +109,14 @@ func (adg *AdgenerationAdapter) getRawQuery(id string, request *openrtb.BidReque
 	v.Set("adapterver", adg.version)
 	adSize := getSizes(imp)
 	if adSize != "" {
-		v.Set("size", adSize)
+		v.Set("sizes", adSize)
 	}
 	if request.Site != nil && request.Site.Page != "" {
 		v.Set("tp", request.Site.Page)
 	}
+	if request.Source != nil && request.Source.TID != "" {
+		v.Set("transactionid", request.Source.TID)
+	}
 	return &v
 }
 
@@ -135,7 +141,7 @@ func getSizes(imp *openrtb.Imp) string {
 	}
 	var sizeStr string
 	for _, v := range imp.Banner.Format {
-		sizeStr += strconv.FormatUint(v.W, 10) + "×" + strconv.FormatUint(v.H, 10) + ","
+		sizeStr += strconv.FormatUint(v.W, 10) + "x" + strconv.FormatUint(v.H, 10) + ","
 	}
 	if len(sizeStr) > 0 && strings.LastIndex(sizeStr, ",") == len(sizeStr)-1 {
 		sizeStr = sizeStr[:len(sizeStr)-1]
@@ -255,7 +261,7 @@ func removeWrapper(ad string) string {
 func NewAdgenerationAdapter(endpoint string) *AdgenerationAdapter {
 	return &AdgenerationAdapter{
 		endpoint,
-		"1.0.1",
+		"1.0.2",
 		"JPY",
 	}
 }
diff --git a/adapters/info.go b/adapters/info.go
index 982827c9..d94196d0 100644
--- a/adapters/info.go
+++ b/adapters/info.go
@@ -201,10 +201,11 @@ const (
 )
 
 type BidderInfo struct {
-	Status       BidderStatus      `yaml:"status" json:"status"`
-	Maintainer   *MaintainerInfo   `yaml:"maintainer" json:"maintainer"`
-	Capabilities *CapabilitiesInfo `yaml:"capabilities" json:"capabilities"`
-	AliasOf      string            `json:"aliasOf,omitempty"`
+	Status                  BidderStatus      `yaml:"status" json:"status"`
+	Maintainer              *MaintainerInfo   `yaml:"maintainer" json:"maintainer"`
+	Capabilities            *CapabilitiesInfo `yaml:"capabilities" json:"capabilities"`
+	AliasOf                 string            `json:"aliasOf,omitempty"`
+	ModifyingVastXmlAllowed bool              `yaml:"modifyingVastXmlAllowed" json:"-" xml:"-"`
 }
 
 type MaintainerInfo struct {
diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go
index 812095f9..b9272489 100644
--- a/adapters/rubicon/rubicon.go
+++ b/adapters/rubicon/rubicon.go
@@ -275,6 +275,7 @@ var rubiSizeMap = map[rubiSize]int{
 	{w: 320, h: 500}:   278,
 	{w: 320, h: 400}:   282,
 	{w: 640, h: 380}:   288,
+	{w: 500, h: 1000}:  548,
 }
 
 // defines the contract for bidrequest.user.ext.eids[i].ext
diff --git a/analytics/config/config.go b/analytics/config/config.go
index 7f7ded0f..fa63cb5a 100644
--- a/analytics/config/config.go
+++ b/analytics/config/config.go
@@ -69,3 +69,9 @@ func (ea enabledAnalytics) LogAmpObject(ao *analytics.AmpObject) {
 		module.LogAmpObject(ao)
 	}
 }
+
+func (ea enabledAnalytics) LogNotificationEventObject(ne *analytics.NotificationEvent) {
+	for _, module := range ea {
+		module.LogNotificationEventObject(ne)
+	}
+}
diff --git a/analytics/core.go b/analytics/core.go
index 737d1334..2cbf98d4 100644
--- a/analytics/core.go
+++ b/analytics/core.go
@@ -21,6 +21,7 @@ type PBSAnalyticsModule interface {
 	LogCookieSyncObject(*CookieSyncObject)
 	LogSetUIDObject(*SetUIDObject)
 	LogAmpObject(*AmpObject)
+	LogNotificationEventObject(*NotificationEvent)
 }
 
 //Loggable object of a transaction at /openrtb2/auction endpoint
@@ -67,3 +68,9 @@ type CookieSyncObject struct {
 	Errors       []error
 	BidderStatus []*usersync.CookieSyncBidders
 }
+
+// NotificationEvent is a loggable object
+type NotificationEvent struct {
+	Request *EventRequest   `json:"request"`
+	Account *config.Account `json:"account"`
+}
diff --git a/analytics/event.go b/analytics/event.go
new file mode 100644
index 00000000..5350223d
--- /dev/null
+++ b/analytics/event.go
@@ -0,0 +1,38 @@
+package analytics
+
+// EventType enumerates the values of events Prebid Server can receive for an ad.
+type EventType string
+
+// Possible values of events Prebid Server can receive for an ad.
+const (
+	Win EventType = "win"
+	Imp EventType = "imp"
+)
+
+// ResponseFormat enumerates the values of a Prebid Server event.
+type ResponseFormat string
+
+const (
+	// Blank describes an event which returns an HTTP 200 with an empty body.
+	Blank ResponseFormat = "b"
+	// Image describes an event which returns an HTTP 200 with a PNG body.
+	Image ResponseFormat = "i"
+)
+
+// Analytics indicates if the notification event should be handled or not
+type Analytics string
+
+const (
+	Enabled  Analytics = "1"
+	Disabled Analytics = "0"
+)
+
+type EventRequest struct {
+	Type      EventType      `json:"type,omitempty"`
+	Format    ResponseFormat `json:"format,omitempty"`
+	Analytics Analytics      `json:"analytics,omitempty"`
+	BidID     string         `json:"bidid,omitempty"`
+	AccountID string         `json:"account_id,omitempty"`
+	Bidder    string         `json:"bidder,omitempty"`
+	Timestamp int64          `json:"timestamp,omitempty"`
+}
diff --git a/analytics/filesystem/file_module.go b/analytics/filesystem/file_module.go
index 54d492ad..a0721d98 100644
--- a/analytics/filesystem/file_module.go
+++ b/analytics/filesystem/file_module.go
@@ -12,11 +12,12 @@ import (
 type RequestType string
 
 const (
-	COOKIE_SYNC RequestType = "/cookie_sync"
-	AUCTION     RequestType = "/openrtb2/auction"
-	VIDEO       RequestType = "/openrtb2/video"
-	SETUID      RequestType = "/set_uid"
-	AMP         RequestType = "/openrtb2/amp"
+	COOKIE_SYNC        RequestType = "/cookie_sync"
+	AUCTION            RequestType = "/openrtb2/auction"
+	VIDEO              RequestType = "/openrtb2/video"
+	SETUID             RequestType = "/set_uid"
+	AMP                RequestType = "/openrtb2/amp"
+	NOTIFICATION_EVENT RequestType = "/event"
 )
 
 //Module that can perform transactional logging
@@ -72,6 +73,18 @@ func (f *FileLogger) LogAmpObject(ao *analytics.AmpObject) {
 	f.Logger.Flush()
 }
 
+//Logs NotificationEvent to file
+func (f *FileLogger) LogNotificationEventObject(ne *analytics.NotificationEvent) {
+	if ne == nil {
+		return
+	}
+	//Code to parse the object and log in a way required
+	var b bytes.Buffer
+	b.WriteString(jsonifyNotificationEventObject(ne))
+	f.Logger.Debug(b.String())
+	f.Logger.Flush()
+}
+
 //Method to initialize the analytic module
 func NewFileLogger(filename string) (analytics.PBSAnalyticsModule, error) {
 	options := glog.LogOptions{
@@ -176,3 +189,20 @@ func jsonifyAmpObject(ao *analytics.AmpObject) string {
 		return fmt.Sprintf("Transactional Logs Error: Amp object badly formed %v", err)
 	}
 }
+
+func jsonifyNotificationEventObject(ne *analytics.NotificationEvent) string {
+	type alias analytics.NotificationEvent
+	b, err := json.Marshal(&struct {
+		Type RequestType `json:"type"`
+		*alias
+	}{
+		Type:  NOTIFICATION_EVENT,
+		alias: (*alias)(ne),
+	})
+
+	if err == nil {
+		return string(b)
+	} else {
+		return fmt.Sprintf("Transactional Logs Error: NotificationEvent object badly formed %v", err)
+	}
+}
diff --git a/analytics/pubstack/pubstack_module.go b/analytics/pubstack/pubstack_module.go
index 9f1a81c7..60ccde02 100644
--- a/analytics/pubstack/pubstack_module.go
+++ b/analytics/pubstack/pubstack_module.go
@@ -125,6 +125,9 @@ func (p *PubstackModule) LogAuctionObject(ao *analytics.AuctionObject) {
 	p.eventChannels[auction].Push(payload)
 }
 
+func (p *PubstackModule) LogNotificationEventObject(ne *analytics.NotificationEvent) {
+}
+
 func (p *PubstackModule) LogVideoObject(vo *analytics.VideoObject) {
 	p.muxConfig.RLock()
 	defer p.muxConfig.RUnlock()
diff --git a/config/accounts.go b/config/accounts.go
index 11c2e10e..162818eb 100644
--- a/config/accounts.go
+++ b/config/accounts.go
@@ -2,7 +2,8 @@ package config
 
 // Account represents a publisher account configuration
 type Account struct {
-	ID       string      `mapstructure:"id" json:"id"`
-	Disabled bool        `mapstructure:"disabled" json:"disabled"`
-	CacheTTL DefaultTTLs `mapstructure:"cache_ttl" json:"cache_ttl"`
+	ID            string      `mapstructure:"id" json:"id"`
+	Disabled      bool        `mapstructure:"disabled" json:"disabled"`
+	CacheTTL      DefaultTTLs `mapstructure:"cache_ttl" json:"cache_ttl"`
+	EventsEnabled bool        `mapstructure:"events_enabled" json:"events_enabled"`
 }
diff --git a/config/config.go b/config/config.go
index 832d67d8..a50f127e 100755
--- a/config/config.go
+++ b/config/config.go
@@ -41,6 +41,8 @@ type Configuration struct {
 	StoredRequests    StoredRequests  `mapstructure:"stored_requests"`
 	StoredRequestsAMP StoredRequests  `mapstructure:"stored_amp_req"`
 	CategoryMapping   StoredRequests  `mapstructure:"category_mapping"`
+	VTrack            VTrack          `mapstructure:"vtrack"`
+	Event             Event           `mapstructure:"event"`
 	Accounts          StoredRequests  `mapstructure:"accounts"`
 	// Note that StoredVideo refers to stored video requests, and has nothing to do with caching video creatives.
 	StoredVideo StoredRequests `mapstructure:"stored_video_req"`
@@ -315,6 +317,16 @@ type PubstackBuffer struct {
 	Timeout    string `mapstructure:"timeout"`
 }
 
+type VTrack struct {
+	TimeoutMS          int64 `mapstructure:"timeout_ms"`
+	AllowUnknownBidder bool  `mapstructure:"allow_unknown_bidder"`
+	Enabled            bool  `mapstructure:"enabled"`
+}
+
+type Event struct {
+	TimeoutMS int64 `mapstructure:"timeout_ms"`
+}
+
 type HostCookie struct {
 	Domain             string `mapstructure:"domain"`
 	Family             string `mapstructure:"family"`
@@ -734,7 +746,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	// openrtb_ext.BidderRTBHouse doesn't have a good default.
 	// openrtb_ext.BidderRubicon doesn't have a good default.
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSharethrough, "https://match.sharethrough.com/FGMrCMMc/v1?redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsharethrough%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
-	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartadserver, "https://ssbsync.smartadserver.com/api/sync?callerId=5&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsmartadserver%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bssb_sync_pid%5D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartadserver, "https://ssbsync-global.smartadserver.com/api/sync?callerId=5&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsmartadserver%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%5Bssb_sync_pid%5D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartRTB, "https://market-global.smrtb.com/sync/all?nid=smartrtb&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&rr="+url.QueryEscape(externalURL)+"%252Fsetuid%253Fbidder%253Dsmartrtb%2526gdpr%253D{{.GDPR}}%2526gdpr_consent%253D{{.GDPRConsent}}%2526uid%253D%257BXID%257D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSmartyAds, "https://as.ck-ie.com/prebid.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsmartyads%26uid%3D%5BUID%5D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderSomoaudience, "https://publisher-east.mobileadtrading.com/usersync?ru="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dsomoaudience%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D")
@@ -886,6 +898,12 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("stored_video_req.http_events.refresh_rate_seconds", 0)
 	v.SetDefault("stored_video_req.http_events.timeout_ms", 0)
 
+	v.SetDefault("vtrack.timeout_ms", 2000)
+	v.SetDefault("vtrack.allow_unknown_bidder", true)
+	v.SetDefault("vtrack.enabled", true)
+
+	v.SetDefault("event.timeout_ms", 1000)
+
 	v.SetDefault("accounts.filesystem.enabled", false)
 	v.SetDefault("accounts.filesystem.directorypath", "./stored_requests/data/by_id")
 	v.SetDefault("accounts.in_memory_cache.type", "none")
@@ -965,7 +983,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	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.smartadserver.endpoint", "https://ssb-global.smartadserver.com")
 	v.SetDefault("adapters.smartrtb.endpoint", "http://market-east.smrtb.com/json/publisher/rtb?pubid={{.PublisherID}}")
 	v.SetDefault("adapters.smartyads.endpoint", "http://{{.Host}}.smartyads.com/bid?rtb_seat_id={{.SourceId}}&secret_key={{.AccountID}}")
 	v.SetDefault("adapters.somoaudience.endpoint", "http://publisher-east.mobileadtrading.com/rtb/bid")
diff --git a/endpoints/events/event.go b/endpoints/events/event.go
new file mode 100644
index 00000000..fe178d8f
--- /dev/null
+++ b/endpoints/events/event.go
@@ -0,0 +1,338 @@
+package events
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"github.com/julienschmidt/httprouter"
+	accountService "github.com/prebid/prebid-server/account"
+	"github.com/prebid/prebid-server/analytics"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/stored_requests"
+	"net/http"
+	"net/url"
+	"strconv"
+	"time"
+)
+
+const (
+	// Required
+	TemplateUrl        = "%v/event?t=%v&b=%v&a=%v"
+	TypeParameter      = "t"
+	BidIdParameter     = "b"
+	AccountIdParameter = "a"
+
+	// Optional
+	BidderParameter    = "bidder"
+	TimestampParameter = "ts"
+	FormatParameter    = "f"
+	AnalyticsParameter = "x"
+)
+
+var trackingPixelPng = &trackingPixel{
+	Content: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44,
+		0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4,
+		0x89, 0x00, 0x00, 0x00, 0x04, 0x73, 0x42, 0x49, 0x54, 0x08, 0x08, 0x08, 0x08, 0x7C, 0x08, 0x64, 0x88,
+		0x00, 0x00, 0x00, 0x0D, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x63, 0x60, 0x60, 0x60, 0x60, 0x00, 0x00,
+		0x00, 0x05, 0x00, 0x01, 0x87, 0xA1, 0x4E, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
+		0x42, 0x60, 0x82},
+	ContentType: "image/png",
+}
+
+type trackingPixel struct {
+	Content     []byte `json:"content,omitempty"`
+	ContentType string `json:"content_type,omitempty"`
+}
+
+type eventEndpoint struct {
+	Accounts      stored_requests.AccountFetcher
+	Analytics     analytics.PBSAnalyticsModule
+	Cfg           *config.Configuration
+	TrackingPixel *trackingPixel
+}
+
+func NewEventEndpoint(cfg *config.Configuration, accounts stored_requests.AccountFetcher, analytics analytics.PBSAnalyticsModule) httprouter.Handle {
+	ee := &eventEndpoint{
+		Accounts:      accounts,
+		Analytics:     analytics,
+		Cfg:           cfg,
+		TrackingPixel: trackingPixelPng,
+	}
+
+	return ee.Handle
+}
+
+func (e *eventEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+	// parse event request from http req
+	eventRequest, errs := ParseEventRequest(r)
+
+	// handle possible parsing errors
+	if len(errs) > 0 {
+		w.WriteHeader(http.StatusBadRequest)
+
+		for _, err := range errs {
+			w.Write([]byte(fmt.Sprintf("invalid request: %s\n", err.Error())))
+		}
+
+		return
+	}
+
+	// validate account id
+	accountId, err := checkRequiredParameter(r, AccountIdParameter)
+
+	if err != nil {
+		w.WriteHeader(http.StatusUnauthorized)
+		w.Write([]byte(fmt.Sprintf("Account '%s' is required query parameter and can't be empty", AccountIdParameter)))
+		return
+	}
+	eventRequest.AccountID = accountId
+
+	if eventRequest.Analytics != analytics.Enabled {
+		w.WriteHeader(http.StatusNoContent)
+		return
+	}
+
+	ctx := context.Background()
+	if e.Cfg.Event.TimeoutMS > 0 {
+		var cancel context.CancelFunc
+		ctx, cancel = context.WithTimeout(ctx, time.Duration(e.Cfg.Event.TimeoutMS)*time.Millisecond)
+		defer cancel()
+	}
+
+	// get account details
+	account, errs := accountService.GetAccount(ctx, e.Cfg, e.Accounts, eventRequest.AccountID)
+	if len(errs) > 0 {
+		status, messages := HandleAccountServiceErrors(errs)
+		w.WriteHeader(status)
+
+		for _, message := range messages {
+			w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", message)))
+		}
+		return
+	}
+
+	// account does not support events
+	if !account.EventsEnabled {
+		w.WriteHeader(http.StatusUnauthorized)
+		w.Write([]byte(fmt.Sprintf("Account '%s' doesn't support events", eventRequest.AccountID)))
+		return
+	}
+
+	// handle notification event
+	e.Analytics.LogNotificationEventObject(&analytics.NotificationEvent{
+		Request: eventRequest,
+		Account: account,
+	})
+
+	// Add tracking pixel if format == image
+	if eventRequest.Format == analytics.Image {
+		w.WriteHeader(http.StatusOK)
+		w.Header().Add("Content-Type", e.TrackingPixel.ContentType)
+		w.Write(e.TrackingPixel.Content)
+
+		return
+	}
+
+	w.WriteHeader(http.StatusNoContent)
+}
+
+// EventRequestToUrl converts an analytics.EventRequest to an URL
+func EventRequestToUrl(externalUrl string, request *analytics.EventRequest) string {
+	s := fmt.Sprintf(TemplateUrl, externalUrl, request.Type, request.BidID, request.AccountID)
+
+	return s + optionalParameters(request)
+}
+
+// ParseEventRequest parses an analytics.EventRequest from an Http request
+func ParseEventRequest(r *http.Request) (*analytics.EventRequest, []error) {
+	event := &analytics.EventRequest{}
+	var errs []error
+	// validate type
+	if err := readType(event, r); err != nil {
+		errs = append(errs, err)
+	}
+
+	// validate bidid
+	if bidid, err := checkRequiredParameter(r, BidIdParameter); err != nil {
+		errs = append(errs, err)
+	} else {
+		event.BidID = bidid
+	}
+
+	// validate timestamp (optional)
+	if err := readTimestamp(event, r); err != nil {
+		errs = append(errs, err)
+	}
+
+	// validate format (optional)
+	if err := readFormat(event, r); err != nil {
+		errs = append(errs, err)
+	}
+
+	// validate analytics (optional)
+	if err := readAnalytics(event, r); err != nil {
+		errs = append(errs, err)
+	}
+
+	// Bidder
+	event.Bidder = r.URL.Query().Get(BidderParameter)
+
+	return event, errs
+}
+
+// HandleAccountServiceErrors handles account.GetAccount errors
+func HandleAccountServiceErrors(errs []error) (status int, messages []string) {
+	messages = []string{}
+	status = http.StatusBadRequest
+
+	for _, er := range errs {
+		if errors.Is(er, context.DeadlineExceeded) {
+			er = &errortypes.Timeout{
+				Message: er.Error(),
+			}
+		}
+
+		messages = append(messages, er.Error())
+
+		errCode := errortypes.ReadCode(er)
+
+		if errCode == errortypes.BlacklistedAppErrorCode || errCode == errortypes.BlacklistedAcctErrorCode {
+			status = http.StatusServiceUnavailable
+		}
+
+		if errCode == errortypes.TimeoutErrorCode && status == http.StatusBadRequest {
+			status = http.StatusGatewayTimeout
+		}
+	}
+
+	return status, messages
+}
+
+func optionalParameters(request *analytics.EventRequest) string {
+	r := url.Values{}
+
+	// timestamp
+	if request.Timestamp > 0 {
+		r.Add(TimestampParameter, strconv.FormatInt(request.Timestamp, 10))
+	}
+
+	// bidder
+	if request.Bidder != "" {
+		r.Add(BidderParameter, request.Bidder)
+	}
+
+	// format
+	switch request.Format {
+	case analytics.Blank:
+		r.Add(FormatParameter, string(analytics.Blank))
+	case analytics.Image:
+		r.Add(FormatParameter, string(analytics.Image))
+	}
+
+	//analytics
+	switch request.Analytics {
+	case analytics.Enabled:
+		r.Add(AnalyticsParameter, string(analytics.Enabled))
+	case analytics.Disabled:
+		r.Add(AnalyticsParameter, string(analytics.Disabled))
+	}
+
+	opt := r.Encode()
+
+	if opt != "" {
+		return "&" + opt
+	}
+
+	return opt
+}
+
+// readType validates analytics.EventRequest type
+func readType(er *analytics.EventRequest, httpRequest *http.Request) error {
+	t, err := checkRequiredParameter(httpRequest, TypeParameter)
+
+	if err != nil {
+		return err
+	}
+
+	switch t {
+	case string(analytics.Imp):
+		er.Type = analytics.Imp
+		return nil
+	case string(analytics.Win):
+		er.Type = analytics.Win
+		return nil
+	default:
+		return &errortypes.BadInput{Message: fmt.Sprintf("unknown type: '%s'", t)}
+	}
+}
+
+// readFormat validates analytics.EventRequest format attribute
+func readFormat(er *analytics.EventRequest, httpRequest *http.Request) error {
+	f := httpRequest.URL.Query().Get(FormatParameter)
+
+	if f != "" {
+		switch f {
+		case string(analytics.Blank):
+			er.Format = analytics.Blank
+			return nil
+		case string(analytics.Image):
+			er.Format = analytics.Image
+			return nil
+		default:
+			return &errortypes.BadInput{Message: fmt.Sprintf("unknown format: '%s'", f)}
+		}
+	}
+
+	return nil
+}
+
+// readAnalytics validates analytics.EventRequest analytics attribute
+func readAnalytics(er *analytics.EventRequest, httpRequest *http.Request) error {
+	a := httpRequest.URL.Query().Get(AnalyticsParameter)
+
+	if a != "" {
+		switch a {
+		case string(analytics.Enabled):
+			er.Analytics = analytics.Enabled
+			return nil
+		case string(analytics.Disabled):
+			er.Analytics = analytics.Disabled
+			return nil
+		default:
+			return &errortypes.BadInput{Message: fmt.Sprintf("unknown analytics: '%s'", a)}
+		}
+	}
+
+	er.Analytics = analytics.Enabled
+	return nil
+}
+
+// readTimestamp validates analytics.EventRequest timestamp attribute
+func readTimestamp(er *analytics.EventRequest, httpRequest *http.Request) error {
+	t := httpRequest.URL.Query().Get(TimestampParameter)
+
+	if t != "" {
+		ts, err := strconv.ParseInt(t, 10, 64)
+
+		if err != nil {
+			return &errortypes.BadInput{Message: fmt.Sprintf("invalid request: error parsing timestamp '%s'", t)}
+		}
+
+		er.Timestamp = ts
+		return nil
+	}
+
+	return nil
+}
+
+// checkRequiredParameter checks if http.Request contains all required parameters
+func checkRequiredParameter(httpRequest *http.Request, parameter string) (string, error) {
+	t := httpRequest.URL.Query().Get(parameter)
+
+	if t == "" {
+		return "", &errortypes.BadInput{Message: fmt.Sprintf("parameter '%s' is required", parameter)}
+	}
+
+	return t, nil
+}
diff --git a/endpoints/events/vtrack.go b/endpoints/events/vtrack.go
new file mode 100644
index 00000000..c4051554
--- /dev/null
+++ b/endpoints/events/vtrack.go
@@ -0,0 +1,300 @@
+package events
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/golang/glog"
+	"github.com/julienschmidt/httprouter"
+	accountService "github.com/prebid/prebid-server/account"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/analytics"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/prebid_cache_client"
+	"github.com/prebid/prebid-server/stored_requests"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"sort"
+	"strings"
+	"time"
+)
+
+const (
+	AccountParameter   = "a"
+	ImpressionCloseTag = "</Impression>"
+	ImpressionOpenTag  = "<Impression>"
+)
+
+type vtrackEndpoint struct {
+	Cfg         *config.Configuration
+	Accounts    stored_requests.AccountFetcher
+	BidderInfos adapters.BidderInfos
+	Cache       prebid_cache_client.Client
+}
+
+type BidCacheRequest struct {
+	Puts []prebid_cache_client.Cacheable `json:"puts"`
+}
+
+type BidCacheResponse struct {
+	Responses []CacheObject `json:"responses"`
+}
+
+type CacheObject struct {
+	UUID string `json:"uuid"`
+}
+
+func NewVTrackEndpoint(cfg *config.Configuration, accounts stored_requests.AccountFetcher, cache prebid_cache_client.Client, bidderInfos adapters.BidderInfos) httprouter.Handle {
+	vte := &vtrackEndpoint{
+		Cfg:         cfg,
+		Accounts:    accounts,
+		BidderInfos: bidderInfos,
+		Cache:       cache,
+	}
+
+	return vte.Handle
+}
+
+// /vtrack Handler
+func (v *vtrackEndpoint) Handle(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+
+	// get account id from request parameter
+	accountId := getAccountId(r)
+
+	// account id is required
+	if accountId == "" {
+		w.WriteHeader(http.StatusBadRequest)
+		w.Write([]byte(fmt.Sprintf("Account '%s' is required query parameter and can't be empty", AccountParameter)))
+		return
+	}
+
+	// parse puts request from request body
+	req, err := ParseVTrackRequest(r, v.Cfg.MaxRequestSize+1)
+
+	// check if there was any error while parsing puts request
+	if err != nil {
+		w.WriteHeader(http.StatusBadRequest)
+		w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", err.Error())))
+		return
+	}
+
+	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(v.Cfg.VTrack.TimeoutMS)*time.Millisecond))
+	defer cancel()
+
+	// get account details
+	account, errs := accountService.GetAccount(ctx, v.Cfg, v.Accounts, accountId)
+	if len(errs) > 0 {
+		status, messages := HandleAccountServiceErrors(errs)
+		w.WriteHeader(status)
+
+		for _, message := range messages {
+			w.Write([]byte(fmt.Sprintf("Invalid request: %s\n", message)))
+		}
+		return
+	}
+
+	// insert impression tracking if account allows events and bidder allows VAST modification
+	if v.Cache != nil {
+		cachingResponse, errs := v.handleVTrackRequest(ctx, req, account)
+
+		if len(errs) > 0 {
+			w.WriteHeader(http.StatusInternalServerError)
+			for _, err := range errs {
+				w.Write([]byte(fmt.Sprintf("Error(s) updating vast: %s\n", err.Error())))
+
+				return
+			}
+		}
+
+		d, err := json.Marshal(*cachingResponse)
+
+		if err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+			w.Write([]byte(fmt.Sprintf("Error serializing pbs cache response: %s\n", err.Error())))
+
+			return
+		}
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusOK)
+		w.Write(d)
+
+		return
+	}
+
+	w.WriteHeader(http.StatusInternalServerError)
+	w.Write([]byte("PBS Cache client is not configured"))
+}
+
+// GetVastUrlTracking creates a vast url tracking
+func GetVastUrlTracking(externalUrl string, bidid string, bidder string, accountId string, timestamp int64) string {
+
+	eventReq := &analytics.EventRequest{
+		Type:      analytics.Imp,
+		BidID:     bidid,
+		AccountID: accountId,
+		Bidder:    bidder,
+		Timestamp: timestamp,
+		Format:    analytics.Blank,
+	}
+
+	return EventRequestToUrl(externalUrl, eventReq)
+}
+
+// ParseVTrackRequest parses a BidCacheRequest from an HTTP Request
+func ParseVTrackRequest(httpRequest *http.Request, maxRequestSize int64) (req *BidCacheRequest, err error) {
+	req = &BidCacheRequest{}
+	err = nil
+
+	// Pull the request body into a buffer, so we have it for later usage.
+	lr := &io.LimitedReader{
+		R: httpRequest.Body,
+		N: maxRequestSize,
+	}
+
+	defer httpRequest.Body.Close()
+	requestJson, err := ioutil.ReadAll(lr)
+	if err != nil {
+		return req, err
+	}
+
+	// Check if the request size was too large
+	if lr.N <= 0 {
+		err = &errortypes.BadInput{Message: fmt.Sprintf("request size exceeded max size of %d bytes", maxRequestSize-1)}
+		return req, err
+	}
+
+	if len(requestJson) == 0 {
+		err = &errortypes.BadInput{Message: "request body is empty"}
+		return req, err
+	}
+
+	if err := json.Unmarshal(requestJson, req); err != nil {
+		return req, err
+	}
+
+	for _, bcr := range req.Puts {
+		if bcr.BidID == "" {
+			err = error(&errortypes.BadInput{Message: fmt.Sprint("'bidid' is required field and can't be empty")})
+			return req, err
+		}
+
+		if bcr.Bidder == "" {
+			err = error(&errortypes.BadInput{Message: fmt.Sprint("'bidder' is required field and can't be empty")})
+			return req, err
+		}
+	}
+
+	return req, nil
+}
+
+// handleVTrackRequest handles a VTrack request
+func (v *vtrackEndpoint) handleVTrackRequest(ctx context.Context, req *BidCacheRequest, account *config.Account) (*BidCacheResponse, []error) {
+	biddersAllowingVastUpdate := getBiddersAllowingVastUpdate(req, &v.BidderInfos, v.Cfg.VTrack.AllowUnknownBidder)
+	// cache data
+	r, errs := v.cachePutObjects(ctx, req, biddersAllowingVastUpdate, account.ID)
+
+	// handle pbs caching errors
+	if len(errs) != 0 {
+		glog.Errorf("Error(s) updating vast: %v", errs)
+		return nil, errs
+	}
+
+	// build response
+	response := &BidCacheResponse{
+		Responses: []CacheObject{},
+	}
+
+	for _, uuid := range r {
+		response.Responses = append(response.Responses, CacheObject{
+			UUID: uuid,
+		})
+	}
+
+	return response, nil
+}
+
+// cachePutObjects caches BidCacheRequest data
+func (v *vtrackEndpoint) cachePutObjects(ctx context.Context, req *BidCacheRequest, biddersAllowingVastUpdate map[string]struct{}, accountId string) ([]string, []error) {
+	var cacheables []prebid_cache_client.Cacheable
+
+	for _, c := range req.Puts {
+
+		nc := &prebid_cache_client.Cacheable{
+			Type:       c.Type,
+			Data:       c.Data,
+			TTLSeconds: c.TTLSeconds,
+			Key:        c.Key,
+		}
+
+		if _, ok := biddersAllowingVastUpdate[c.Bidder]; ok && nc.Data != nil {
+			nc.Data = modifyVastXml(v.Cfg.ExternalURL, nc.Data, c.BidID, c.Bidder, accountId, c.Timestamp)
+		}
+
+		cacheables = append(cacheables, *nc)
+	}
+
+	return v.Cache.PutJson(ctx, cacheables)
+}
+
+// getBiddersAllowingVastUpdate returns a list of bidders that allow VAST XML modification
+func getBiddersAllowingVastUpdate(req *BidCacheRequest, bidderInfos *adapters.BidderInfos, allowUnknownBidder bool) map[string]struct{} {
+	bl := map[string]struct{}{}
+
+	for _, bcr := range req.Puts {
+		if _, ok := bl[bcr.Bidder]; isAllowVastForBidder(bcr.Bidder, bidderInfos, allowUnknownBidder) && !ok {
+			bl[bcr.Bidder] = struct{}{}
+		}
+	}
+
+	return bl
+}
+
+// isAllowVastForBidder checks if a bidder is active and allowed to modify vast xml data
+func isAllowVastForBidder(bidder string, bidderInfos *adapters.BidderInfos, allowUnknownBidder bool) bool {
+	//if bidder is active and isModifyingVastXmlAllowed is true
+	// check if bidder is configured
+	if b, ok := (*bidderInfos)[bidder]; bidderInfos != nil && ok {
+		// check if bidder is enabled
+		return b.Status == adapters.StatusActive && b.ModifyingVastXmlAllowed
+	}
+
+	return allowUnknownBidder
+}
+
+// getAccountId extracts an account id from an HTTP Request
+func getAccountId(httpRequest *http.Request) string {
+	return httpRequest.URL.Query().Get(AccountParameter)
+}
+
+// modifyVastXml modifies BidCacheRequest element Vast XML data
+func modifyVastXml(externalUrl string, data json.RawMessage, bidid string, bidder string, accountId string, timestamp int64) json.RawMessage {
+	c := string(data)
+	ci := strings.Index(c, ImpressionCloseTag)
+
+	// no impression tag - pass it as it is
+	if ci == -1 {
+		return data
+	}
+
+	vastUrlTracking := GetVastUrlTracking(externalUrl, bidid, bidder, accountId, timestamp)
+	impressionUrl := "<![CDATA[" + vastUrlTracking + "]]>"
+	oi := strings.Index(c, ImpressionOpenTag)
+
+	if ci-oi == len(ImpressionOpenTag) {
+		return json.RawMessage(strings.Replace(c, ImpressionOpenTag, ImpressionOpenTag+impressionUrl, 1))
+	}
+
+	return json.RawMessage(strings.Replace(c, ImpressionCloseTag, ImpressionCloseTag+ImpressionOpenTag+impressionUrl+ImpressionCloseTag, 1))
+}
+
+func contains(s []string, e string) bool {
+	if len(s) == 0 {
+		return false
+	}
+
+	i := sort.SearchStrings(s, e)
+	return i < len(s) && s[i] == e
+}
diff --git a/prebid_cache_client/client.go b/prebid_cache_client/client.go
index 1ad78830..43dfbc9a 100644
--- a/prebid_cache_client/client.go
+++ b/prebid_cache_client/client.go
@@ -41,10 +41,14 @@ const (
 )
 
 type Cacheable struct {
-	Type       PayloadType
-	Data       json.RawMessage
-	TTLSeconds int64
-	Key        string
+	Type       PayloadType     `json:"type,omitempty"`
+	Data       json.RawMessage `json:"value,omitempty"`
+	TTLSeconds int64           `json:"ttlseconds,omitempty"`
+	Key        string          `json:"key,omitempty"`
+
+	BidID     string `json:"bidid,omitempty"`     // this is "/vtrack" specific
+	Bidder    string `json:"bidder,omitempty"`    // this is "/vtrack" specific
+	Timestamp int64  `json:"timestamp,omitempty"` // this is "/vtrack" specific
 }
 
 func NewClient(httpClient *http.Client, conf *config.Cache, extCache *config.ExternalCache, metrics pbsmetrics.MetricsEngine) Client {
@@ -181,6 +185,25 @@ func encodeValueToBuffer(value Cacheable, leadingComma bool, buffer *bytes.Buffe
 		buffer.WriteString(string(value.Key))
 		buffer.WriteString(`"`)
 	}
+
+	//vtrack specific
+	if len(value.BidID) > 0 {
+		buffer.WriteString(`,"bidid":"`)
+		buffer.WriteString(string(value.BidID))
+		buffer.WriteString(`"`)
+	}
+
+	if len(value.Bidder) > 0 {
+		buffer.WriteString(`,"bidder":"`)
+		buffer.WriteString(string(value.Bidder))
+		buffer.WriteString(`"`)
+	}
+
+	if value.Timestamp > 0 {
+		buffer.WriteString(`,"timestamp":`)
+		buffer.WriteString(strconv.FormatInt(value.Timestamp, 10))
+	}
+
 	buffer.WriteByte('}')
 	return nil
 }
diff --git a/router/router.go b/router/router.go
index 4826f0e5..919781f7 100644
--- a/router/router.go
+++ b/router/router.go
@@ -6,6 +6,7 @@ import (
 	"database/sql"
 	"encoding/json"
 	"fmt"
+	"github.com/prebid/prebid-server/endpoints/events"
 	"io/ioutil"
 	"net/http"
 	"path/filepath"
@@ -277,6 +278,16 @@ func New(cfg *config.Configuration, rateConvertor *currencies.RateConverter) (r
 	r.GET("/", serveIndex)
 	r.ServeFiles("/static/*filepath", http.Dir("static"))
 
+	// vtrack endpoint
+	if cfg.VTrack.Enabled {
+		vtrackEndpoint := events.NewVTrackEndpoint(cfg, accounts, cacheClient, bidderInfos)
+		r.POST("/vtrack", vtrackEndpoint)
+	}
+
+	// event endpoint
+	eventEndpoint := events.NewEventEndpoint(cfg, accounts, pbsAnalytics)
+	r.GET("/event", eventEndpoint)
+
 	userSyncDeps := &pbs.UserSyncDeps{
 		HostCookieConfig: &(cfg.HostCookie),
 		ExternalUrl:      cfg.ExternalURL,


Test patch:
diff --git a/adapters/adgeneration/adgeneration_test.go b/adapters/adgeneration/adgeneration_test.go
index 3c795ea2..130f6d15 100644
--- a/adapters/adgeneration/adgeneration_test.go
+++ b/adapters/adgeneration/adgeneration_test.go
@@ -14,7 +14,7 @@ func TestJsonSamples(t *testing.T) {
 	adapterstest.RunJSONBidderTest(t, "adgenerationtest", NewAdgenerationAdapter("https://d.socdm.com/adsv/v1"))
 }
 
-func TestgetRequestUri(t *testing.T) {
+func TestGetRequestUri(t *testing.T) {
 	bidder := NewAdgenerationAdapter("https://d.socdm.com/adsv/v1")
 	// Test items
 	failedRequest := &openrtb.BidRequest{
@@ -24,6 +24,7 @@ func TestgetRequestUri(t *testing.T) {
 			{ID: "extImpBidder-failed-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"_bidder": { "id": "58278" }}`)},
 			{ID: "extImpAdgeneration-failed-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "_id": "58278" }}`)},
 		},
+		Source: &openrtb.Source{TID: "SourceTID"},
 		Device: &openrtb.Device{UA: "testUA", IP: "testIP"},
 		Site:   &openrtb.Site{Page: "https://supership.com"},
 		User:   &openrtb.User{BuyerUID: "buyerID"},
@@ -33,6 +34,7 @@ func TestgetRequestUri(t *testing.T) {
 		Imp: []openrtb.Imp{
 			{ID: "bidRequest-success-test", Banner: &openrtb.Banner{Format: []openrtb.Format{{W: 300, H: 250}}}, Ext: json.RawMessage(`{"bidder": { "id": "58278" }}`)},
 		},
+		Source: &openrtb.Source{TID: "SourceTID"},
 		Device: &openrtb.Device{UA: "testUA", IP: "testIP"},
 		Site:   &openrtb.Site{Page: "https://supership.com"},
 		User:   &openrtb.User{BuyerUID: "buyerID"},
@@ -50,14 +52,6 @@ func TestgetRequestUri(t *testing.T) {
 	}
 	numRequests = len(successRequest.Imp)
 	for index := 0; index < numRequests; index++ {
-		// RequestUri Test.
-		httpRequests, err := bidder.getRequestUri(successRequest, index)
-		if err != nil {
-			t.Errorf("getRequestUri: %v did throw an error: %v", successRequest.Imp[index], err)
-		}
-		if httpRequests == "adapterver="+bidder.version+"&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html" {
-			t.Errorf("getRequestUri: %v did return Request: %s", successRequest.Imp[index], httpRequests)
-		}
 		// getRawQuery Test.
 		adgExt, err := unmarshalExtImpAdgeneration(&successRequest.Imp[index])
 		if err != nil {
@@ -65,27 +59,33 @@ func TestgetRequestUri(t *testing.T) {
 		}
 		rawQuery := bidder.getRawQuery(adgExt.Id, successRequest, &successRequest.Imp[index])
 		expectQueries := map[string]string{
-			"posall":     "SSPLOC",
-			"id":         adgExt.Id,
-			"sdktype":    "0",
-			"hb":         "true",
-			"currency":   bidder.getCurrency(successRequest),
-			"sdkname":    "prebidserver",
-			"adapterver": bidder.version,
-			"size":       getSizes(&successRequest.Imp[index]),
-			"tp":         successRequest.Site.Name,
+			"posall":        "SSPLOC",
+			"id":            adgExt.Id,
+			"sdktype":       "0",
+			"hb":            "true",
+			"currency":      bidder.getCurrency(successRequest),
+			"sdkname":       "prebidserver",
+			"adapterver":    bidder.version,
+			"sizes":         getSizes(&successRequest.Imp[index]),
+			"tp":            successRequest.Site.Page,
+			"transactionid": successRequest.Source.TID,
 		}
 		for key, expectedValue := range expectQueries {
 			actualValue := rawQuery.Get(key)
-			if actualValue == "" {
-				if !(key == "size" || key == "tp") {
-					t.Errorf("getRawQuery: key %s is required value.", key)
-				}
-			}
 			if actualValue != expectedValue {
 				t.Errorf("getRawQuery: %s value does not match expected %s, actual %s", key, expectedValue, actualValue)
 			}
 		}
+
+		// RequestUri Test.
+		actualUri, err := bidder.getRequestUri(successRequest, index)
+		if err != nil {
+			t.Errorf("getRequestUri: %v did throw an error: %v", successRequest.Imp[index], err)
+		}
+		expectedUri := "https://d.socdm.com/adsv/v1?adapterver=" + bidder.version + "&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=https%3A%2F%2Fsupership.com&transactionid=SourceTID"
+		if actualUri != expectedUri {
+			t.Errorf("getRequestUri: does not match expected %s, actual %s", expectedUri, actualUri)
+		}
 	}
 }
 
@@ -99,7 +99,7 @@ func TestGetSizes(t *testing.T) {
 
 	request = &openrtb.Imp{Banner: multiFormatBanner}
 	size = getSizes(request)
-	if size != "300×250,320×50" {
+	if size != "300x250,320x50" {
 		t.Errorf("%v does not match size.", multiFormatBanner)
 	}
 	request = &openrtb.Imp{Banner: noFormatBanner}
diff --git a/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json b/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json
index 10bf1c4a..655f6b75 100644
--- a/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json
+++ b/adapters/adgeneration/adgenerationtest/exemplary/single-banner.json
@@ -4,6 +4,9 @@
         "site": {
             "page": "http://example.com/test.html"
         },
+        "device": {
+            "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
+        },
         "imp": [
             {
                 "id": "some-impression-id",
@@ -52,13 +55,16 @@
                 "tmax": 500
             },
             "expectedRequest":{
-                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
+                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.2&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
                 "headers": {
                     "Accept": [
                         "application/json"
                     ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
+                    ],
+                    "User-Agent": [
+                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
                     ]
                 }
             },
diff --git a/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json b/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json
index bc469a1e..b9a79688 100644
--- a/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json
+++ b/adapters/adgeneration/adgenerationtest/supplemental/204-bid-response.json
@@ -4,6 +4,9 @@
         "site": {
             "page": "http://example.com/test.html"
         },
+        "device": {
+            "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
+        },
         "imp": [
             {
                 "id": "some-impression-id",
@@ -52,13 +55,16 @@
                 "tmax": 500
             },
             "expectedRequest":{
-                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
+                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.2&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
                 "headers": {
                     "Accept": [
                         "application/json"
                     ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
+                    ],
+                    "User-Agent": [
+                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
                     ]
                 }
             },
diff --git a/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json b/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json
index 6ac92d9a..9744e8d2 100644
--- a/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json
+++ b/adapters/adgeneration/adgenerationtest/supplemental/400-bid-response.json
@@ -4,6 +4,9 @@
         "site": {
             "page": "http://example.com/test.html"
         },
+        "device": {
+            "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
+        },
         "imp": [
             {
                 "id": "some-impression-id",
@@ -52,13 +55,16 @@
                 "tmax": 500
             },
             "expectedRequest":{
-                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
+                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.2&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
                 "headers": {
                     "Accept": [
                         "application/json"
                     ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
+                    ],
+                    "User-Agent": [
+                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
                     ]
                 }
             },
diff --git a/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json b/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json
index a0abb66d..3b3bc82a 100644
--- a/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json
+++ b/adapters/adgeneration/adgenerationtest/supplemental/no-bid-response.json
@@ -4,6 +4,9 @@
         "site": {
             "page": "http://example.com/test.html"
         },
+        "device": {
+            "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
+        },
         "imp": [
             {
                 "id": "some-impression-id",
@@ -52,13 +55,16 @@
                 "tmax": 500
             },
             "expectedRequest":{
-                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.1&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&size=300%C3%97250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
+                "uri": "https://d.socdm.com/adsv/v1?adapterver=1.0.2&currency=JPY&hb=true&id=58278&posall=SSPLOC&sdkname=prebidserver&sdktype=0&sizes=300x250&t=json3&tp=http%3A%2F%2Fexample.com%2Ftest.html",
                 "headers": {
                     "Accept": [
                         "application/json"
                     ],
                     "Content-Type": [
                         "application/json;charset=utf-8"
+                    ],
+                    "User-Agent": [
+                        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36"
                     ]
                 }
             },
diff --git a/analytics/config/config_test.go b/analytics/config/config_test.go
index 583d475e..52edc79c 100644
--- a/analytics/config/config_test.go
+++ b/analytics/config/config_test.go
@@ -51,6 +51,11 @@ func TestSampleModule(t *testing.T) {
 	if count != 5 {
 		t.Errorf("PBSAnalyticsModule failed at LogVideoObject")
 	}
+
+	am.LogNotificationEventObject(&analytics.NotificationEvent{})
+	if count != 6 {
+		t.Errorf("PBSAnalyticsModule failed at LogNotificationEventObject")
+	}
 }
 
 type sampleModule struct {
@@ -67,6 +72,8 @@ func (m *sampleModule) LogSetUIDObject(so *analytics.SetUIDObject) { *m.count++
 
 func (m *sampleModule) LogAmpObject(ao *analytics.AmpObject) { *m.count++ }
 
+func (m *sampleModule) LogNotificationEventObject(ne *analytics.NotificationEvent) { *m.count++ }
+
 func initAnalytics(count *int) analytics.PBSAnalyticsModule {
 	modules := make(enabledAnalytics, 0)
 	modules = append(modules, &sampleModule{count})
diff --git a/analytics/filesystem/file_module_test.go b/analytics/filesystem/file_module_test.go
index 6e2d9f62..cfc923c7 100644
--- a/analytics/filesystem/file_module_test.go
+++ b/analytics/filesystem/file_module_test.go
@@ -1,6 +1,7 @@
 package filesystem
 
 import (
+	"github.com/prebid/prebid-server/config"
 	"net/http"
 	"os"
 	"strings"
@@ -64,6 +65,20 @@ func TestCookieSyncObject_ToJson(t *testing.T) {
 	}
 }
 
+func TestLogNotificationEventObject_ToJson(t *testing.T) {
+	neo := &analytics.NotificationEvent{
+		Request: &analytics.EventRequest{
+			Bidder: "bidder",
+		},
+		Account: &config.Account{
+			ID: "id",
+		},
+	}
+	if neoJson := jsonifyNotificationEventObject(neo); strings.Contains(neoJson, "Transactional Logs Error") {
+		t.Fatalf("NotificationEventObject failed to convert to json")
+	}
+}
+
 func TestFileLogger_LogObjects(t *testing.T) {
 	if _, err := os.Stat(TEST_DIR); os.IsNotExist(err) {
 		if err = os.MkdirAll(TEST_DIR, 0755); err != nil {
@@ -77,6 +92,7 @@ func TestFileLogger_LogObjects(t *testing.T) {
 		fl.LogAmpObject(&analytics.AmpObject{})
 		fl.LogSetUIDObject(&analytics.SetUIDObject{})
 		fl.LogCookieSyncObject(&analytics.CookieSyncObject{})
+		fl.LogNotificationEventObject(&analytics.NotificationEvent{})
 	} else {
 		t.Fatalf("Couldn't initialize file logger: %v", err)
 	}
diff --git a/analytics/pubstack/pubstack_module_test.go b/analytics/pubstack/pubstack_module_test.go
index 8d4dfdd6..6bebc937 100644
--- a/analytics/pubstack/pubstack_module_test.go
+++ b/analytics/pubstack/pubstack_module_test.go
@@ -2,11 +2,9 @@ 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"
@@ -16,7 +14,7 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func loadJsonFromFile() (*analytics.AuctionObject, error) {
+func loadJSONFromFile() (*analytics.AuctionObject, error) {
 	req, err := os.Open("mocks/mock_openrtb_request.json")
 	if err != nil {
 		return nil, err
@@ -57,130 +55,171 @@ func loadJsonFromFile() (*analytics.AuctionObject, error) {
 	}, 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
+func TestPubstackModuleErrors(t *testing.T) {
+	tests := []struct {
+		description  string
+		refreshDelay string
+		maxByteSize  string
+		maxTime      string
+	}{
+		{
+			description:  "refresh delay is in an invalid format",
+			refreshDelay: "1invalid",
+			maxByteSize:  "90MB",
+			maxTime:      "15m",
+		},
+		{
+			description:  "max byte size is in an invalid format",
+			refreshDelay: "1h",
+			maxByteSize:  "90invalid",
+			maxTime:      "15m",
+		},
+		{
+			description:  "max time is in an invalid format",
+			refreshDelay: "1h",
+			maxByteSize:  "90MB",
+			maxTime:      "15invalid",
+		},
 	}
-	mockedEvent, err := loadJsonFromFile()
-	if err != nil {
-		t.Fail()
+
+	for _, tt := range tests {
+		_, err := NewPubstackModule(&http.Client{}, "scope", "http://example.com", tt.refreshDelay, 100, tt.maxByteSize, tt.maxTime)
+		assert.NotNil(t, err, tt.description)
 	}
+}
 
-	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,
+func TestPubstackModuleSuccess(t *testing.T) {
+	tests := []struct {
+		description string
+		feature     string
+		logObject   func(analytics.PBSAnalyticsModule)
+	}{
+		{
+			description: "auction events are only published when logging an auction object with auction feature on",
+			feature:     auction,
+			logObject: func(module analytics.PBSAnalyticsModule) {
+				module.LogAuctionObject(&analytics.AuctionObject{Status: http.StatusOK})
+			},
+		},
+		{
+			description: "AMP events are only published when logging an AMP object with AMP feature on",
+			feature:     amp,
+			logObject: func(module analytics.PBSAnalyticsModule) {
+				module.LogAmpObject(&analytics.AmpObject{Status: http.StatusOK})
+			},
+		},
+		{
+			description: "video events are only published when logging a video object with video feature on",
+			feature:     video,
+			logObject: func(module analytics.PBSAnalyticsModule) {
+				module.LogVideoObject(&analytics.VideoObject{Status: http.StatusOK})
+			},
+		},
+		{
+			description: "cookie events are only published when logging a cookie object with cookie feature on",
+			feature:     cookieSync,
+			logObject: func(module analytics.PBSAnalyticsModule) {
+				module.LogCookieSyncObject(&analytics.CookieSyncObject{Status: http.StatusOK})
+			},
+		},
+		{
+			description: "setUID events are only published when logging a setUID object with setUID feature on",
+			feature:     setUID,
+			logObject: func(module analytics.PBSAnalyticsModule) {
+				module.LogSetUIDObject(&analytics.SetUIDObject{Status: http.StatusOK})
+			},
+		},
 	}
 
-	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)
+	for _, tt := range tests {
+		// original config is loaded when the module is created
+		// the feature is disabled so no events should be sent
+		origConfig := &Configuration{
+			Features: map[string]bool{
+				tt.feature: false,
+			},
+		}
+		// updated config is hot-reloaded after some time passes
+		// the feature is enabled so events should be sent
+		updatedConfig := &Configuration{
+			Features: map[string]bool{
+				tt.feature: true,
+			},
+		}
+
+		// create server with root endpoint that returns the current config
+		// add an intake endpoint that PBS hits when events are sent
+		rootCount := 0
+		mux := http.NewServeMux()
+		mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
+			rootCount++
+			defer req.Body.Close()
+
+			if rootCount > 1 {
+				if data, err := json.Marshal(updatedConfig); err != nil {
+					res.WriteHeader(http.StatusBadRequest)
+				} else {
+					res.Write(data)
+				}
+			} else {
+				if data, err := json.Marshal(origConfig); err != nil {
+					res.WriteHeader(http.StatusBadRequest)
+				} else {
+					res.Write(data)
+				}
+			}
+		})
+
+		intakeChannel := make(chan int) // using a channel rather than examining the count directly to avoid race
+		mux.HandleFunc("/intake/"+tt.feature+"/", func(res http.ResponseWriter, req *http.Request) {
+			intakeChannel <- 1
+		})
+		server := httptest.NewServer(mux)
+		client := server.Client()
+
+		// set the server url on each of the configs
+		origConfig.Endpoint = server.URL
+		updatedConfig.Endpoint = server.URL
+
+		// instantiate module with 25ms config refresh rate
+		module, err := NewPubstackModule(client, "scope", server.URL, "15ms", 100, "1B", "10ms")
+		assert.Nil(t, err, tt.description)
+
+		// allow time for the module to load the original config
+		time.Sleep(10 * time.Millisecond)
+
+		pubstack, _ := module.(*PubstackModule)
+		// attempt to log but no event channel was created because the feature is disabled in the original config
+		tt.logObject(pubstack)
+
+		// verify no event was received over a 10ms period
+		assertChanNone(t, intakeChannel, tt.description)
+
+		// allow time for the server to start serving the updated config
+		time.Sleep(10 * time.Millisecond)
+
+		// attempt to log; the event channel should have been created because the feature is enabled in updated config
+		tt.logObject(pubstack)
+
+		// verify an event was received within 10ms
+		assertChanOne(t, intakeChannel, tt.description)
+	}
+}
 
+func assertChanNone(t *testing.T, c <-chan int, msgAndArgs ...interface{}) bool {
+	select {
+	case <-c:
+		return assert.Fail(t, "Should NOT receive an event, but did", msgAndArgs...)
+	case <-time.After(10 * time.Millisecond):
+		return true
+	}
+}
+
+func assertChanOne(t *testing.T, c <-chan int, msgAndArgs ...interface{}) bool {
+	select {
+	case <-c:
+		return true
+	case <-time.After(10 * time.Millisecond):
+		return assert.Fail(t, "Should receive an event, but did NOT", msgAndArgs...)
+	}
 }
diff --git a/endpoints/events/account_test.go b/endpoints/events/account_test.go
new file mode 100644
index 00000000..ff0dd51b
--- /dev/null
+++ b/endpoints/events/account_test.go
@@ -0,0 +1,161 @@
+package events
+
+import (
+	"errors"
+	"fmt"
+	"github.com/julienschmidt/httprouter"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/stored_requests"
+	"github.com/stretchr/testify/assert"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+)
+
+func TestHandleAccountServiceErrors(t *testing.T) {
+	tests := map[string]struct {
+		fetcher *mockAccountsFetcher
+		cfg     *config.Configuration
+		want    struct {
+			code     int
+			response string
+		}
+	}{
+		"badRequest": {
+			fetcher: &mockAccountsFetcher{
+				Fail:  true,
+				Error: errors.New("some error"),
+			},
+			cfg: &config.Configuration{
+				AccountDefaults: config.Account{Disabled: true},
+				AccountRequired: true,
+				MaxRequestSize:  maxSize,
+				VTrack: config.VTrack{
+					TimeoutMS: int64(2000), AllowUnknownBidder: false,
+				},
+			},
+			want: struct {
+				code     int
+				response string
+			}{
+				code:     400,
+				response: "Invalid request: some error\nInvalid request: Prebid-server could not verify the Account ID. Please reach out to the prebid server host.\n",
+			},
+		},
+		"serviceUnavailable": {
+			fetcher: &mockAccountsFetcher{
+				Fail: false,
+			},
+			cfg: &config.Configuration{
+				BlacklistedAcctMap: map[string]bool{"testacc": true},
+				MaxRequestSize:     maxSize,
+				VTrack: config.VTrack{
+					TimeoutMS: int64(2000), AllowUnknownBidder: false,
+				},
+			},
+			want: struct {
+				code     int
+				response string
+			}{
+				code:     503,
+				response: "Invalid request: Prebid-server has disabled Account ID: testacc, please reach out to the prebid server host.\n",
+			},
+		},
+		"timeout": {
+			fetcher: &mockAccountsFetcher{
+				Fail:       false,
+				DurationMS: 50,
+			},
+			cfg: &config.Configuration{
+				AccountDefaults: config.Account{Disabled: true},
+				AccountRequired: true,
+				Event: config.Event{
+					TimeoutMS: 1,
+				},
+				MaxRequestSize: maxSize,
+				VTrack: config.VTrack{
+					TimeoutMS:          int64(1),
+					AllowUnknownBidder: false,
+				},
+			},
+			want: struct {
+				code     int
+				response string
+			}{
+				code:     504,
+				response: "Invalid request: context deadline exceeded\nInvalid request: Prebid-server could not verify the Account ID. Please reach out to the prebid server host.\n",
+			},
+		},
+	}
+
+	for name, test := range tests {
+
+		handlers := []struct {
+			name string
+			h    httprouter.Handle
+			r    *http.Request
+		}{
+			vast(t, test.cfg, test.fetcher),
+			event(test.cfg, test.fetcher),
+		}
+
+		for _, handler := range handlers {
+			t.Run(handler.name+"-"+name, func(t *testing.T) {
+				test.cfg.MarshalAccountDefaults()
+
+				recorder := httptest.NewRecorder()
+
+				// execute
+				handler.h(recorder, handler.r, nil)
+				d, err := ioutil.ReadAll(recorder.Result().Body)
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				// validate
+				assert.Equal(t, test.want.code, recorder.Result().StatusCode, fmt.Sprintf("Expected %d", test.want.code))
+				assert.Equal(t, test.want.response, string(d))
+			})
+		}
+	}
+}
+
+func event(cfg *config.Configuration, fetcher stored_requests.AccountFetcher) struct {
+	name string
+	h    httprouter.Handle
+	r    *http.Request
+} {
+	return struct {
+		name string
+		h    httprouter.Handle
+		r    *http.Request
+	}{
+		name: "event",
+		h:    NewEventEndpoint(cfg, fetcher, nil),
+		r:    httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=1&a=testacc", strings.NewReader("")),
+	}
+}
+
+func vast(t *testing.T, cfg *config.Configuration, fetcher stored_requests.AccountFetcher) struct {
+	name string
+	h    httprouter.Handle
+	r    *http.Request
+} {
+	vtrackBody, err := getValidVTrackRequestBody(true, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return struct {
+		name string
+		h    httprouter.Handle
+		r    *http.Request
+	}{
+		name: "vast",
+		h:    NewVTrackEndpoint(cfg, fetcher, &vtrackMockCacheClient{}, adapters.BidderInfos{}),
+		r:    httptest.NewRequest("POST", "/vtrack?a=testacc", strings.NewReader(vtrackBody)),
+	}
+}
diff --git a/endpoints/events/event_test.go b/endpoints/events/event_test.go
new file mode 100644
index 00000000..ba807184
--- /dev/null
+++ b/endpoints/events/event_test.go
@@ -0,0 +1,664 @@
+package events
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"github.com/prebid/prebid-server/analytics"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/stored_requests"
+	"github.com/stretchr/testify/assert"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+	"time"
+)
+
+// Mock Analytics Module
+type eventsMockAnalyticsModule struct {
+	Fail    bool
+	Error   error
+	Invoked bool
+}
+
+func (e *eventsMockAnalyticsModule) LogAuctionObject(ao *analytics.AuctionObject) {
+	if e.Fail {
+		panic(e.Error)
+	}
+	return
+}
+
+func (e *eventsMockAnalyticsModule) LogVideoObject(vo *analytics.VideoObject) {
+	if e.Fail {
+		panic(e.Error)
+	}
+	return
+}
+
+func (e *eventsMockAnalyticsModule) LogCookieSyncObject(cso *analytics.CookieSyncObject) {
+	if e.Fail {
+		panic(e.Error)
+	}
+	return
+}
+
+func (e *eventsMockAnalyticsModule) LogSetUIDObject(so *analytics.SetUIDObject) {
+	if e.Fail {
+		panic(e.Error)
+	}
+	return
+}
+
+func (e *eventsMockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject) {
+	if e.Fail {
+		panic(e.Error)
+	}
+	return
+}
+
+func (e *eventsMockAnalyticsModule) LogNotificationEventObject(ne *analytics.NotificationEvent) {
+	if e.Fail {
+		panic(e.Error)
+	}
+	e.Invoked = true
+
+	return
+}
+
+// Mock Account fetcher
+var mockAccountData = map[string]json.RawMessage{
+	"events_enabled":  json.RawMessage(`{"events_enabled":true}`),
+	"events_disabled": json.RawMessage(`{"events_enabled":false}`),
+}
+
+type mockAccountsFetcher struct {
+	Fail       bool
+	Error      error
+	DurationMS int
+}
+
+func (maf mockAccountsFetcher) FetchAccount(ctx context.Context, accountID string) (json.RawMessage, []error) {
+	if maf.DurationMS > 0 {
+		select {
+		case <-time.After(time.Duration(maf.DurationMS) * time.Millisecond):
+			break
+		case <-ctx.Done():
+			return nil, []error{ctx.Err()}
+		}
+	}
+
+	if account, ok := mockAccountData[accountID]; ok {
+		return account, nil
+	}
+
+	if maf.Fail {
+		return nil, []error{maf.Error}
+	}
+
+	return nil, []error{stored_requests.NotFoundError{accountID, "Account"}}
+}
+
+// Tests
+
+func TestShouldReturnBadRequestWhenTypeIsMissing(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?b=test", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with missing type parameter")
+	assert.Equal(t, "invalid request: parameter 't' is required\n", string(d))
+}
+
+func TestShouldReturnBadRequestWhenTypeIsInvalid(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccounts := &mockAccountsFetcher{}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=test&b=t", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccounts, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with invalid type parameter")
+	assert.Equal(t, "invalid request: unknown type: 'test'\n", string(d))
+}
+
+func TestShouldReturnBadRequestWhenBidIdIsMissing(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=win", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with missing bidid parameter")
+	assert.Equal(t, "invalid request: parameter 'b' is required\n", string(d))
+}
+
+func TestShouldReturnBadRequestWhenTimestampIsInvalid(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=q", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with invalid timestamp parameter")
+	assert.Equal(t, "invalid request: invalid request: error parsing timestamp 'q'\n", string(d))
+}
+
+func TestShouldReturnUnauthorizedWhenAccountIsMissing(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 401, recorder.Result().StatusCode, "Expected 401 on request with missing account id parameter")
+	assert.Equal(t, "Account 'a' is required query parameter and can't be empty", string(d))
+}
+
+func TestShouldReturnBadRequestWhenFormatValueIsInvalid(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=q", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with invalid format parameter")
+	assert.Equal(t, "invalid request: unknown format: 'q'\n", string(d))
+}
+
+func TestShouldReturnBadRequestWhenAnalyticsValueIsInvalid(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=4", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with invalid analytics parameter")
+	assert.Equal(t, "invalid request: unknown analytics: '4'\n", string(d))
+}
+
+func TestShouldNotPassEventToAnalyticsReporterWhenAccountNotFound(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail:  true,
+		Error: stored_requests.NotFoundError{ID: "testacc"},
+	}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=1&a=testacc", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 401, recorder.Result().StatusCode, "Expected 401 on account not found")
+	assert.Equal(t, "Account 'testacc' doesn't support events", string(d))
+}
+
+func TestShouldNotPassEventToAnalyticsReporterWhenAccountEventNotEnabled(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail: false,
+	}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=1&a=events_disabled", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 401, recorder.Result().StatusCode, "Expected 401 on account with events disabled")
+	assert.Equal(t, "Account 'events_disabled' doesn't support events", string(d))
+}
+
+func TestShouldPassEventToAnalyticsReporterWhenAccountEventEnabled(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail: false,
+	}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=1&a=events_enabled", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	// validate
+	assert.Equal(t, 204, recorder.Result().StatusCode, "Expected 204 when account has events enabled")
+	assert.Equal(t, true, mockAnalyticsModule.Invoked)
+}
+
+func TestShouldNotPassEventToAnalyticsReporterWhenAnalyticsValueIsZero(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail: false,
+	}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=b&x=0&a=events_enabled", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	// validate
+	assert.Equal(t, 204, recorder.Result().StatusCode)
+	assert.Equal(t, true, mockAnalyticsModule.Invoked != true)
+}
+
+func TestShouldRespondWithPixelAndContentTypeWhenRequestFormatIsImage(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail: false,
+	}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=win&b=test&ts=1234&f=i&x=1&a=events_enabled", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 200, recorder.Result().StatusCode, "Expected 200 with tracking pixel when format is imp")
+	assert.Equal(t, true, mockAnalyticsModule.Invoked)
+	assert.Equal(t, "image/png", recorder.Header().Get("Content-Type"))
+	assert.Equal(t, "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAABHNCSVQICAgIfAhkiAAAAA1JREFUCJljYGBgYAAAAAUAAYehTtQAAAAASUVORK5CYII=", base64.URLEncoding.EncodeToString(d))
+}
+
+func TestShouldRespondWithNoContentWhenRequestFormatIsNotDefined(t *testing.T) {
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail: false,
+	}
+
+	// mock PBS Analytics Module
+	mockAnalyticsModule := &eventsMockAnalyticsModule{
+		Fail: false,
+	}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("GET", "/event?t=imp&b=test&ts=1234&x=1&a=events_enabled", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := NewEventEndpoint(cfg, mockAccountsFetcher, mockAnalyticsModule)
+
+	// execute
+	e(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 204, recorder.Result().StatusCode, "Expected 200 with empty response")
+	assert.Equal(t, true, mockAnalyticsModule.Invoked)
+	assert.Equal(t, "", recorder.Header().Get("Content-Type"))
+	assert.Equal(t, 0, len(d))
+}
+
+func TestShouldParseEventCorrectly(t *testing.T) {
+
+	tests := map[string]struct {
+		req      *http.Request
+		expected *analytics.EventRequest
+	}{
+		"one": {
+			req: httptest.NewRequest("GET", "/event?t=win&b=bidId&f=b&ts=1000&x=1&a=accountId&bidder=bidder", strings.NewReader("")),
+			expected: &analytics.EventRequest{
+				Type:      analytics.Win,
+				BidID:     "bidId",
+				Timestamp: 1000,
+				Bidder:    "bidder",
+				AccountID: "",
+				Format:    analytics.Blank,
+				Analytics: analytics.Enabled,
+			},
+		},
+		"two": {
+			req: httptest.NewRequest("GET", "/event?t=win&b=bidId&ts=0&a=accountId", strings.NewReader("")),
+			expected: &analytics.EventRequest{
+				Type:      analytics.Win,
+				BidID:     "bidId",
+				Timestamp: 0,
+				Analytics: analytics.Enabled,
+			},
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+
+			// execute
+			er, errs := ParseEventRequest(test.req)
+
+			// validate
+			assert.Equal(t, 0, len(errs))
+			assert.EqualValues(t, test.expected, er)
+		})
+	}
+}
+
+func TestEventRequestToUrl(t *testing.T) {
+	externalUrl := "http://localhost:8000"
+	tests := map[string]struct {
+		er   *analytics.EventRequest
+		want string
+	}{
+		"one": {
+			er: &analytics.EventRequest{
+				Type:      analytics.Imp,
+				BidID:     "bidid",
+				AccountID: "accountId",
+				Bidder:    "bidder",
+				Timestamp: 1234567,
+				Format:    analytics.Blank,
+				Analytics: analytics.Enabled,
+			},
+			want: "http://localhost:8000/event?t=imp&b=bidid&a=accountId&bidder=bidder&f=b&ts=1234567&x=1",
+		},
+		"two": {
+			er: &analytics.EventRequest{
+				Type:      analytics.Win,
+				BidID:     "bidid",
+				AccountID: "accountId",
+				Bidder:    "bidder",
+				Timestamp: 1234567,
+				Format:    analytics.Image,
+				Analytics: analytics.Disabled,
+			},
+			want: "http://localhost:8000/event?t=win&b=bidid&a=accountId&bidder=bidder&f=i&ts=1234567&x=0",
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			expected := EventRequestToUrl(externalUrl, test.er)
+			// validate
+			assert.Equal(t, test.want, expected)
+		})
+	}
+}
diff --git a/endpoints/events/vtrack_test.go b/endpoints/events/vtrack_test.go
new file mode 100644
index 00000000..0ec04663
--- /dev/null
+++ b/endpoints/events/vtrack_test.go
@@ -0,0 +1,692 @@
+package events
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/prebid_cache_client"
+	"github.com/prebid/prebid-server/stored_requests"
+	"github.com/stretchr/testify/assert"
+	"io/ioutil"
+	"net/http/httptest"
+	"strings"
+	"testing"
+)
+
+const (
+	maxSize = 1024 * 256
+
+	vastXmlWithImpressionWithContent    = "<VAST version=\"3.0\"><Ad><Wrapper><AdSystem>prebid.org wrapper</AdSystem><VASTAdTagURI><![CDATA[adm2]]></VASTAdTagURI><Impression>content</Impression><Creatives></Creatives></Wrapper></Ad></VAST>"
+	vastXmlWithImpressionWithoutContent = "<VAST version=\"3.0\"><Ad><Wrapper><AdSystem>prebid.org wrapper</AdSystem><VASTAdTagURI><![CDATA[adm2]]></VASTAdTagURI><Impression></Impression><Creatives></Creatives></Wrapper></Ad></VAST>"
+	vastXmlWithoutImpression            = "<VAST version=\"3.0\"><Ad><Wrapper><AdSystem>prebid.org wrapper</AdSystem><VASTAdTagURI><![CDATA[adm2]]></VASTAdTagURI><Creatives></Creatives></Wrapper></Ad></VAST>"
+)
+
+// Mock pbs cache client
+type vtrackMockCacheClient struct {
+	Fail  bool
+	Error error
+	Uuids []string
+}
+
+func (m *vtrackMockCacheClient) PutJson(ctx context.Context, values []prebid_cache_client.Cacheable) ([]string, []error) {
+	if m.Fail {
+		return []string{}, []error{m.Error}
+	}
+	return m.Uuids, []error{}
+}
+func (m *vtrackMockCacheClient) GetExtCacheData() (scheme string, host string, path string) {
+	return
+}
+
+// Test
+func TestShouldRespondWithBadRequestWhenAccountParameterIsMissing(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// mock config
+	cfg := &config.Configuration{
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("POST", "/vtrack", strings.NewReader(reqData))
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: nil,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with missing account parameter")
+	assert.Equal(t, "Account 'a' is required query parameter and can't be empty", string(d))
+}
+
+func TestShouldRespondWithBadRequestWhenRequestBodyIsEmpty(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize:  maxSize,
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	reqData := ""
+
+	req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(reqData))
+
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: nil,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with empty body")
+	assert.Equal(t, "Invalid request: request body is empty\n", string(d))
+}
+
+func TestShouldRespondWithBadRequestWhenRequestBodyIsInvalid(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize:  maxSize,
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	reqData := "invalid"
+
+	req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(reqData))
+
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: nil,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with invalid body")
+}
+
+func TestShouldRespondWithBadRequestWhenBidIdIsMissing(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize:  maxSize,
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	data := &BidCacheRequest{
+		Puts: []prebid_cache_client.Cacheable{
+			{},
+		},
+	}
+
+	reqData, err := json.Marshal(data)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(string(reqData)))
+
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: nil,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with elements missing bidid")
+	assert.Equal(t, "Invalid request: 'bidid' is required field and can't be empty\n", string(d))
+}
+
+func TestShouldRespondWithBadRequestWhenBidderIsMissing(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize:  maxSize,
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	data := &BidCacheRequest{
+		Puts: []prebid_cache_client.Cacheable{
+			{
+				BidID: "test",
+			},
+		},
+	}
+
+	reqData, err := json.Marshal(data)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(string(reqData)))
+
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: nil,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 on request with elements missing bidder")
+	assert.Equal(t, "Invalid request: 'bidder' is required field and can't be empty\n", string(d))
+}
+
+func TestShouldRespondWithInternalServerErrorWhenPbsCacheClientFails(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{
+		Fail:  true,
+		Error: fmt.Errorf("pbs cache client failed"),
+	}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize: maxSize, VTrack: config.VTrack{
+			TimeoutMS: int64(2000), AllowUnknownBidder: true,
+		},
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	data, err := getValidVTrackRequestBody(false, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data))
+
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: nil,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 500, recorder.Result().StatusCode, "Expected 500 when pbs cache client fails")
+	assert.Equal(t, "Error(s) updating vast: pbs cache client failed\n", string(d))
+}
+
+func TestShouldTolerateAccountNotFound(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail:  true,
+		Error: stored_requests.NotFoundError{},
+	}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize: maxSize, VTrack: config.VTrack{
+			TimeoutMS: int64(2000), AllowUnknownBidder: false,
+		},
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	data, err := getValidVTrackRequestBody(true, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req := httptest.NewRequest("POST", "/vtrack?a=1235", strings.NewReader(data))
+
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: nil,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	// validate
+	assert.Equal(t, 200, recorder.Result().StatusCode, "Expected 200 when account is not found and request is valid")
+	assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
+}
+
+func TestShouldSendToCacheExpectedPutsAndUpdatableBiddersWhenBidderVastNotAllowed(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{
+		Fail:  false,
+		Uuids: []string{"uuid1"},
+	}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail: false,
+	}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize: maxSize, VTrack: config.VTrack{
+			TimeoutMS: int64(2000), AllowUnknownBidder: false,
+		},
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// bidder info
+	bidderInfos := make(adapters.BidderInfos)
+	bidderInfos["bidder"] = adapters.BidderInfo{
+		Status:                  adapters.StatusActive,
+		ModifyingVastXmlAllowed: false,
+	}
+	bidderInfos["updatable_bidder"] = adapters.BidderInfo{
+		Status:                  adapters.StatusActive,
+		ModifyingVastXmlAllowed: true,
+	}
+
+	// prepare
+	data, err := getValidVTrackRequestBody(false, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data))
+
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: bidderInfos,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 200, recorder.Result().StatusCode, "Expected 200 when account is not found and request is valid")
+	assert.Equal(t, "{\"responses\":[{\"uuid\":\"uuid1\"}]}", string(d), "Expected 200 when account is found and request is valid")
+	assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
+}
+
+func TestShouldSendToCacheExpectedPutsAndUpdatableBiddersWhenBidderVastAllowed(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{
+		Fail:  false,
+		Uuids: []string{"uuid1", "uuid2"},
+	}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail: false,
+	}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize: maxSize, VTrack: config.VTrack{
+			TimeoutMS: int64(2000), AllowUnknownBidder: false,
+		},
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// bidder info
+	bidderInfos := make(adapters.BidderInfos)
+	bidderInfos["bidder"] = adapters.BidderInfo{
+		Status:                  adapters.StatusActive,
+		ModifyingVastXmlAllowed: true,
+	}
+	bidderInfos["updatable_bidder"] = adapters.BidderInfo{
+		Status:                  adapters.StatusActive,
+		ModifyingVastXmlAllowed: true,
+	}
+
+	// prepare
+	data, err := getValidVTrackRequestBody(true, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data))
+
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: bidderInfos,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 200, recorder.Result().StatusCode, "Expected 200 when account is not found and request is valid")
+	assert.Equal(t, "{\"responses\":[{\"uuid\":\"uuid1\"},{\"uuid\":\"uuid2\"}]}", string(d), "Expected 200 when account is found and request is valid")
+	assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
+}
+
+func TestShouldSendToCacheExpectedPutsAndUpdatableUnknownBiddersWhenUnknownBidderIsAllowed(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{
+		Fail:  false,
+		Uuids: []string{"uuid1", "uuid2"},
+	}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail: false,
+	}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize: maxSize, VTrack: config.VTrack{
+			TimeoutMS: int64(2000), AllowUnknownBidder: true,
+		},
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// bidder info
+	bidderInfos := make(adapters.BidderInfos)
+
+	// prepare
+	data, err := getValidVTrackRequestBody(true, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data))
+
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: bidderInfos,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 200, recorder.Result().StatusCode, "Expected 200 when account is not found and request is valid")
+	assert.Equal(t, "{\"responses\":[{\"uuid\":\"uuid1\"},{\"uuid\":\"uuid2\"}]}", string(d), "Expected 200 when account is found, request has unknown bidders but allowUnknownBidders is enabled")
+	assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"))
+}
+
+func TestShouldReturnBadRequestWhenRequestExceedsMaxRequestSize(t *testing.T) {
+	// mock pbs cache client
+	mockCacheClient := &vtrackMockCacheClient{
+		Fail:  false,
+		Uuids: []string{"uuid1", "uuid2"},
+	}
+
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail: false,
+	}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize: 1,
+		VTrack: config.VTrack{
+			TimeoutMS: int64(2000), AllowUnknownBidder: true,
+		},
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// bidder info
+	bidderInfos := make(adapters.BidderInfos)
+
+	// prepare
+	data, err := getValidVTrackRequestBody(true, false)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data))
+
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: bidderInfos,
+		Cache:       mockCacheClient,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 400, recorder.Result().StatusCode, "Expected 400 when request exceeds max request size")
+	assert.Equal(t, "Invalid request: request size exceeded max size of 1 bytes\n", string(d))
+}
+
+func TestShouldRespondWithInternalErrorPbsCacheIsNotConfigured(t *testing.T) {
+	// mock AccountsFetcher
+	mockAccountsFetcher := &mockAccountsFetcher{
+		Fail: false,
+	}
+
+	// config
+	cfg := &config.Configuration{
+		MaxRequestSize: maxSize, VTrack: config.VTrack{
+			TimeoutMS: int64(2000), AllowUnknownBidder: false,
+		},
+		AccountDefaults: config.Account{},
+	}
+	cfg.MarshalAccountDefaults()
+
+	// prepare
+	data, err := getValidVTrackRequestBody(true, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	req := httptest.NewRequest("POST", "/vtrack?a=events_enabled", strings.NewReader(data))
+	recorder := httptest.NewRecorder()
+
+	e := vtrackEndpoint{
+		Cfg:         cfg,
+		BidderInfos: nil,
+		Cache:       nil,
+		Accounts:    mockAccountsFetcher,
+	}
+
+	// execute
+	e.Handle(recorder, req, nil)
+
+	d, err := ioutil.ReadAll(recorder.Result().Body)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// validate
+	assert.Equal(t, 500, recorder.Result().StatusCode, "Expected 500 when pbs cache is not configured")
+	assert.Equal(t, "PBS Cache client is not configured", string(d))
+}
+
+func TestVastUrlShouldReturnExpectedUrl(t *testing.T) {
+	url := GetVastUrlTracking("http://external-url", "bidId", "bidder", "accountId", 1000)
+	assert.Equal(t, "http://external-url/event?t=imp&b=bidId&a=accountId&bidder=bidder&f=b&ts=1000", url, "Invalid vast url")
+}
+
+func getValidVTrackRequestBody(withImpression bool, withContent bool) (string, error) {
+	d, e := getVTrackRequestData(withImpression, withContent)
+
+	if e != nil {
+		return "", e
+	}
+
+	req := &BidCacheRequest{
+		Puts: []prebid_cache_client.Cacheable{
+			{
+				Type:       prebid_cache_client.TypeXML,
+				BidID:      "bidId1",
+				Bidder:     "bidder",
+				Data:       d,
+				TTLSeconds: 3600,
+				Timestamp:  1000,
+			},
+			{
+				Type:       prebid_cache_client.TypeXML,
+				BidID:      "bidId2",
+				Bidder:     "updatable_bidder",
+				Data:       d,
+				TTLSeconds: 3600,
+				Timestamp:  1000,
+			},
+		},
+	}
+
+	buf := &bytes.Buffer{}
+	enc := json.NewEncoder(buf)
+	enc.SetEscapeHTML(false)
+
+	e = enc.Encode(req)
+
+	return buf.String(), e
+}
+
+func getVTrackRequestData(wi bool, wic bool) (db []byte, e error) {
+	data := &bytes.Buffer{}
+	enc := json.NewEncoder(data)
+	enc.SetEscapeHTML(false)
+
+	if wi && wic {
+		e = enc.Encode(vastXmlWithImpressionWithContent)
+		return data.Bytes(), e
+	} else if wi {
+		e = enc.Encode(vastXmlWithImpressionWithoutContent)
+	} else {
+		enc.Encode(vastXmlWithoutImpression)
+	}
+
+	return data.Bytes(), e
+}
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index f0cf1dcf..be57271e 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -1157,6 +1157,9 @@ func (logger mockLogger) LogCookieSyncObject(cookieObject *analytics.CookieSyncO
 func (logger mockLogger) LogSetUIDObject(uuidObj *analytics.SetUIDObject) {
 	return
 }
+func (logger mockLogger) LogNotificationEventObject(uuidObj *analytics.NotificationEvent) {
+	return
+}
 func (logger mockLogger) LogAmpObject(ao *analytics.AmpObject) {
 	*logger.ampObject = *ao
 }
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index a4e9bfbb..27741b22 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -1203,6 +1203,8 @@ func (m *mockAnalyticsModule) LogSetUIDObject(so *analytics.SetUIDObject) { retu
 
 func (m *mockAnalyticsModule) LogAmpObject(ao *analytics.AmpObject) { return }
 
+func (m *mockAnalyticsModule) LogNotificationEventObject(ne *analytics.NotificationEvent) { return }
+
 func mockDeps(t *testing.T, ex *mockExchangeVideo) *endpointDeps {
 	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
 	deps := &endpointDeps{
diff --git a/exchange/cachetest/customcachekey.json b/exchange/cachetest/customcachekey.json
index bb2a37ca..9a9008fe 100644
--- a/exchange/cachetest/customcachekey.json
+++ b/exchange/cachetest/customcachekey.json
@@ -26,14 +26,14 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data": "{\"id\": \"appbid001\",\"impid\": \"oneImp\",\"price\": 7.64,\"cat\": [\"11_sports_22\"]}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value": "{\"id\": \"appbid001\",\"impid\": \"oneImp\",\"price\": 7.64,\"cat\": [\"11_sports_22\"]}"
         },
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data": "{\"id\": \"pubbid001\", \"impid\": \"oneImp\", \"price\": 5.64, \"cat\": [\"33_news_44\"]}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value": "{\"id\": \"pubbid001\", \"impid\": \"oneImp\", \"price\": 5.64, \"cat\": [\"33_news_44\"]}"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/customcachekey_no_bidders.json b/exchange/cachetest/customcachekey_no_bidders.json
index b8521582..9824ed55 100644
--- a/exchange/cachetest/customcachekey_no_bidders.json
+++ b/exchange/cachetest/customcachekey_no_bidders.json
@@ -26,9 +26,9 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data": "{\"id\": \"appbid001\", \"impid\": \"oneImp\", \"price\": 7.64, \"cat\": [\"11_sports_22\"]}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value": "{\"id\": \"appbid001\", \"impid\": \"oneImp\", \"price\": 7.64, \"cat\": [\"11_sports_22\"]}"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/customcachekey_no_winners.json b/exchange/cachetest/customcachekey_no_winners.json
index f6204db3..75ef628e 100644
--- a/exchange/cachetest/customcachekey_no_winners.json
+++ b/exchange/cachetest/customcachekey_no_winners.json
@@ -26,14 +26,14 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data": "{\"id\": \"appbid001\", \"impid\": \"oneImp\", \"price\": 7.64, \"cat\": [\"11_sports_22\"]}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value": "{\"id\": \"appbid001\", \"impid\": \"oneImp\", \"price\": 7.64, \"cat\": [\"11_sports_22\"]}"
         },
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data": "{\"id\":\"pubbid001\",\"impid\":\"oneImp\",\"price\":5.64,\"cat\":[\"33_news_44\"]}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value": "{\"id\":\"pubbid001\",\"impid\":\"oneImp\",\"price\":5.64,\"cat\":[\"33_news_44\"]}"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/debuglog_disabled.json b/exchange/cachetest/debuglog_disabled.json
index 88d6332c..a7efeff0 100644
--- a/exchange/cachetest/debuglog_disabled.json
+++ b/exchange/cachetest/debuglog_disabled.json
@@ -36,13 +36,13 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}"
         }, {
-            "Type": "json",
-            "TTLSeconds": 3660,
-            "Data": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}"
+            "type": "json",
+            "ttlseconds": 3660,
+            "value": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/debuglog_enabled.json b/exchange/cachetest/debuglog_enabled.json
index 670b694f..e6c85c57 100644
--- a/exchange/cachetest/debuglog_enabled.json
+++ b/exchange/cachetest/debuglog_enabled.json
@@ -36,17 +36,17 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}"
         }, {
-            "Type": "json",
-            "TTLSeconds": 3660,
-            "Data": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}"
+            "type": "json",
+            "ttlseconds": 3660,
+            "value": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}"
         }, {
-            "Type": "xml",
-            "TTLSeconds": 3600,
-            "Data": "\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cLog\u003e\u003cRequest\u003etest request string\u003c/Request\u003e\u003cHeaders\u003etest headers string\u003c/Headers\u003e\u003cResponse\u003etest response string\u003c/Response\u003e\u003c/Log\u003e"
+            "type": "xml",
+            "ttlseconds": 3600,
+            "value": "\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cLog\u003e\u003cRequest\u003etest request string\u003c/Request\u003e\u003cHeaders\u003etest headers string\u003c/Headers\u003e\u003cResponse\u003etest response string\u003c/Response\u003e\u003c/Log\u003e"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/defaultbanner.json b/exchange/cachetest/defaultbanner.json
index ca44589c..8bc59f63 100644
--- a/exchange/cachetest/defaultbanner.json
+++ b/exchange/cachetest/defaultbanner.json
@@ -24,13 +24,13 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600}"
         }, {
-            "Type": "json",
-            "TTLSeconds": 360,
-            "Data": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64 }"
+            "type": "json",
+            "ttlseconds": 360,
+            "value": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64 }"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/defaultbanner_no_bidders.json b/exchange/cachetest/defaultbanner_no_bidders.json
index d5171821..6dc534b5 100644
--- a/exchange/cachetest/defaultbanner_no_bidders.json
+++ b/exchange/cachetest/defaultbanner_no_bidders.json
@@ -24,9 +24,9 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600 }"
+            "type": "json",
+            "ttlseconds": 660,
+            "value":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\": 600 }"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/defaultbanner_no_winners.json b/exchange/cachetest/defaultbanner_no_winners.json
index fe43462b..154e1faa 100644
--- a/exchange/cachetest/defaultbanner_no_winners.json
+++ b/exchange/cachetest/defaultbanner_no_winners.json
@@ -24,13 +24,13 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\":   600 }"
+            "type": "json",
+            "ttlseconds": 660,
+            "value":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\":   600 }"
         }, {
-            "Type": "json",
-            "TTLSeconds": 360,
-            "Data": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64 }"
+            "type": "json",
+            "ttlseconds": 360,
+            "value": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64 }"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/defaultvideo.json b/exchange/cachetest/defaultvideo.json
index 4d38585d..8d7fcfed 100644
--- a/exchange/cachetest/defaultvideo.json
+++ b/exchange/cachetest/defaultvideo.json
@@ -26,13 +26,13 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}"
         }, {
-            "Type": "json",
-            "TTLSeconds": 3660,
-            "Data": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}"
+            "type": "json",
+            "ttlseconds": 3660,
+            "value": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64}"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/defaultvideo_no_bidders.json b/exchange/cachetest/defaultvideo_no_bidders.json
index 0a9e9f1d..ab8f13ff 100644
--- a/exchange/cachetest/defaultvideo_no_bidders.json
+++ b/exchange/cachetest/defaultvideo_no_bidders.json
@@ -26,9 +26,9 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64}"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/defaultvideo_no_winners.json b/exchange/cachetest/defaultvideo_no_winners.json
index 98a12a5a..21ad558c 100644
--- a/exchange/cachetest/defaultvideo_no_winners.json
+++ b/exchange/cachetest/defaultvideo_no_winners.json
@@ -28,13 +28,13 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 660,
-            "Data": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"nurl\": \"http://domain.com/win-notify/1\"}"
+            "type": "json",
+            "ttlseconds": 660,
+            "value": "{\"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"nurl\": \"http://domain.com/win-notify/1\"}"
         }, {
-            "Type": "json",
-            "TTLSeconds": 3660,
-            "Data": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64, \"nurl\": \"http://domain.com/win-notify/1\"}"
+            "type": "json",
+            "ttlseconds": 3660,
+            "value": "{\"id\": \"bidTwo\", \"impid\": \"twoImp\", \"price\": 5.64, \"nurl\": \"http://domain.com/win-notify/1\"}"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/cachetest/multibid.json b/exchange/cachetest/multibid.json
index f2405466..09095bd5 100644
--- a/exchange/cachetest/multibid.json
+++ b/exchange/cachetest/multibid.json
@@ -54,25 +54,25 @@
     ],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 360,
-            "Data":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\":   600 }"
+            "type": "json",
+            "ttlseconds": 360,
+            "value":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\":   600 }"
         }, {
-            "Type": "json",
-            "TTLSeconds": 260,
-            "Data": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64, \"exp\":   200 }"
+            "type": "json",
+            "ttlseconds": 260,
+            "value": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64, \"exp\":   200 }"
         }, {
-            "Type": "json",
-            "TTLSeconds": 360,
-            "Data": "{ \"id\": \"bidThree\", \"impid\": \"oneImp\", \"price\": 2.3 }"
+            "type": "json",
+            "ttlseconds": 360,
+            "value": "{ \"id\": \"bidThree\", \"impid\": \"oneImp\", \"price\": 2.3 }"
         }, {
-            "Type": "json",
-            "TTLSeconds": 0,
-            "Data": "{ \"id\": \"bidFour\", \"impid\": \"twoImp\", \"price\": 1.64 }"
+            "type": "json",
+            "ttlseconds": 0,
+            "value": "{ \"id\": \"bidFour\", \"impid\": \"twoImp\", \"price\": 1.64 }"
         }, {
-            "Type": "json",
-            "TTLSeconds": 960,
-            "Data": "{ \"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\":   900 }"
+            "type": "json",
+            "ttlseconds": 960,
+            "value": "{ \"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\":   900 }"
         }
     ],
     "targetDataIncludeWinners":true,
diff --git a/exchange/cachetest/multibid_no_bidders.json b/exchange/cachetest/multibid_no_bidders.json
index 1ec47579..446ae9ca 100644
--- a/exchange/cachetest/multibid_no_bidders.json
+++ b/exchange/cachetest/multibid_no_bidders.json
@@ -54,13 +54,13 @@
     ],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "Data": "{\"id\": \"bidOne\",\"impid\": \"oneImp\",\"price\": 7.64,\"exp\": 600}",
-            "TTLSeconds": 360
+            "type": "json",
+            "value": "{\"id\": \"bidOne\",\"impid\": \"oneImp\",\"price\": 7.64,\"exp\": 600}",
+            "ttlseconds": 360
         }, {
-            "Type": "json",
-            "Data": "{\"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\":   900}",
-            "TTLSeconds": 960
+            "type": "json",
+            "value": "{\"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\":   900}",
+            "ttlseconds": 960
         }
     ],
     "targetDataIncludeWinners":true,
diff --git a/exchange/cachetest/multibid_no_winners.json b/exchange/cachetest/multibid_no_winners.json
index 2221a54c..2f260076 100644
--- a/exchange/cachetest/multibid_no_winners.json
+++ b/exchange/cachetest/multibid_no_winners.json
@@ -54,25 +54,25 @@
     ],
     "expectedCacheables": [
         {
-            "Type": "json",
-            "TTLSeconds": 360,
-            "Data":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\":   600 }"
+            "type": "json",
+            "ttlseconds": 360,
+            "value":"{ \"id\": \"bidOne\", \"impid\": \"oneImp\", \"price\": 7.64, \"exp\":   600 }"
         }, {
-            "Type": "json",
-            "TTLSeconds": 260,
-            "Data": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64, \"exp\":   200 }"
+            "type": "json",
+            "ttlseconds": 260,
+            "value": "{ \"id\": \"bidTwo\", \"impid\": \"oneImp\", \"price\": 5.64, \"exp\":   200 }"
         }, {
-            "Type": "json",
-            "TTLSeconds": 360,
-            "Data": "{ \"id\": \"bidThree\", \"impid\": \"oneImp\", \"price\": 2.3 }"
+            "type": "json",
+            "ttlseconds": 360,
+            "value": "{ \"id\": \"bidThree\", \"impid\": \"oneImp\", \"price\": 2.3 }"
         }, {
-            "Type": "json",
-            "TTLSeconds": 0,
-            "Data": "{ \"id\": \"bidFour\", \"impid\": \"twoImp\", \"price\": 1.64 }"
+            "type": "json",
+            "ttlseconds": 0,
+            "value": "{ \"id\": \"bidFour\", \"impid\": \"twoImp\", \"price\": 1.64 }"
         }, {
-            "Type": "json",
-            "TTLSeconds": 960,
-            "Data": "{ \"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\":   900 }"
+            "type": "json",
+            "ttlseconds": 960,
+            "value": "{ \"id\": \"bidFive\", \"impid\": \"twoImp\", \"price\": 7.64, \"exp\":   900 }"
         }
     ],
     "targetDataIncludeWinners":false,
diff --git a/exchange/customcachekeytest/customcachekey.json b/exchange/customcachekeytest/customcachekey.json
index e578f980..9b903575 100644
--- a/exchange/customcachekeytest/customcachekey.json
+++ b/exchange/customcachekeytest/customcachekey.json
@@ -28,14 +28,14 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "xml",
-            "TTLSeconds": 660,
-            "Key": "11_sports_22_",
-            "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" 
+            "type": "xml",
+            "ttlseconds": 660,
+            "key": "11_sports_22_",
+            "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e"
         }, {
-            "Type": "xml",
-            "TTLSeconds": 660,
-            "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherdomain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" 
+            "type": "xml",
+            "ttlseconds": 660,
+            "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherdomain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/customcachekeytest/customcachekey_no_bidders.json b/exchange/customcachekeytest/customcachekey_no_bidders.json
index 0f09c8db..589d1e6b 100644
--- a/exchange/customcachekeytest/customcachekey_no_bidders.json
+++ b/exchange/customcachekeytest/customcachekey_no_bidders.json
@@ -27,10 +27,10 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "xml",
-            "TTLSeconds": 660,
-            "Key": "11_sports_22_",
-            "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" 
+            "type": "xml",
+            "ttlseconds": 660,
+            "key": "11_sports_22_",
+            "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/customcachekeytest/customcachekey_no_winners.json b/exchange/customcachekeytest/customcachekey_no_winners.json
index f21c8bda..3eaf6cfb 100644
--- a/exchange/customcachekeytest/customcachekey_no_winners.json
+++ b/exchange/customcachekeytest/customcachekey_no_winners.json
@@ -27,14 +27,14 @@
     }],
     "expectedCacheables": [
         {
-            "Type": "xml",
-            "TTLSeconds": 660,
-            "Key": "11_sports_22_",
-            "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" 
+            "type": "xml",
+            "ttlseconds": 660,
+            "key": "11_sports_22_",
+            "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e"
         }, {
-            "Type": "xml",
-            "TTLSeconds": 660,
-            "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" 
+            "type": "xml",
+            "ttlseconds": 660,
+            "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e"
         }
     ],
     "defaultTTLs": {
diff --git a/exchange/impcustomcachekeytest/multiImpVast.json b/exchange/impcustomcachekeytest/multiImpVast.json
index bf0b310b..db106974 100644
--- a/exchange/impcustomcachekeytest/multiImpVast.json
+++ b/exchange/impcustomcachekeytest/multiImpVast.json
@@ -64,29 +64,29 @@
 		"bidder": "rubicon"
 	}],
 	"expectedCacheables": [{
-			"Type": "xml",
-			"TTLSeconds": 360,
-            "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://bidoneproducts.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" 
+			"type": "xml",
+			"ttlseconds": 360,
+            "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://bidoneproducts.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e"
 		}, {
-			"Type": "xml",
-			"TTLSeconds": 260,
-			"Key": "34_news_44",
-            "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" 
+			"type": "xml",
+			"ttlseconds": 260,
+			"key": "34_news_44",
+            "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://domain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e"
 		}, {
-			"Type": "xml",
-			"TTLSeconds": 360,
-            "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherdomain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" 
+			"type": "xml",
+			"ttlseconds": 360,
+            "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherdomain.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e"
 		},
 		{
-			"Type": "xml",
-			"TTLSeconds": 3660,
-            "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://bidseveryday.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" 
+			"type": "xml",
+			"ttlseconds": 3660,
+            "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://bidseveryday.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e"
 		},
 		{
-			"Type": "xml",
-			"TTLSeconds": 960,
-			"Key": "13_sports_22",
-            "Data":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherbidder.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e" 
+			"type": "xml",
+			"ttlseconds": 960,
+			"key": "13_sports_22",
+            "value":"\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://anotherbidder.com/win-notify/1]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e"
 		}
 	],
 	"defaultTTLs": {
diff --git a/exchange/impcustomcachekeytest/multiImpVideo.json b/exchange/impcustomcachekeytest/multiImpVideo.json
index 7c1d7af0..fc343888 100644
--- a/exchange/impcustomcachekeytest/multiImpVideo.json
+++ b/exchange/impcustomcachekeytest/multiImpVideo.json
@@ -60,32 +60,32 @@
 		"bidder": "rubicon"
 	}],
 	"expectedCacheables": [{
-			"Type": "xml",
-			"TTLSeconds": 3660,
-			"Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
-			"Key": "12.00_sports_30s"
+			"type": "xml",
+			"ttlseconds": 3660,
+			"value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
+			"key": "12.00_sports_30s"
 		}, {
-			"Type": "xml",
-			"TTLSeconds": 3660,
-			"Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
-			"Key": "20_news_30s"
+			"type": "xml",
+			"ttlseconds": 3660,
+			"value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
+			"key": "20_news_30s"
 		}, {
-			"Type": "xml",
-			"TTLSeconds": 3660,
-			"Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
-			"Key": "18_furniture_30s"
+			"type": "xml",
+			"ttlseconds": 3660,
+			"value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
+			"key": "18_furniture_30s"
 		},
 		{
-			"Type": "xml",
-			"TTLSeconds": 3660,
-			"Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
-			"Key": "17_auto_30s"
+			"type": "xml",
+			"ttlseconds": 3660,
+			"value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
+			"key": "17_auto_30s"
 		},
 		{
-			"Type": "xml",
-			"TTLSeconds": 3660,
-			"Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://rubicon_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
-			"Key": "17_music_30s"
+			"type": "xml",
+			"ttlseconds": 3660,
+			"value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://rubicon_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
+			"key": "17_music_30s"
 		}
 	],
 	"defaultTTLs": {
diff --git a/exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json b/exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json
index 0b8ee141..4dd15344 100644
--- a/exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json
+++ b/exchange/impcustomcachekeytest/multiImpVideoNoIncludeBidderKeys.json
@@ -61,16 +61,16 @@
 	}],
 	"expectedCacheables": [
 		{
-			"Type": "xml",
-			"TTLSeconds": 3660,
-			"Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
-			"Key": "20_news_30s"
+			"type": "xml",
+			"ttlseconds": 3660,
+			"value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://spotx_1_0.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
+			"key": "20_news_30s"
 		},
 		{
-			"Type": "xml",
-			"TTLSeconds": 3660,
-			"Data": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
-			"Key": "18_furniture_30s"
+			"type": "xml",
+			"ttlseconds": 3660,
+			"value": "\u003cVAST version=\"3.0\"\u003e\u003cAd\u003e\u003cWrapper\u003e\u003cAdSystem\u003eprebid.org wrapper\u003c/AdSystem\u003e\u003cVASTAdTagURI\u003e\u003c![CDATA[http://apn_1_1.com]]\u003e\u003c/VASTAdTagURI\u003e\u003cImpression\u003e\u003c/Impression\u003e\u003cCreatives\u003e\u003c/Creatives\u003e\u003c/Wrapper\u003e\u003c/Ad\u003e\u003c/VAST\u003e",
+			"key": "18_furniture_30s"
 		}
 	],
 	"defaultTTLs": {
diff --git a/prebid_cache_client/client_test.go b/prebid_cache_client/client_test.go
index 72fd2761..0bd29699 100644
--- a/prebid_cache_client/client_test.go
+++ b/prebid_cache_client/client_test.go
@@ -174,8 +174,11 @@ func TestEncodeValueToBuffer(t *testing.T) {
 		Type:       TypeJSON,
 		Data:       json.RawMessage(`{}`),
 		TTLSeconds: 300,
+		BidID:      "bid",
+		Bidder:     "bdr",
+		Timestamp:  123456789,
 	}
-	expected := string(`{"type":"json","ttlseconds":300,"value":{}}`)
+	expected := string(`{"type":"json","ttlseconds":300,"value":{},"bidid":"bid","bidder":"bdr","timestamp":123456789}`)
 	_ = encodeValueToBuffer(testCache, false, buf)
 	actual := buf.String()
 	assertStringEqual(t, expected, actual)