Repository: prebid/prebid-server
Pull request #1467 (2020-09-01 16:55:00)

Vtrack and event endpoints
Adds /event and /vtrack endpoints

**Note:** _events_enabled_ column is added to accounts_account table:

_SELECT uuid, price_granularity, **events_enabled** FROM accounts_account where uuid = $1 LIMIT 1_

You can add the column to your _postgres_ table by running the following command (adjust accordingly depending on your relational database):

ALTER TABLE account_accounts ADD COLUMN events_enabled BOOLEAN DEFAULT FALSE;

EDIT: Implements https://github.com/prebid/prebid-server/issues/1015

danielguedesb: > This is awesome! Thank you for tackling such a large feature implementation! My first round of feedback is largely centered around Go specific styling and use of the legacy (read: outdated) account code.
> 
> Just to verify, you're coding against this spec?
> https://docs.prebid.org/prebid-server/endpoints/pbs-endpoint-event.html

Yes, the implementation was based on PBS's Java version.
Thanks for the feedback!
SyntaxNode: We merged in https://github.com/prebid/prebid-server/pull/1483 today, so you may take advantage of that code here.
bsardo: This is looking good. Nothing new to add to @SyntaxNode's test coverage comments.
laurb9: I have run a few requests against the event endpoint in my test server. It looks good, with a few comments:

2) Without passing `x=1` explicitly, the event is not logged.
The default should be to log the event, the examples in the doc only pass `x` when they need
to skip logging, otherwise they leave it out.

> the event url in this scenario carries an extra parameter "x=0" which tells PBS to not log this event to analytics.

```
curl -si 'http://localhost:8000/event?t=imp&b=123&a=test'
HTTP/1.1 204 No Content
```

2) With an invalid account, the server returns 500. I think it should be 400.
```
curl -si 'http://localhost:8000/event?t=imp&b=123&a=invalid_acct&x=1'
HTTP/1.1 500 Internal Server Error

Invalid request: Prebid-server could not verify the Account ID. Please reach out to the prebid server host.
```

3) The logged event has `Request` capitalized but `account` in lowercase. It should be consistent with the
other jsonified events.

```json
{"type":"/event","Request":{"type":"imp","analytics":"1","bidid":"123","account_id":"valid_acct"},"account":{"id":"VALID_ACCT","disabled":false,"cache_ttl":{"Banner":600,"Video":600,"Native":600,"Audio":3600},"events_enabled":true}}
```

4) The /event endpoint allows any (or no) bidder (see above). I think it should be required.

Will do the /vtrack after lunch and post my results here as well.

danielguedesb: 
> 1. Without passing `x=1` explicitly, the event is not logged.
>    The default should be to log the event, the examples in the doc only pass `x` when they need
>    to skip logging, otherwise they leave it out.
> 

You're right - Taking a closer look at PBS [Java](https://github.com/rubicon-project/prebid-server-java/blob/0f4a78d9b7b45fb65f25ffdb747334f21203a730/src/main/java/org/prebid/server/events/EventUtil.java#L124), they default to 'x=1' unless you specify otherwise.

> 2. With an invalid account, the server returns 500. I think it should be 400.

It seems to be the returned status code in other places as well - refactored.

> 3. The logged event has `Request` capitalized but `account` in lowercase. It should be consistent with the

I agree, refactored.

> 
> 4. The /event endpoint allows any (or no) bidder (see above). I think it should be required.
> 

Java's version doesn't enforce it, any particular reason why it should/shouldn't be required?


laurb9: I used a test request as described in #1015 and it appears /vtrack endpoint does not decode the `value` field, which causes it to fail writing to cache.
```
curl 'http://localhost:8000/vtrack?a=valid_acct' --header 'content-type: application/json' --data @- <<EOF 
{
  "puts": [
    {
      "bidid": "test-bid-id-1",
      "bidder": "openx",
      "timestamp": 123456789,
      "type": "xml",
      "value": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><VAST version=\"2.0\"><Ad id=\"123456\"><Wrapper><VASTAdTagURI><![CDATA[http://localhost/]]></VASTAdTagURI><Impression><![CDATA[http://sampledomain.com/event/impression]]></Impression><Creatives></Creatives></Wrapper></Ad></VAST>",
      "ttlseconds": 3600
    }
  ]
}
EOF
```
response
```
Error(s) updating vast: Prebid Cache call to http://cache:2424/cache returned 400: Request body {"puts":[{"type":"xml","ttlseconds":3600,"value":}]} is not valid JSON.
```

I have modified the request to replace `value` with `Data` and the request succeeds but the json response is returned with `content-type: text/plain`

SyntaxNode: @laurb9 Thank you for the detailed review. @danielguedesb Thank you being very patient with the review process. This is looking good to us. Do you both agree we're ready to merge in?
laurb9: > @laurb9 Thank you for the detailed review. @danielguedesb Thank you being very patient with the review process. This is looking good to us. Do you both agree we're ready to merge in?

Looks good to me as well.

There work left to be done for event feature parity with java but that is out of scope with the description for this PR, so they can be done separately - I think wurl insertion in bids and VAST rewriting when pbs caches is remaining.


Patch:
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/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..7825a5f3 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"`
@@ -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")
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/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/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)