Repository: prebid/prebid-server
Pull request #1334 (2020-06-04 04:04:45)

Privacy: Limit Ad Tracking
Adds LMT support and defaults to enabled.


Patch:
diff --git a/adapters/adtarget/adtarget.go b/adapters/adtarget/adtarget.go
new file mode 100644
index 00000000..77622d45
--- /dev/null
+++ b/adapters/adtarget/adtarget.go
@@ -0,0 +1,189 @@
+package adtarget
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type AdtargetAdapter struct {
+	endpoint string
+}
+
+type adtargetImpExt struct {
+	Adtarget openrtb_ext.ExtImpAdtarget `json:"adtarget"`
+}
+
+func (a *AdtargetAdapter) MakeRequests(request *openrtb.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+
+	totalImps := len(request.Imp)
+	errors := make([]error, 0, totalImps)
+	imp2source := make(map[int][]int)
+
+	for i := 0; i < totalImps; i++ {
+
+		sourceId, err := validateImpressionAndSetExt(&request.Imp[i])
+
+		if err != nil {
+			errors = append(errors, err)
+			continue
+		}
+
+		if _, ok := imp2source[sourceId]; !ok {
+			imp2source[sourceId] = make([]int, 0, totalImps-i)
+		}
+
+		imp2source[sourceId] = append(imp2source[sourceId], i)
+
+	}
+
+	totalReqs := len(imp2source)
+	if 0 == totalReqs {
+		return nil, errors
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	reqs := make([]*adapters.RequestData, 0, totalReqs)
+
+	imps := request.Imp
+	request.Imp = make([]openrtb.Imp, 0, len(imps))
+	for sourceId, impIndexes := range imp2source {
+		request.Imp = request.Imp[:0]
+
+		for i := 0; i < len(impIndexes); i++ {
+			request.Imp = append(request.Imp, imps[impIndexes[i]])
+		}
+
+		body, err := json.Marshal(request)
+		if err != nil {
+			errors = append(errors, fmt.Errorf("error while encoding bidRequest, err: %s", err))
+			return nil, errors
+		}
+
+		reqs = append(reqs, &adapters.RequestData{
+			Method:  "POST",
+			Uri:     a.endpoint + fmt.Sprintf("?aid=%d", sourceId),
+			Body:    body,
+			Headers: headers,
+		})
+	}
+
+	return reqs, errors
+}
+
+func (a *AdtargetAdapter) MakeBids(bidReq *openrtb.BidRequest, unused *adapters.RequestData, httpRes *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+
+	if httpRes.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+	if httpRes.StatusCode == http.StatusBadRequest {
+		return nil, []error{&errortypes.BadInput{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", httpRes.StatusCode),
+		}}
+	}
+	var bidResp openrtb.BidResponse
+	if err := json.Unmarshal(httpRes.Body, &bidResp); err != nil {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("error while decoding response, err: %s", err),
+		}}
+	}
+
+	bidResponse := adapters.NewBidderResponse()
+	var errors []error
+
+	var impOK bool
+	for _, sb := range bidResp.SeatBid {
+		for i := 0; i < len(sb.Bid); i++ {
+
+			bid := sb.Bid[i]
+
+			impOK = false
+			mediaType := openrtb_ext.BidTypeBanner
+			for _, imp := range bidReq.Imp {
+				if imp.ID == bid.ImpID {
+
+					impOK = true
+
+					if imp.Video != nil {
+						mediaType = openrtb_ext.BidTypeVideo
+						break
+					}
+				}
+			}
+
+			if !impOK {
+				errors = append(errors, &errortypes.BadServerResponse{
+					Message: fmt.Sprintf("ignoring bid id=%s, request doesn't contain any impression with id=%s", bid.ID, bid.ImpID),
+				})
+				continue
+			}
+
+			bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+				Bid:     &bid,
+				BidType: mediaType,
+			})
+		}
+	}
+
+	return bidResponse, errors
+}
+
+func validateImpressionAndSetExt(imp *openrtb.Imp) (int, error) {
+
+	if imp.Banner == nil && imp.Video == nil {
+		return 0, &errortypes.BadInput{
+			Message: fmt.Sprintf("ignoring imp id=%s, Adtarget supports only Video and Banner", imp.ID),
+		}
+	}
+
+	if 0 == len(imp.Ext) {
+		return 0, &errortypes.BadInput{
+			Message: fmt.Sprintf("ignoring imp id=%s, extImpBidder is empty", imp.ID),
+		}
+	}
+
+	var bidderExt adapters.ExtImpBidder
+
+	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
+		return 0, &errortypes.BadInput{
+			Message: fmt.Sprintf("ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err),
+		}
+	}
+
+	impExt := openrtb_ext.ExtImpAdtarget{}
+	err := json.Unmarshal(bidderExt.Bidder, &impExt)
+	if err != nil {
+		return 0, &errortypes.BadInput{
+			Message: fmt.Sprintf("ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err),
+		}
+	}
+
+	// common extension for all impressions
+	var impExtBuffer []byte
+
+	impExtBuffer, err = json.Marshal(&adtargetImpExt{
+		Adtarget: impExt,
+	})
+
+	if impExt.BidFloor > 0 {
+		imp.BidFloor = impExt.BidFloor
+	}
+
+	imp.Ext = impExtBuffer
+
+	return impExt.SourceId, nil
+}
+
+func NewAdtargetBidder(endpoint string) *AdtargetAdapter {
+	return &AdtargetAdapter{
+		endpoint: endpoint,
+	}
+}
diff --git a/adapters/adtarget/usersync.go b/adapters/adtarget/usersync.go
new file mode 100644
index 00000000..20bced25
--- /dev/null
+++ b/adapters/adtarget/usersync.go
@@ -0,0 +1,12 @@
+package adtarget
+
+import (
+	"text/template"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/usersync"
+)
+
+func NewAdtargetSyncer(temp *template.Template) usersync.Usersyncer {
+	return adapters.NewSyncer("adtarget", 0, temp, adapters.SyncTypeRedirect)
+}
diff --git a/config/config.go b/config/config.go
index e93aed46..0f470c6a 100755
--- a/config/config.go
+++ b/config/config.go
@@ -49,6 +49,7 @@ type Configuration struct {
 	AMPTimeoutAdjustment int64              `mapstructure:"amp_timeout_adjustment_ms"`
 	GDPR                 GDPR               `mapstructure:"gdpr"`
 	CCPA                 CCPA               `mapstructure:"ccpa"`
+	LMT                  LMT                `mapstructure:"lmt"`
 	CurrencyConverter    CurrencyConverter  `mapstructure:"currency_converter"`
 	DefReqConfig         DefReqConfig       `mapstructure:"default_request"`
 
@@ -66,6 +67,8 @@ type Configuration struct {
 	PemCertsFile string `mapstructure:"certificates_file"`
 	// Custom headers to handle request timeouts from queueing infrastructure
 	RequestTimeoutHeaders RequestTimeoutHeaders `mapstructure:"request_timeout_headers"`
+	// Debug/logging flags go here
+	Debug Debug `mapstructure:"debug"`
 }
 
 const MIN_COOKIE_SIZE_BYTES = 500
@@ -104,6 +107,7 @@ func (cfg *Configuration) validate() configErrors {
 	errs = cfg.GDPR.validate(errs)
 	errs = cfg.CurrencyConverter.validate(errs)
 	errs = validateAdapters(cfg.Adapters, errs)
+	errs = cfg.Debug.validate(errs)
 	return errs
 }
 
@@ -136,6 +140,13 @@ func (cfg *AuctionTimeouts) LimitAuctionTimeout(requested time.Duration) time.Du
 	return requested
 }
 
+// Privacy is a grouping of privacy related configs to assist in dependency injection.
+type Privacy struct {
+	CCPA CCPA
+	GDPR GDPR
+	LMT  LMT
+}
+
 type GDPR struct {
 	HostVendorID            int          `mapstructure:"host_vendor_id"`
 	UsersyncIfAmbiguous     bool         `mapstructure:"usersync_if_ambiguous"`
@@ -190,6 +201,10 @@ type CCPA struct {
 	Enforce bool `mapstructure:"enforce"`
 }
 
+type LMT struct {
+	Enforce bool `mapstructure:"enforce"`
+}
+
 type Analytics struct {
 	File FileLogs `mapstructure:"file"`
 }
@@ -450,6 +465,30 @@ type DefReqFiles struct {
 	FileName string `mapstructure:"name"`
 }
 
+type Debug struct {
+	TimeoutNotification TimeoutNotification `mapstructure:"timeout_notification"`
+}
+
+func (cfg *Debug) validate(errs configErrors) configErrors {
+	return cfg.TimeoutNotification.validate(errs)
+}
+
+type TimeoutNotification struct {
+	// Log timeout notifications in the application log
+	Log bool `mapstructure:"log"`
+	// Fraction of notifications to log
+	SamplingRate float32 `mapstructure:"sampling_rate"`
+	// Only log failures
+	FailOnly bool `mapstructure:"fail_only"`
+}
+
+func (cfg *TimeoutNotification) validate(errs configErrors) configErrors {
+	if cfg.SamplingRate < 0.0 || cfg.SamplingRate > 1.0 {
+		errs = append(errs, fmt.Errorf("debug.timeout_notification.sampling_rate must be positive and not greater than 1.0. Got %f", cfg.SamplingRate))
+	}
+	return errs
+}
+
 // New uses viper to get our server configurations.
 func New(v *viper.Viper) (*Configuration, error) {
 	var c Configuration
@@ -521,6 +560,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdkernel, "https://sync.adkernel.com/user-sync?t=image&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadkernel%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BUID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdkernelAdn, "https://tag.adkernel.com/syncr?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3DadkernelAdn%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BUID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdpone, "https://usersync.adpone.com/csync?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadpone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdtarget, "https://sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadtarget%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdtelligent, "https://sync.adtelligent.com/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadtelligent%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderAdmixer, "https://inv-nets.admixer.net/adxcm.aspx?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=1&rurl="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dadmixer%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%24visitor_cookie%24%24")
 	// openrtb_ext.BidderAdOcean doesn't have a good default.
@@ -575,7 +615,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	// openrtb_ext.BidderVrtcal doesn't have a good default.
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldlab, "https://ad.yieldlab.net/mr?t=2&pid=9140838&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldlab%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25%25YL_UID%25%25")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldmo, "https://ads.yieldmo.com/pbsync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldmo%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
-	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldone, "https://y.one.impact-ad.jp/hbs_sc?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderYieldone, "https://y.one.impact-ad.jp/hbs_cs?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirectUri="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dyieldone%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderZeroClickFraud, "https://s.0cf.io/sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dzeroclickfraud%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7Buid%7D")
 }
 
@@ -725,6 +765,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.adocean.endpoint", "https://{{.Host}}")
 	v.SetDefault("adapters.adoppler.endpoint", "http://app.trustedmarketplace.io/ads")
 	v.SetDefault("adapters.adpone.endpoint", "http://rtb.adpone.com/bid-request?src=prebid_server")
+	v.SetDefault("adapters.adtarget.endpoint", "http://ghb.console.adtarget.com.tr/pbs/ortb")
 	v.SetDefault("adapters.adtelligent.endpoint", "http://ghb.adtelligent.com/pbs/ortb")
 	v.SetDefault("adapters.advangelists.endpoint", "http://nep.advangelists.com/xp/get?pubid={{.PublisherID}}")
 	v.SetDefault("adapters.aja.endpoint", "https://ad.as.amanad.adtdp.com/v1/bid/4")
@@ -776,7 +817,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.telaria.endpoint", "https://ads.tremorhub.com/ad/rtb/prebid")
 	v.SetDefault("adapters.triplelift_native.disabled", true)
 	v.SetDefault("adapters.triplelift_native.extra_info", "{\"publisher_whitelist\":[]}")
-	v.SetDefault("adapters.triplelift.endpoint", "https://tlx.3lift.com/s2s/auction?supplier_id=20")
+	v.SetDefault("adapters.triplelift.endpoint", "https://tlx.3lift.com/s2s/auction?sra=1&supplier_id=20")
 	v.SetDefault("adapters.ucfunnel.endpoint", "http://apac-hk-adx.aralego.com/prebid")
 	v.SetDefault("adapters.unruly.endpoint", "http://targeting.unrulymedia.com/openrtb/2.2")
 	v.SetDefault("adapters.valueimpression.endpoint", "https://rtb.valueimpression.com/endpoint")
@@ -807,6 +848,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("gdpr.tcf2.purpose_one_treatement.access_allowed", true)
 	v.SetDefault("gdpr.amp_exception", false)
 	v.SetDefault("ccpa.enforce", false)
+	v.SetDefault("lmt.enforce", true)
 	v.SetDefault("currency_converter.fetch_url", "https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json")
 	v.SetDefault("currency_converter.fetch_interval_seconds", 1800) // fetch currency rates every 30 minutes
 	v.SetDefault("default_request.type", "")
@@ -820,6 +862,10 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("request_timeout_headers.request_time_in_queue", "")
 	v.SetDefault("request_timeout_headers.request_timeout_in_queue", "")
 
+	v.SetDefault("debug.timeout_notification.log", false)
+	v.SetDefault("debug.timeout_notification.sampling_rate", 0.0)
+	v.SetDefault("debug.timeout_notification.fail_only", false)
+
 	// Set environment variable support:
 	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
 	v.SetEnvPrefix("PBS")
diff --git a/config/util/loggers.go b/config/util/loggers.go
new file mode 100644
index 00000000..88702e68
--- /dev/null
+++ b/config/util/loggers.go
@@ -0,0 +1,24 @@
+package util
+
+import (
+	"math/rand"
+)
+
+type logMsg func(string, ...interface{})
+
+type randomGenerator func() float32
+
+// LogRandomSample will log a randam sample of the messages it is sent, based on the chance to log
+// chance = 1.0 => always log,
+// chance = 0.0 => never log
+func LogRandomSample(msg string, logger logMsg, chance float32) {
+	logRandomSampleImpl(msg, logger, chance, rand.Float32)
+}
+
+func logRandomSampleImpl(msg string, logger logMsg, chance float32, randGenerator randomGenerator) {
+	if chance < 1.0 && randGenerator() > chance {
+		// this is the chance we don't log anything
+		return
+	}
+	logger(msg)
+}
diff --git a/docs/bidders/adtarget.md b/docs/bidders/adtarget.md
new file mode 100644
index 00000000..b658a728
--- /dev/null
+++ b/docs/bidders/adtarget.md
@@ -0,0 +1,5 @@
+# Adtarget bidder
+
+To use the Adtarget bidder you will need an aid from an exchange account on [https://console.adtarget.com.tr](adtarget.com.tr).
+
+For further information, please contact kamil@adtarget.com.tr
\ No newline at end of file
diff --git a/docs/bidders/pubnative.md b/docs/bidders/pubnative.md
new file mode 100644
index 00000000..a25cafe0
--- /dev/null
+++ b/docs/bidders/pubnative.md
@@ -0,0 +1,62 @@
+# Pubnative Bidder
+
+## Prerequisite
+Before adding PubNative as a new bidder, there are 3 prerequisites:
+- As a Publisher, you need to have Prebid Mobile SDK integrated.
+- You need a configured Prebid Server (either self-hosted or hosted by 3rd party).
+- You need to be integrated with Ad Server SDK (e.g. Mopub) or internal product which communicates with Prebid Mobile SDK.
+
+Please see [documentation](https://developers.pubnative.net/docs/prebid-adding-pubnative-as-a-bidder) for more info.
+
+## Configuration 
+
+- bidder should be always set to "pubnative" (`imp.ext.pubnative`)
+- zone_id (int) should be always set to 1, unless special use case agreed with our account manager. (`imp.ext.pubnative.zone_id`)
+- app_auth_token (string) is unique per publisher app. Please contact our account manager to obtain yours. (`imp.ext.pubnative.app_auth_token`)
+
+An example is illustrated in a section below.
+
+## Testing
+
+Please consult with our Account Manager for testing. 
+We need to confirm that your ad request is correctly received by our system.
+
+The following test parameters can be used to verify that Prebid Server is working properly with the 
+Pubnative adapter.
+
+The following json can be used to do a request to prebid server for verifying its integration with Pubnative adapter.
+
+```json
+{
+    "id": "some-impression-id",
+    "site": {
+      "page": "https://good.site/url"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "pubnative": {
+            "zone_id": 1,
+            "app_auth_token": "b620e282f3c74787beedda34336a4821"
+          }
+        }
+      }
+    ],
+    "device": {
+      "os": "android",
+      "h": 700,
+      "w": 375
+    },
+    "tmax": 500,
+    "test": 1
+}
+```
\ No newline at end of file
diff --git a/docs/developers/add-new-bidder.md b/docs/developers/add-new-bidder.md
index e68185fd..d76a1fd2 100644
--- a/docs/developers/add-new-bidder.md
+++ b/docs/developers/add-new-bidder.md
@@ -46,6 +46,16 @@ If bidder is going to support long form video make sure bidder has:
 Note: `bid.bidVideo.PrimaryCategory` or `TypedBid.bid.Cat` should be specified.
 To learn more about IAB categories, please refer to this convenience link (not the final official definition): [IAB categories](https://adtagmacros.com/list-of-iab-categories-for-advertisement/)
 
+### Timeout notification support
+This is an optional feature. If you wish to get timeout notifications when a bid request from PBS times out, you can implement the
+`MakeTimeoutNotification` method in your adapter. If you do not wish timeout notification, do not implement the method.
+
+`func (a *Adapter) MakeTimeoutNotification(req *adapters.RequestData) (*adapters.RequestData, []error)`
+
+Here the `RequestData` supplied as an argument is the request returned from `MakeRequests` that timed out. If an adapter generates
+multiple requests, and more than one of them times out, then there will be a call to `MakeTimeoutNotification` for each failed
+request. The function should then return a `RequestData` object that will be the timeout notification to be sent to the bidder, or a list of errors encountered trying to create the timeout notification request. Timeout notifications will not generate subsequent timeout notifications if they timeout or fail.
+
 ## Test Your Bidder
 
 ### Automated Tests
diff --git a/docs/endpoints/openrtb2/auction.md b/docs/endpoints/openrtb2/auction.md
index 67430e51..d0921618 100644
--- a/docs/endpoints/openrtb2/auction.md
+++ b/docs/endpoints/openrtb2/auction.md
@@ -14,53 +14,94 @@ This endpoint runs an auction with the given OpenRTB 2.5 bid request.
 
 ### Sample request
 
-The [Prebid sample ad](http://prebid.org/examples/pbjs_demo.html) can be loaded with the request sample [here](../../../endpoints/openrtb2/sample-requests/valid-whole/exemplary/prebid-test-ad.json).
+This is a sample OpenRTB 2.5 bid request for a Xandr (formerly AppNexus) test placement. Please note, the Xandr Ad Server will only
+respond with a bid if the "test" field is set to 1.
 
-Other examples can be found in [endpoints/openrtb2/sample-requests/valid-whole/exemplary](../../../endpoints/openrtb2/sample-requests/valid-whole/exemplary).
+```
+{
+  "id": "some-request-id",
+  "test": 1,
+  "site": {
+    "page": "prebid.org"
+  },
+  "imp": [{
+    "id": "some-impression-id",
+    "banner": {
+      "format": [{
+        "w": 600,
+        "h": 500
+      }, {
+        "w": 300,
+        "h": 600
+      }]
+    },
+    "ext": {
+      "appnexus": {
+        "placementId": 12883451
+      }
+    }
+  }],
+  "tmax": 500
+}
+```
+
+Additional examples can be found in [endpoints/openrtb2/sample-requests/valid-whole](../../../endpoints/openrtb2/sample-requests/valid-whole).
 
 ### Sample Response
 
 This endpoint will respond with either:
 
-- An OpenRTB 2.5 BidResponse, or
-- An HTTP 400 status code if the request is malformed
+- An OpenRTB 2.5 bid response, or
+- HTTP 400 if the request is malformed, or
+- HTTP 503 if the account or app specified in the request is blacklisted
 
-A "hello world" response from the prebid sample ad request is shown below.
+This is the corresponding response to the above sample OpenRTB 2.5 bid request, with the `ext.debug` field removed and the `seatbid.bid.adm` field simplified.
 
 ```
 {
   "id": "some-request-id",
-  "seatbid": [
-    {
-      "seat": "appnexus"
-      "bid": [
-        {
-          "id": "4625436751433509010",
-          "impid": "some-impression-id",
-          "price": 0.5,
-          "adm": "<script type=\"application/javascript\">... the creative javascript is in here ... </script>",
-          "adid": "29681110",
-          "adomain": [
-            "appnexus.com"
-          ],
-          "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
-          "cid": "958",
-          "crid": "29681110",
-          "w": 300,
-          "h": 250,
-          "ext": {
-            "bidder": {
-              "appnexus": {
-                "brand_id": 1,
-                "auction_id": 6127490747252133000,
-                "bidder_id": 2
-              }
-            }
+  "seatbid": [{
+    "seat": "appnexus",
+    "bid": [{
+      "id": "145556724130495288",
+      "impid": "some-impression-id",
+      "price": 0.01,
+      "adm": "<script type=\"application/javascript\">...</script>",
+      "adid": "107987536",
+      "adomain": [
+        "appnexus.com"
+      ],
+      "iurl": "https://nym1-ib.adnxs.com/cr?id=107987536",
+      "cid": "3532",
+      "crid": "107987536",
+      "w": 600,
+      "h": 500,
+      "ext": {
+        "prebid": {
+          "type": "banner",
+          "video": {
+            "duration": 0,
+            "primary_category": ""
+          }
+        },
+        "bidder": {
+          "appnexus": {
+            "brand_id": 1,
+            "auction_id": 7311907164510136364,
+            "bidder_id": 2,
+            "bid_ad_type": 0
           }
         }
-      ]
-    }
-  ]
+      }
+    }]
+  }],
+  "cur": "USD",
+  "ext": {
+    "responsetimemillis": {
+      "appnexus": 10
+    },
+    "tmaxrequest": 500
+  }
 }
 ```
 
@@ -69,12 +110,12 @@ A "hello world" response from the prebid sample ad request is shown below.
 #### Conventions
 
 OpenRTB 2.5 permits exchanges to define their own extensions to any object from the spec.
-These fall under the `ext` property of JSON objects.
+These fall under the `ext` field of JSON objects.
 
 If `ext` is defined on an object, Prebid Server uses the following conventions:
 
-1. `ext` in "Request objects" uses `ext.prebid` and/or `ext.{anyBidderCode}`.
-2. `ext` on "Response objects" uses `ext.prebid` and/or `ext.bidder`.
+1. `ext` in "request objects" uses `ext.prebid` and/or `ext.{anyBidderCode}`.
+2. `ext` on "response objects" uses `ext.prebid` and/or `ext.bidder`.
 The only exception here is the top-level `BidResponse`, because it's bidder-independent.
 
 `ext.{anyBidderCode}` and `ext.bidder` extensions are defined by bidders.
@@ -84,9 +125,9 @@ Exceptions are made for extensions with "standard" recommendations:
 
 - `request.user.ext.digitrust` -- To support Digitrust
 - `request.regs.ext.gdpr` and `request.user.ext.consent` -- To support GDPR
+- `request.regs.us_privacy` -- To support CCPA
 - `request.site.ext.amp` -- To identify AMP as the request source
 - `request.app.ext.source` and `request.app.ext.version` -- To support identifying the displaymanager/SDK in mobile apps. If given, we expect these to be strings.
-- `request.regs.coppa` -- to support COPPA
 
 #### Bid Adjustments
 
@@ -98,7 +139,7 @@ If you find that some bidders use Gross bids, publishers can adjust for it with
   "ext": {
     "prebid": {
       "bidadjustmentfactors": {
-        "appnexus: 0.8,
+        "appnexus": 0.8,
         "rubicon": 0.7
       }
     }
@@ -126,8 +167,8 @@ to set these params on the response at `response.seatbid[i].bid[j].ext.prebid.ta
         "pricegranularity": {
           "precision": 2,
           "ranges": [{
-                "max":20.00,
-                "increment":0.10 // This is equivalent to the deprecated "pricegranularity": "medium"
+            "max": 20.00,
+            "increment": 0.10 // This is equivalent to the deprecated "pricegranularity": "medium"
           }]
         },
         "includewinners": false, // Optional param defaulting to true
@@ -146,23 +187,29 @@ One of "includewinners" or "includebidderkeys" must be true (both default to tru
 MediaType PriceGranularity (PBS-Java only) - when a single OpenRTB request contains multiple impressions with different mediatypes, or a single impression supports multiple formats, the different mediatypes may need different price granularities. If `mediatypepricegranularity` is present, `pricegranularity` would only be used for any mediatypes not specified. 
 
 ```
-            "ext": {
-                "prebid": {
-                    "targeting": {
-                        "mediatypepricegranularity": {
-                            "banner": { "ranges": [
-                                {"max": 20, "increment": 0.5}
-                            ]},
-                            "video": { "ranges": [
-                                {"max": 10, "increment": 1},
-                                {"max": 20, "increment": 2},
-                                {"max": 50, "increment": 5}
-                            ]}
-                        }
-                    }
-                    "includewinners": true
-                }
-             }
+{
+  "ext": {
+    "prebid": {
+      "targeting": {
+        "mediatypepricegranularity": {
+          "banner": {
+            "ranges": [
+              {"max": 20, "increment": 0.5}
+            ]
+          },
+          "video": {
+            "ranges": [
+              {"max": 10, "increment": 1},
+              {"max": 20, "increment": 2},
+              {"max": 50, "increment": 5}
+            ]
+          }
+        }
+      },
+      "includewinners": true
+    }
+  }
+}
 ```
 
 **Response format** (returned in `bid.ext.prebid.targeting`)
@@ -238,22 +285,20 @@ This can be used to request bids from the same Bidder with different params. For
 
 ```
 {
-  "imp": [
-    {
-      "id": "some-impression-id",
-      "video": {
-        "mimes": ["video/mp4"]
+  "imp": [{
+    "id": "some-impression-id",
+    "video": {
+      "mimes": ["video/mp4"]
+    },
+    "ext": {
+      "appnexus": {
+        "placementId": 123
       },
-      "ext": {
-        "appnexus: {
-          "placementId": 123
-        },
-        "districtm": {
-          "placementId": 456
-        }
+      "districtm": {
+        "placementId": 456
       }
     }
-  ],
+  }],
   "ext": {
     "prebid": {
       "aliases": {
@@ -303,12 +348,12 @@ For example, a request may return this in `response.ext`
   "ext": {
     "errors": {
       "appnexus": [{
-          "code": 2,
-          "message": "A hybrid Banner/Audio Imp was offered, but Appnexus doesn't support Audio."
+        "code": 2,
+        "message": "A hybrid Banner/Audio Imp was offered, but Appnexus doesn't support Audio."
       }],
       "rubicon": [{
-          "code": 1,
-          "message": "The request exceeded the timeout allocated"
+        "code": 1,
+        "message": "The request exceeded the timeout allocated"
       }]
     }
   }
@@ -413,16 +458,14 @@ The values will be numbers that indicate the minimum allowed size for the ad, as
 Example:
 ```
 {
-  "imp": [
-    {
-      ...
-      "banner": {
-        ...
-      }
-      "instl": 1,
+  "imp": [{
+    ...
+    "banner": {
       ...
     }
-  ]
+    "instl": 1,
+    ...
+  }]
   "device": {
     ...
     "h": 640,
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index bcb13724..bd50fca9 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -315,7 +315,12 @@ func (deps *endpointDeps) validateRequest(req *openrtb.BidRequest) []error {
 	}
 
 	if err := ccpaPolicy.Validate(); err != nil {
-		errL = append(errL, &errortypes.Warning{Message: fmt.Sprintf("CCPA value is invalid and will be ignored. (%s)", err.Error())})
+		errL = append(errL, &errortypes.InvalidPrivacyConsent{Message: fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err)})
+
+		ccpaPolicy.Value = ""
+		if err := ccpaPolicy.Write(req); err != nil {
+			errL = append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err))
+		}
 	}
 
 	impIDs := make(map[string]int, len(req.Imp))
diff --git a/endpoints/openrtb2/sample-requests/video/video_invalid_sample.json b/endpoints/openrtb2/sample-requests/video/video_invalid_sample.json
index 0a9fe656..d62f4043 100644
--- a/endpoints/openrtb2/sample-requests/video/video_invalid_sample.json
+++ b/endpoints/openrtb2/sample-requests/video/video_invalid_sample.json
@@ -1,68 +1,69 @@
 {
-  "description": "Video endpoint valid request.",
+  "description": "Video endpoint valid request due to missing pods.",
 
-  "requestPayload":
-{
-  "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
-  "accountid": "555888777",
-
-  "site": {
-    "page": "prebid.com"
-  },
-  "user": {
-    "buyeruids": {
-      "appnexus": "unique_id_an",
-      "rubicon": "unique_id_rubi"
+  "requestPayload": {
+    "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
+    "site": {
+      "page": "prebid.com"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "yob": 1991,
+      "gender": "F",
+      "keywords": "Hotels, Travelling",
+      "ext": {
+        "prebid": {
+          "buyeruids": {
+            "appnexus": "unique_id_an",
+            "rubicon": "unique_id_rubi"
+          }
+        }
+      }
+    },
+    "device": {
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
+      "lmt": 44,
+      "os": "mac os",
+      "w": 640,
+      "h": 480,
+      "didsha1": "didsha1",
+      "didmd5": "didmd5",
+      "dpidsha1": "dpidsha1",
+      "dpidmd5": "dpidmd5",
+      "macsha1": "macsha1",
+      "macmd5": "macmd5"
+    },
+    "includebrandcategory": {
+      "primaryadserver": 1,
+      "publisher": ""
+    },
+    "video": {
+      "w": 640,
+      "h": 480,
+      "mimes": [
+        "video/mp4"
+      ],
+      "protocols": [
+        2, 3, 5, 6
+      ]
     },
-    "gdpr": {
-      "consentrequired": false,
-      "consentstring": "something"
+    "content": {
+      "episode": 6,
+      "title": "episodeName",
+      "series": "TvName",
+      "season": "season3",
+      "len": 900,
+      "livestream": 0
     },
-    "yob": 1991,
-    "gender": "F",
-    "keywords": "Hotels, Travelling"
-  },
-  "device11": {
-    "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
-    "ip": "123.145.167.10",
-    "devicetype": 1,
-    "dnt": 33,
-    "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
-    "lmt": 44,
-    "os": "mac os",
-    "w": 640,
-    "h": 480,
-    "didsha1": "didsha1",
-    "didmd5": "didmd5",
-    "dpidsha1": "dpidsha1",
-    "dpidmd5": "dpidmd5",
-    "macsha1": "macsha1",
-    "macmd5": "macmd5"
-  },
-  "includebrandcategory":{
-    "primaryadserver": 1,
-    "publisher": ""
-  },
-  "video": {
-    "w": 640,
-    "h": 480,
-    "mimes": [
-      "video/mp4"
-    ],
-    "protocols": [
-      2,3,5,6
-    ]
-  },
-  "content": {
-    "episode": 6,
-    "title": "episodeName",
-    "series": "TvName",
-    "season": "season3",
-    "len": 900,
-    "livestream": 0
-  },
-  "cacheconfig": {
-    "ttl": 42
+    "cacheconfig": {
+      "ttl": 42
+    }
   }
-}
 }
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample.json
index caa16f52..7ccdbf83 100644
--- a/endpoints/openrtb2/sample-requests/video/video_valid_sample.json
+++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample.json
@@ -1,85 +1,86 @@
 {
   "description": "Video endpoint valid request.",
 
-  "requestPayload":
-{
-  "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
-  "accountid": "555888777",
-  "podconfig": {
-    "durationrangesec": [
-      30
-    ],
-    "requireexactduration": true,
-    "pods": [
-      {
-        "podid": 1,
-        "adpoddurationsec": 180,
-        "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
-      },
-      {
-        "podid": 2,
-        "adpoddurationsec": 150,
-        "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+  "requestPayload": {
+    "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
+    "podconfig": {
+      "durationrangesec": [
+        30
+      ],
+      "requireexactduration": true,
+      "pods": [{
+          "podid": 1,
+          "adpoddurationsec": 180,
+          "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
+        },
+        {
+          "podid": 2,
+          "adpoddurationsec": 150,
+          "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+        }
+      ]
+    },
+    "site": {
+      "page": "prebid.com"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "yob": 1991,
+      "gender": "F",
+      "keywords": "Hotels, Travelling",
+      "ext": {
+        "prebid": {
+          "buyeruids": {
+            "appnexus": "unique_id_an",
+            "rubicon": "unique_id_rubi"
+          }
+        }
       }
-    ]
-  },
-  "site": {
-    "page": "prebid.com"
-  },
-  "user": {
-    "buyeruids": {
-      "appnexus": "unique_id_an",
-      "rubicon": "unique_id_rubi"
     },
-    "gdpr": {
-      "consentrequired": false,
-      "consentstring": "something"
+    "device": {
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
+      "lmt": 44,
+      "os": "mac os",
+      "w": 640,
+      "h": 480,
+      "didsha1": "didsha1",
+      "didmd5": "didmd5",
+      "dpidsha1": "dpidsha1",
+      "dpidmd5": "dpidmd5",
+      "macsha1": "macsha1",
+      "macmd5": "macmd5"
+    },
+    "includebrandcategory": {
+      "primaryadserver": 1,
+      "publisher": ""
+    },
+    "video": {
+      "w": 640,
+      "h": 480,
+      "mimes": [
+        "video/mp4"
+      ],
+      "protocols": [
+        2, 3, 5, 6
+      ]
+    },
+    "content": {
+      "episode": 6,
+      "title": "episodeName",
+      "series": "TvName",
+      "season": "season3",
+      "len": 900,
+      "livestream": 0
     },
-    "yob": 1991,
-    "gender": "F",
-    "keywords": "Hotels, Travelling"
-  },
-  "device11": {
-    "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
-    "ip": "123.145.167.10",
-    "devicetype": 1,
-    "dnt": 33,
-    "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
-    "lmt": 44,
-    "os": "mac os",
-    "w": 640,
-    "h": 480,
-    "didsha1": "didsha1",
-    "didmd5": "didmd5",
-    "dpidsha1": "dpidsha1",
-    "dpidmd5": "dpidmd5",
-    "macsha1": "macsha1",
-    "macmd5": "macmd5"
-  },
-  "includebrandcategory":{
-    "primaryadserver": 1,
-    "publisher": ""
-  },
-  "video": {
-    "w": 640,
-    "h": 480,
-    "mimes": [
-      "video/mp4"
-    ],
-    "protocols": [
-      2,3,5,6
-    ]
-  },
-  "content": {
-    "episode": 6,
-    "title": "episodeName",
-    "series": "TvName",
-    "season": "season3",
-    "len": 900,
-    "livestream": 0
-  },
-  "cacheconfig": {
-    "ttl": 42
+    "cacheconfig": {
+      "ttl": 42
+    }
   }
-}
 }
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_malformed.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_malformed.json
new file mode 100644
index 00000000..b512c683
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_malformed.json
@@ -0,0 +1,88 @@
+{
+  "description": "Video endpoint valid request.",
+
+  "requestPayload": {
+    "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
+    "podconfig": {
+      "durationrangesec": [
+        30
+      ],
+      "requireexactduration": true,
+      "pods": [{
+          "podid": 1,
+          "adpoddurationsec": 180,
+          "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
+        },
+        {
+          "podid": 2,
+          "adpoddurationsec": 150,
+          "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+        }
+      ]
+    },
+    "site": {
+      "page": "prebid.com"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0,
+        "us_privacy": "${malformed}"
+      }
+    },
+    "user": {
+      "buyeruid": "anyId",
+      "yob": 1991,
+      "gender": "F",
+      "keywords": "Hotels, Travelling",
+      "ext": {
+        "prebid": {
+          "buyeruids": {
+            "appnexus": "unique_id_an",
+            "rubicon": "unique_id_rubi"
+          }
+        }
+      }
+    },
+    "device": {
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
+      "lmt": 44,
+      "os": "mac os",
+      "w": 640,
+      "h": 480,
+      "didsha1": "didsha1",
+      "didmd5": "didmd5",
+      "dpidsha1": "dpidsha1",
+      "dpidmd5": "dpidmd5",
+      "macsha1": "macsha1",
+      "macmd5": "macmd5"
+    },
+    "includebrandcategory": {
+      "primaryadserver": 1,
+      "publisher": ""
+    },
+    "video": {
+      "w": 640,
+      "h": 480,
+      "mimes": [
+        "video/mp4"
+      ],
+      "protocols": [
+        2, 3, 5, 6
+      ]
+    },
+    "content": {
+      "episode": 6,
+      "title": "episodeName",
+      "series": "TvName",
+      "season": "season3",
+      "len": 900,
+      "livestream": 0
+    },
+    "cacheconfig": {
+      "ttl": 42
+    }
+  }
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_valid.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_valid.json
new file mode 100644
index 00000000..cfa389d4
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_ccpa_valid.json
@@ -0,0 +1,88 @@
+{
+  "description": "Video endpoint valid request.",
+
+  "requestPayload": {
+    "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
+    "podconfig": {
+      "durationrangesec": [
+        30
+      ],
+      "requireexactduration": true,
+      "pods": [{
+          "podid": 1,
+          "adpoddurationsec": 180,
+          "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
+        },
+        {
+          "podid": 2,
+          "adpoddurationsec": 150,
+          "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+        }
+      ]
+    },
+    "site": {
+      "page": "prebid.com"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0,
+        "us_privacy": "1NYN"
+      }
+    },
+    "user": {
+      "buyeruid": "anyId",
+      "yob": 1991,
+      "gender": "F",
+      "keywords": "Hotels, Travelling",
+      "ext": {
+        "prebid": {
+          "buyeruids": {
+            "appnexus": "unique_id_an",
+            "rubicon": "unique_id_rubi"
+          }
+        }
+      }
+    },
+    "device": {
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
+      "lmt": 44,
+      "os": "mac os",
+      "w": 640,
+      "h": 480,
+      "didsha1": "didsha1",
+      "didmd5": "didmd5",
+      "dpidsha1": "dpidsha1",
+      "dpidmd5": "dpidmd5",
+      "macsha1": "macsha1",
+      "macmd5": "macmd5"
+    },
+    "includebrandcategory": {
+      "primaryadserver": 1,
+      "publisher": ""
+    },
+    "video": {
+      "w": 640,
+      "h": 480,
+      "mimes": [
+        "video/mp4"
+      ],
+      "protocols": [
+        2, 3, 5, 6
+      ]
+    },
+    "content": {
+      "episode": 6,
+      "title": "episodeName",
+      "series": "TvName",
+      "season": "season3",
+      "len": 900,
+      "livestream": 0
+    },
+    "cacheconfig": {
+      "ttl": 42
+    }
+  }
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_different_durations.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_different_durations.json
index 504af2d6..c3ad7769 100644
--- a/endpoints/openrtb2/sample-requests/video/video_valid_sample_different_durations.json
+++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_different_durations.json
@@ -1,86 +1,87 @@
 {
-  "description": "Video endpoint valid request.",
+  "description": "Video endpoint valid request with different durations.",
 
-  "requestPayload":
-{
-  "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
-  "accountid": "555888777",
-  "podconfig": {
-    "durationrangesec": [
-      15,
-      30
-    ],
-    "requireexactduration": true,
-    "pods": [
-      {
-        "podid": 1,
-        "adpoddurationsec": 180,
-        "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
-      },
-      {
-        "podid": 2,
-        "adpoddurationsec": 150,
-        "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
+  "requestPayload": {
+    "storedrequestid": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
+    "podconfig": {
+      "durationrangesec": [
+        15,
+        30
+      ],
+      "requireexactduration": true,
+      "pods": [{
+          "podid": 1,
+          "adpoddurationsec": 180,
+          "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+        },
+        {
+          "podid": 2,
+          "adpoddurationsec": 150,
+          "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
+        }
+      ]
+    },
+    "site": {
+      "page": "prebid.com"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
+      }
+    },
+    "user": {
+      "yob": 1991,
+      "gender": "F",
+      "keywords": "Hotels, Travelling",
+      "ext": {
+        "prebid": {
+          "buyeruids": {
+            "appnexus": "unique_id_an",
+            "rubicon": "unique_id_rubi"
+          }
+        }
       }
-    ]
-  },
-  "site": {
-    "page": "prebid.com"
-  },
-  "user": {
-    "buyeruids": {
-      "appnexus": "unique_id_an",
-      "rubicon": "unique_id_rubi"
     },
-    "gdpr": {
-      "consentrequired": false,
-      "consentstring": "something"
+    "device": {
+      "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
+      "lmt": 44,
+      "os": "mac os",
+      "w": 640,
+      "h": 480,
+      "didsha1": "didsha1",
+      "didmd5": "didmd5",
+      "dpidsha1": "dpidsha1",
+      "dpidmd5": "dpidmd5",
+      "macsha1": "macsha1",
+      "macmd5": "macmd5"
+    },
+    "includebrandcategory": {
+      "primaryadserver": 1,
+      "publisher": ""
+    },
+    "video": {
+      "w": 640,
+      "h": 480,
+      "mimes": [
+        "video/mp4"
+      ],
+      "protocols": [
+        2, 3, 5, 6
+      ]
+    },
+    "content": {
+      "episode": 6,
+      "title": "episodeName",
+      "series": "TvName",
+      "season": "season3",
+      "len": 900,
+      "livestream": 0
     },
-    "yob": 1991,
-    "gender": "F",
-    "keywords": "Hotels, Travelling"
-  },
-  "device11": {
-    "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2",
-    "ip": "123.145.167.10",
-    "devicetype": 1,
-    "dnt": 33,
-    "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
-    "lmt": 44,
-    "os": "mac os",
-    "w": 640,
-    "h": 480,
-    "didsha1": "didsha1",
-    "didmd5": "didmd5",
-    "dpidsha1": "dpidsha1",
-    "dpidmd5": "dpidmd5",
-    "macsha1": "macsha1",
-    "macmd5": "macmd5"
-  },
-  "includebrandcategory":{
-    "primaryadserver": 1,
-    "publisher": ""
-  },
-  "video": {
-    "w": 640,
-    "h": 480,
-    "mimes": [
-      "video/mp4"
-    ],
-    "protocols": [
-      2,3,5,6
-    ]
-  },
-  "content": {
-    "episode": 6,
-    "title": "episodeName",
-    "series": "TvName",
-    "season": "season3",
-    "len": 900,
-    "livestream": 0
-  },
-  "cacheconfig": {
-    "ttl": 42
+    "cacheconfig": {
+      "ttl": 42
+    }
   }
-}
 }
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_with_device_user_agent.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_with_device_user_agent.json
index 68c3f4e1..6a9dc605 100644
--- a/endpoints/openrtb2/sample-requests/video/video_valid_sample_with_device_user_agent.json
+++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_with_device_user_agent.json
@@ -1,80 +1,85 @@
-
 {
-  "accountid": "555888777",
-  "podconfig": {
-    "durationrangesec": [
-      30
-    ],
-    "requireexactduration": true,
-    "pods": [
-      {
-        "podid": 1,
-        "adpoddurationsec": 180,
-        "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
-      },
-      {
-        "podid": 2,
-        "adpoddurationsec": 150,
-        "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+  "description": "Video endpoint valid request with device data.",
+
+  "requestPayload": {
+    "podconfig": {
+      "durationrangesec": [
+        30
+      ],
+      "requireexactduration": true,
+      "pods": [{
+          "podid": 1,
+          "adpoddurationsec": 180,
+          "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
+        },
+        {
+          "podid": 2,
+          "adpoddurationsec": 150,
+          "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+        }
+      ]
+    },
+    "site": {
+      "page": "prebid.com"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
       }
-    ]
-  },
-  "site": {
-    "page": "prebid.com"
-  },
-  "user": {
-    "buyeruids": {
-      "appnexus": "unique_id_an",
-      "rubicon": "unique_id_rubi"
     },
-    "gdpr": {
-      "consentrequired": false,
-      "consentstring": "something"
+    "user": {
+      "yob": 1991,
+      "gender": "F",
+      "keywords": "Hotels, Travelling",
+      "ext": {
+        "prebid": {
+          "buyeruids": {
+            "appnexus": "unique_id_an",
+            "rubicon": "unique_id_rubi"
+          }
+        }
+      }
+    },
+    "device": {
+      "ua": "TestHeaderSample",
+      "ip": "123.145.167.10",
+      "devicetype": 1,
+      "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
+      "lmt": 44,
+      "os": "mac os",
+      "w": 640,
+      "h": 480,
+      "didsha1": "didsha1",
+      "didmd5": "didmd5",
+      "dpidsha1": "dpidsha1",
+      "dpidmd5": "dpidmd5",
+      "macsha1": "macsha1",
+      "macmd5": "macmd5"
+    },
+    "includebrandcategory": {
+      "primaryadserver": 1,
+      "publisher": ""
+    },
+    "video": {
+      "w": 640,
+      "h": 480,
+      "mimes": [
+        "video/mp4"
+      ],
+      "protocols": [
+        2, 3, 5, 6
+      ]
+    },
+    "content": {
+      "episode": 6,
+      "title": "episodeName",
+      "series": "TvName",
+      "season": "season3",
+      "len": 900,
+      "livestream": 0
     },
-    "yob": 1991,
-    "gender": "F",
-    "keywords": "Hotels, Travelling"
-  },
-  "device": {
-    "ua": "TestHeaderSample",
-    "ip": "123.145.167.10",
-    "devicetype": 1,
-    "dnt": 33,
-    "ifa": "AA000DFE74168477C70D291f574D344790E0BB11",
-    "lmt": 44,
-    "os": "mac os",
-    "w": 640,
-    "h": 480,
-    "didsha1": "didsha1",
-    "didmd5": "didmd5",
-    "dpidsha1": "dpidsha1",
-    "dpidmd5": "dpidmd5",
-    "macsha1": "macsha1",
-    "macmd5": "macmd5"
-  },
-  "includebrandcategory":{
-    "primaryadserver": 1,
-    "publisher": ""
-  },
-  "video": {
-    "w": 640,
-    "h": 480,
-    "mimes": [
-      "video/mp4"
-    ],
-    "protocols": [
-      2,3,5,6
-    ]
-  },
-  "content": {
-    "episode": 6,
-    "title": "episodeName",
-    "series": "TvName",
-    "season": "season3",
-    "len": 900,
-    "livestream": 0
-  },
-  "cacheconfig": {
-    "ttl": 42
+    "cacheconfig": {
+      "ttl": 42
+    }
   }
-}
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/video/video_valid_sample_without_device_user_agent.json b/endpoints/openrtb2/sample-requests/video/video_valid_sample_without_device_user_agent.json
index e040a562..19939186 100644
--- a/endpoints/openrtb2/sample-requests/video/video_valid_sample_without_device_user_agent.json
+++ b/endpoints/openrtb2/sample-requests/video/video_valid_sample_without_device_user_agent.json
@@ -1,63 +1,69 @@
-
 {
-  "accountid": "555888777",
-  "podconfig": {
-    "durationrangesec": [
-      30
-    ],
-    "requireexactduration": true,
-    "pods": [
-      {
-        "podid": 1,
-        "adpoddurationsec": 180,
-        "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
-      },
-      {
-        "podid": 2,
-        "adpoddurationsec": 150,
-        "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+  "description": "Video endpoint valid request without device data.",
+
+  "requestPayload": {
+    "podconfig": {
+      "durationrangesec": [
+        30
+      ],
+      "requireexactduration": true,
+      "pods": [{
+          "podid": 1,
+          "adpoddurationsec": 180,
+          "configid": "fba10607-0c12-43d1-ad07-b8a513bc75d6"
+        },
+        {
+          "podid": 2,
+          "adpoddurationsec": 150,
+          "configid": "8b452b41-2681-4a20-9086-6f16ffad7773"
+        }
+      ]
+    },
+    "site": {
+      "page": "prebid.com"
+    },
+    "regs": {
+      "ext": {
+        "gdpr": 0
       }
-    ]
-  },
-  "site": {
-    "page": "prebid.com"
-  },
-  "user": {
-    "buyeruids": {
-      "appnexus": "unique_id_an",
-      "rubicon": "unique_id_rubi"
     },
-    "gdpr": {
-      "consentrequired": false,
-      "consentstring": "something"
+    "user": {
+      "yob": 1991,
+      "gender": "F",
+      "keywords": "Hotels, Travelling",
+      "ext": {
+        "prebid": {
+          "buyeruids": {
+            "appnexus": "unique_id_an",
+            "rubicon": "unique_id_rubi"
+          }
+        }
+      }
+    },
+    "includebrandcategory": {
+      "primaryadserver": 1,
+      "publisher": ""
+    },
+    "video": {
+      "w": 640,
+      "h": 480,
+      "mimes": [
+        "video/mp4"
+      ],
+      "protocols": [
+        2, 3, 5, 6
+      ]
+    },
+    "content": {
+      "episode": 6,
+      "title": "episodeName",
+      "series": "TvName",
+      "season": "season3",
+      "len": 900,
+      "livestream": 0
     },
-    "yob": 1991,
-    "gender": "F",
-    "keywords": "Hotels, Travelling"
-  },
-  "includebrandcategory":{
-    "primaryadserver": 1,
-    "publisher": ""
-  },
-  "video": {
-    "w": 640,
-    "h": 480,
-    "mimes": [
-      "video/mp4"
-    ],
-    "protocols": [
-      2,3,5,6
-    ]
-  },
-  "content": {
-    "episode": 6,
-    "title": "episodeName",
-    "series": "TvName",
-    "season": "season3",
-    "len": 900,
-    "livestream": 0
-  },
-  "cacheconfig": {
-    "ttl": 42
+    "cacheconfig": {
+      "ttl": 42
+    }
   }
-}
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 020a5196..64c99fa5 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -204,7 +204,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	deps.setFieldsImplicitly(r, bidReq) // move after merge
 
 	errL = deps.validateRequest(bidReq)
-	if len(errL) > 0 {
+	if errortypes.ContainsFatalError(errL) {
 		handleError(&labels, w, errL, &vo, &debugLog)
 		return
 	}
@@ -232,7 +232,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	}
 
 	if acctIdErr := validateAccount(deps.cfg, labels.PubID); acctIdErr != nil {
-		errL = append(errL, acctIdErr)
+		errL := []error{err}
 		handleError(&labels, w, errL, &vo, &debugLog)
 		return
 	}
diff --git a/exchange/adapter_map.go b/exchange/adapter_map.go
index 39001611..2ea8f7fb 100755
--- a/exchange/adapter_map.go
+++ b/exchange/adapter_map.go
@@ -5,6 +5,8 @@ import (
 	"net/http"
 	"strings"
 
+	"github.com/prebid/prebid-server/pbsmetrics"
+
 	"github.com/prebid/prebid-server/adapters"
 	ttx "github.com/prebid/prebid-server/adapters/33across"
 	"github.com/prebid/prebid-server/adapters/adform"
@@ -16,6 +18,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/adocean"
 	"github.com/prebid/prebid-server/adapters/adoppler"
 	"github.com/prebid/prebid-server/adapters/adpone"
+	"github.com/prebid/prebid-server/adapters/adtarget"
 	"github.com/prebid/prebid-server/adapters/adtelligent"
 	"github.com/prebid/prebid-server/adapters/advangelists"
 	"github.com/prebid/prebid-server/adapters/aja"
@@ -85,7 +88,7 @@ import (
 // The newAdapterMap function is segregated to its own file to make it a simple and clean location for each Adapter
 // to register itself. No wading through Exchange code to find it.
 
-func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapters.BidderInfos) map[openrtb_ext.BidderName]adaptedBidder {
+func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapters.BidderInfos, me pbsmetrics.MetricsEngine) map[openrtb_ext.BidderName]adaptedBidder {
 	ortbBidders := map[openrtb_ext.BidderName]adapters.Bidder{
 		openrtb_ext.Bidder33Across:     ttx.New33AcrossBidder(cfg.Adapters[string(openrtb_ext.Bidder33Across)].Endpoint),
 		openrtb_ext.BidderAdform:       adform.NewAdformBidder(client, cfg.Adapters[string(openrtb_ext.BidderAdform)].Endpoint),
@@ -97,6 +100,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 		openrtb_ext.BidderAdOcean:      adocean.NewAdOceanBidder(client, cfg.Adapters[strings.ToLower(string(openrtb_ext.BidderAdOcean))].Endpoint),
 		openrtb_ext.BidderAdoppler:     adoppler.NewAdopplerBidder(cfg.Adapters[string(openrtb_ext.BidderAdoppler)].Endpoint),
 		openrtb_ext.BidderAdpone:       adpone.NewAdponeBidder(cfg.Adapters[string(openrtb_ext.BidderAdpone)].Endpoint),
+		openrtb_ext.BidderAdtarget:     adtarget.NewAdtargetBidder(cfg.Adapters[string(openrtb_ext.BidderAdtarget)].Endpoint),
 		openrtb_ext.BidderAdtelligent:  adtelligent.NewAdtelligentBidder(cfg.Adapters[string(openrtb_ext.BidderAdtelligent)].Endpoint),
 		openrtb_ext.BidderAdvangelists: advangelists.NewAdvangelistsBidder(cfg.Adapters[string(openrtb_ext.BidderAdvangelists)].Endpoint),
 		openrtb_ext.BidderAJA:          aja.NewAJABidder(cfg.Adapters[string(openrtb_ext.BidderAJA)].Endpoint),
@@ -190,7 +194,7 @@ func newAdapterMap(client *http.Client, cfg *config.Configuration, infos adapter
 	for name, bidder := range ortbBidders {
 		// Clean out any disabled bidders
 		if infos[string(name)].Status == adapters.StatusActive {
-			allBidders[name] = adaptBidder(adapters.EnforceBidderInfo(bidder, infos[string(name)]), client)
+			allBidders[name] = adaptBidder(adapters.EnforceBidderInfo(bidder, infos[string(name)]), client, cfg, me)
 		}
 	}
 
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 7a53db5e..f9b4a522 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -10,13 +10,18 @@ import (
 	"net/http"
 	"time"
 
+	"github.com/golang/glog"
+	"github.com/prebid/prebid-server/config/util"
+
 	"github.com/mxmCherry/openrtb"
 	nativeRequests "github.com/mxmCherry/openrtb/native/request"
 	nativeResponse "github.com/mxmCherry/openrtb/native/response"
 	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currencies"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/pbsmetrics"
 	"golang.org/x/net/context/ctxhttp"
 )
 
@@ -82,16 +87,20 @@ type pbsOrtbSeatBid struct {
 //
 // The name refers to the "Adapter" architecture pattern, and should not be confused with a Prebid "Adapter"
 // (which is being phased out and replaced by Bidder for OpenRTB auctions)
-func adaptBidder(bidder adapters.Bidder, client *http.Client) adaptedBidder {
+func adaptBidder(bidder adapters.Bidder, client *http.Client, cfg *config.Configuration, me pbsmetrics.MetricsEngine) adaptedBidder {
 	return &bidderAdapter{
-		Bidder: bidder,
-		Client: client,
+		Bidder:      bidder,
+		Client:      client,
+		DebugConfig: cfg.Debug,
+		me:          me,
 	}
 }
 
 type bidderAdapter struct {
-	Bidder adapters.Bidder
-	Client *http.Client
+	Bidder      adapters.Bidder
+	Client      *http.Client
+	DebugConfig config.Debug
+	me          pbsmetrics.MetricsEngine
 }
 
 func (bidder *bidderAdapter) requestBid(ctx context.Context, request *openrtb.BidRequest, name openrtb_ext.BidderName, bidAdjustment float64, conversions currencies.Conversions, reqInfo *adapters.ExtraRequestInfo) (*pbsOrtbSeatBid, []error) {
@@ -365,8 +374,21 @@ func (bidder *bidderAdapter) doTimeoutNotification(timeoutBidder adapters.Timeou
 		httpReq, err := http.NewRequest(toReq.Method, toReq.Uri, bytes.NewBuffer(toReq.Body))
 		if err == nil {
 			httpReq.Header = req.Headers
-			ctxhttp.Do(ctx, bidder.Client, httpReq)
-			// No validation yet on sending notifications
+			httpResp, err := ctxhttp.Do(ctx, bidder.Client, httpReq)
+			success := (err == nil && httpResp.StatusCode >= 200 && httpResp.StatusCode < 300)
+			bidder.me.RecordTimeoutNotice(success)
+			if bidder.DebugConfig.TimeoutNotification.Log && !(bidder.DebugConfig.TimeoutNotification.FailOnly && success) {
+				var msg string
+				if err == nil {
+					msg = fmt.Sprintf("TimeoutNotification: status:(%d) body:%s", httpResp.StatusCode, string(toReq.Body))
+				} else {
+					msg = fmt.Sprintf("TimeoutNotification: error:(%s) body:%s", err.Error(), string(toReq.Body))
+				}
+				// If logging is turned on, and logging is not disallowed via FailOnly
+				util.LogRandomSample(msg, glog.Warningf, bidder.DebugConfig.TimeoutNotification.SamplingRate)
+			}
+		} else {
+			bidder.me.RecordTimeoutNotice(false)
 		}
 	}
 
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 6d51b87d..84ae35d6 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -48,7 +48,7 @@ type exchange struct {
 	currencyConverter   *currencies.RateConverter
 	UsersyncIfAmbiguous bool
 	defaultTTLs         config.DefaultTTLs
-	enforceCCPA         bool
+	privacyConfig       config.Privacy
 }
 
 // Container to pass out response ext data from the GetAllBids goroutines back into the main thread
@@ -69,7 +69,7 @@ type bidResponseWrapper struct {
 func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *config.Configuration, metricsEngine pbsmetrics.MetricsEngine, infos adapters.BidderInfos, gDPR gdpr.Permissions, currencyConverter *currencies.RateConverter) Exchange {
 	e := new(exchange)
 
-	e.adapterMap = newAdapterMap(client, cfg, infos)
+	e.adapterMap = newAdapterMap(client, cfg, infos, metricsEngine)
 	e.cache = cache
 	e.cacheTime = time.Duration(cfg.CacheURL.ExpectedTimeMillis) * time.Millisecond
 	e.me = metricsEngine
@@ -77,7 +77,11 @@ func NewExchange(client *http.Client, cache prebid_cache_client.Client, cfg *con
 	e.currencyConverter = currencyConverter
 	e.UsersyncIfAmbiguous = cfg.GDPR.UsersyncIfAmbiguous
 	e.defaultTTLs = cfg.CacheURL.DefaultTTLs
-	e.enforceCCPA = cfg.CCPA.Enforce
+	e.privacyConfig = config.Privacy{
+		CCPA: cfg.CCPA,
+		GDPR: cfg.GDPR,
+		LMT:  cfg.LMT,
+	}
 	return e
 }
 
@@ -100,7 +104,7 @@ func (e *exchange) HoldAuction(ctx context.Context, bidRequest *openrtb.BidReque
 
 	// Slice of BidRequests, each a copy of the original cleaned to only contain bidder data for the named bidder
 	blabels := make(map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels)
-	cleanRequests, aliases, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.enforceCCPA)
+	cleanRequests, aliases, errs := cleanOpenRTBRequests(ctx, bidRequest, usersyncs, blabels, labels, e.gDPR, e.UsersyncIfAmbiguous, e.privacyConfig)
 
 	// List of bidders we have requests for.
 	liveAdapters := listBiddersWithRequests(cleanRequests)
diff --git a/exchange/utils.go b/exchange/utils.go
index f602d1e8..54122d13 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -8,11 +8,13 @@ import (
 
 	"github.com/buger/jsonparser"
 	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/prebid/prebid-server/privacy"
 	"github.com/prebid/prebid-server/privacy/ccpa"
+	"github.com/prebid/prebid-server/privacy/lmt"
 )
 
 // cleanOpenRTBRequests splits the input request into requests which are sanitized for each bidder. Intended behavior is:
@@ -26,8 +28,8 @@ func cleanOpenRTBRequests(ctx context.Context,
 	blables map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels,
 	labels pbsmetrics.Labels,
 	gDPR gdpr.Permissions,
-	usersyncIfAmbiguous,
-	enforceCCPA bool) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) {
+	usersyncIfAmbiguous bool,
+	privacyConfig config.Privacy) (requestsByBidder map[openrtb_ext.BidderName]*openrtb.BidRequest, aliases map[string]string, errs []error) {
 
 	impsByBidder, errs := splitImps(orig.Imp)
 	if len(errs) > 0 {
@@ -45,15 +47,24 @@ func cleanOpenRTBRequests(ctx context.Context,
 	consent := extractConsent(orig)
 	ampGDPRException := (labels.RType == pbsmetrics.ReqTypeAMP) && gDPR.AMPException()
 
-	privacyEnforcement := privacy.Enforcement{
-		COPPA: orig.Regs != nil && orig.Regs.COPPA == 1,
+	var ccpaPolicy ccpa.Policy
+	if privacyConfig.CCPA.Enforce {
+		ccpaPolicy, _ = ccpa.ReadPolicy(orig)
+	}
+
+	var lmtPolicy lmt.Policy
+	if privacyConfig.LMT.Enforce {
+		lmtPolicy = lmt.ReadPolicy(orig)
 	}
 
-	if enforceCCPA {
-		ccpaPolicy, _ := ccpa.ReadPolicy(orig)
-		privacyEnforcement.CCPA = ccpaPolicy.ShouldEnforce()
+	// request level privacy policies
+	privacyEnforcement := privacy.Enforcement{
+		CCPA:  ccpaPolicy.ShouldEnforce(),
+		COPPA: orig.Regs != nil && orig.Regs.COPPA == 1,
+		LMT:   lmtPolicy.ShouldEnforce(),
 	}
 
+	// bidder level privacy policies
 	for bidder, bidReq := range requestsByBidder {
 
 		if gdpr == 1 {
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index b3ecddb0..659c6616 100755
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -33,6 +33,7 @@ const (
 	BidderAdpone           BidderName = "adpone"
 	BidderAdmixer          BidderName = "admixer"
 	BidderAdOcean          BidderName = "adocean"
+	BidderAdtarget         BidderName = "adtarget"
 	BidderAdtelligent      BidderName = "adtelligent"
 	BidderAdvangelists     BidderName = "advangelists"
 	BidderAJA              BidderName = "aja"
@@ -110,6 +111,7 @@ var BidderMap = map[string]BidderName{
 	"admixer":           BidderAdmixer,
 	"adocean":           BidderAdOcean,
 	"adpone":            BidderAdpone,
+	"adtarget":          BidderAdtarget,
 	"adtelligent":       BidderAdtelligent,
 	"advangelists":      BidderAdvangelists,
 	"aja":               BidderAJA,
diff --git a/openrtb_ext/imp_adtarget.go b/openrtb_ext/imp_adtarget.go
new file mode 100644
index 00000000..a8ac70a1
--- /dev/null
+++ b/openrtb_ext/imp_adtarget.go
@@ -0,0 +1,9 @@
+package openrtb_ext
+
+// ExtImpAdtarget defines the contract for bidrequest.imp[i].ext.adtarget
+type ExtImpAdtarget struct {
+	SourceId    int     `json:"aid"`
+	PlacementId int     `json:"placementId,omitempty"`
+	SiteId      int     `json:"siteId,omitempty"`
+	BidFloor    float64 `json:"bidFloor,omitempty"`
+}
diff --git a/pbsmetrics/config/metrics.go b/pbsmetrics/config/metrics.go
index e1cdaceb..4e249785 100644
--- a/pbsmetrics/config/metrics.go
+++ b/pbsmetrics/config/metrics.go
@@ -188,6 +188,13 @@ func (me *MultiMetricsEngine) RecordRequestQueueTime(success bool, requestType p
 	}
 }
 
+// RecordTimeoutNotice across all engines
+func (me *MultiMetricsEngine) RecordTimeoutNotice(success bool) {
+	for _, thisME := range *me {
+		thisME.RecordTimeoutNotice(success)
+	}
+}
+
 // DummyMetricsEngine is a Noop metrics engine in case no metrics are configured. (may also be useful for tests)
 type DummyMetricsEngine struct{}
 
@@ -262,3 +269,7 @@ func (me *DummyMetricsEngine) RecordPrebidCacheRequestTime(success bool, length
 // RecordRequestQueueTime as a noop
 func (me *DummyMetricsEngine) RecordRequestQueueTime(success bool, requestType pbsmetrics.RequestType, length time.Duration) {
 }
+
+// RecordTimeoutNotice as a noop
+func (me *DummyMetricsEngine) RecordTimeoutNotice(success bool) {
+}
diff --git a/pbsmetrics/go_metrics.go b/pbsmetrics/go_metrics.go
index ff3d9681..1ced4d57 100644
--- a/pbsmetrics/go_metrics.go
+++ b/pbsmetrics/go_metrics.go
@@ -48,6 +48,9 @@ type Metrics struct {
 	ImpsTypeAudio  metrics.Meter
 	ImpsTypeNative metrics.Meter
 
+	TimeoutNotificationSuccess metrics.Meter
+	TimeoutNotificationFailure metrics.Meter
+
 	AdapterMetrics map[openrtb_ext.BidderName]*AdapterMetrics
 	// Don't export accountMetrics because we need helper functions here to insure its properly populated dynamically
 	accountMetrics        map[string]*accountMetrics
@@ -131,6 +134,9 @@ func NewBlankMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderNa
 		ImpsTypeAudio:  blankMeter,
 		ImpsTypeNative: blankMeter,
 
+		TimeoutNotificationSuccess: blankMeter,
+		TimeoutNotificationFailure: blankMeter,
+
 		AdapterMetrics:  make(map[openrtb_ext.BidderName]*AdapterMetrics, len(exchanges)),
 		accountMetrics:  make(map[string]*accountMetrics),
 		MetricsDisabled: disableMetrics,
@@ -209,6 +215,9 @@ func NewMetrics(registry metrics.Registry, exchanges []openrtb_ext.BidderName, d
 
 	newMetrics.userSyncSet[unknownBidder] = metrics.GetOrRegisterMeter("usersync.unknown.sets", registry)
 	newMetrics.userSyncGDPRPrevent[unknownBidder] = metrics.GetOrRegisterMeter("usersync.unknown.gdpr_prevent", registry)
+
+	newMetrics.TimeoutNotificationSuccess = metrics.GetOrRegisterMeter("timeout_notification.ok", registry)
+	newMetrics.TimeoutNotificationFailure = metrics.GetOrRegisterMeter("timeout_notification.failed", registry)
 	return newMetrics
 }
 
@@ -544,6 +553,15 @@ func (me *Metrics) RecordRequestQueueTime(success bool, requestType RequestType,
 
 }
 
+func (me *Metrics) RecordTimeoutNotice(success bool) {
+	if success {
+		me.TimeoutNotificationSuccess.Mark(1)
+	} else {
+		me.TimeoutNotificationFailure.Mark(1)
+	}
+	return
+}
+
 func doMark(bidder openrtb_ext.BidderName, meters map[openrtb_ext.BidderName]metrics.Meter) {
 	met, ok := meters[bidder]
 	if ok {
diff --git a/pbsmetrics/metrics.go b/pbsmetrics/metrics.go
index 611692c9..e65ba313 100644
--- a/pbsmetrics/metrics.go
+++ b/pbsmetrics/metrics.go
@@ -275,4 +275,5 @@ type MetricsEngine interface {
 	RecordStoredImpCacheResult(cacheResult CacheResult, inc int)
 	RecordPrebidCacheRequestTime(success bool, length time.Duration)
 	RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration)
+	RecordTimeoutNotice(sucess bool)
 }
diff --git a/pbsmetrics/metrics_mock.go b/pbsmetrics/metrics_mock.go
index 1f5b84b1..482cbf24 100644
--- a/pbsmetrics/metrics_mock.go
+++ b/pbsmetrics/metrics_mock.go
@@ -101,3 +101,8 @@ func (me *MetricsEngineMock) RecordPrebidCacheRequestTime(success bool, length t
 func (me *MetricsEngineMock) RecordRequestQueueTime(success bool, requestType RequestType, length time.Duration) {
 	me.Called(success, requestType, length)
 }
+
+// RecordTimeoutNotice mock
+func (me *MetricsEngineMock) RecordTimeoutNotice(success bool) {
+	me.Called(success)
+}
diff --git a/pbsmetrics/prometheus/prometheus.go b/pbsmetrics/prometheus/prometheus.go
index d66defea..e385b044 100644
--- a/pbsmetrics/prometheus/prometheus.go
+++ b/pbsmetrics/prometheus/prometheus.go
@@ -28,6 +28,7 @@ type Metrics struct {
 	requestsWithoutCookie        *prometheus.CounterVec
 	storedImpressionsCacheResult *prometheus.CounterVec
 	storedRequestCacheResult     *prometheus.CounterVec
+	timeout_notifications        *prometheus.CounterVec
 
 	// Adapter Metrics
 	adapterBids          *prometheus.CounterVec
@@ -79,6 +80,11 @@ const (
 	requestRejectLabel  = "requestRejectedLabel"
 )
 
+const (
+	requestSuccessful = "ok"
+	requestFailed     = "failed"
+)
+
 // NewMetrics initializes a new Prometheus metrics instance with preloaded label values.
 func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 	requestTimeBuckets := []float64{0.05, 0.1, 0.15, 0.20, 0.25, 0.3, 0.4, 0.5, 0.75, 1}
@@ -147,6 +153,11 @@ func NewMetrics(cfg config.PrometheusMetrics) *Metrics {
 		"Count of stored request cache requests attempts by hits or miss.",
 		[]string{cacheResultLabel})
 
+	metrics.timeout_notifications = newCounter(cfg, metrics.Registry,
+		"timeout_notification",
+		"Count of timeout notifications triggered, and if they were successfully sent.",
+		[]string{successLabel})
+
 	metrics.adapterBids = newCounter(cfg, metrics.Registry,
 		"adapter_bids",
 		"Count of bids labeled by adapter and markup delivery type (adm or nurl).",
@@ -398,3 +409,15 @@ func (m *Metrics) RecordRequestQueueTime(success bool, requestType pbsmetrics.Re
 		requestStatusLabel: successLabelFormatted,
 	}).Observe(length.Seconds())
 }
+
+func (m *Metrics) RecordTimeoutNotice(success bool) {
+	if success {
+		m.timeout_notifications.With(prometheus.Labels{
+			successLabel: requestSuccessful,
+		}).Inc()
+	} else {
+		m.timeout_notifications.With(prometheus.Labels{
+			successLabel: requestFailed,
+		}).Inc()
+	}
+}
diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go
index 64579f2a..11ac4345 100644
--- a/privacy/ccpa/policy.go
+++ b/privacy/ccpa/policy.go
@@ -14,7 +14,7 @@ type Policy struct {
 	Value string
 }
 
-// ReadPolicy extracts the CCPA regulation policy from an OpenRTB regs ext.
+// ReadPolicy extracts the CCPA regulation policy from an OpenRTB request.
 func ReadPolicy(req *openrtb.BidRequest) (Policy, error) {
 	policy := Policy{}
 
@@ -32,6 +32,10 @@ func ReadPolicy(req *openrtb.BidRequest) (Policy, error) {
 // Write mutates an OpenRTB bid request with the context of the CCPA policy.
 func (p Policy) Write(req *openrtb.BidRequest) error {
 	if p.Value == "" {
+		return clearPolicy(req)
+	}
+
+	if req == nil {
 		return nil
 	}
 
@@ -59,6 +63,37 @@ func (p Policy) Write(req *openrtb.BidRequest) error {
 	return err
 }
 
+func clearPolicy(req *openrtb.BidRequest) error {
+	if req == nil {
+		return nil
+	}
+
+	if req.Regs == nil {
+		return nil
+	}
+
+	if len(req.Regs.Ext) == 0 {
+		return nil
+	}
+
+	var extMap map[string]interface{}
+	err := json.Unmarshal(req.Regs.Ext, &extMap)
+	if err == nil {
+		delete(extMap, "us_privacy")
+		if len(extMap) == 0 {
+			req.Regs.Ext = nil
+		} else {
+			ext, err := json.Marshal(extMap)
+			if err == nil {
+				req.Regs.Ext = ext
+			}
+			return err
+		}
+	}
+
+	return err
+}
+
 // Validate returns an error if the CCPA policy does not adhere to the IAB spec.
 func (p Policy) Validate() error {
 	if err := ValidateConsent(p.Value); err != nil {
diff --git a/privacy/enforcement.go b/privacy/enforcement.go
index d302192e..8a5d201f 100644
--- a/privacy/enforcement.go
+++ b/privacy/enforcement.go
@@ -10,11 +10,12 @@ type Enforcement struct {
 	COPPA   bool
 	GDPR    bool
 	GDPRGeo bool
+	LMT     bool
 }
 
 // Any returns true if at least one privacy policy requires enforcement.
 func (e Enforcement) Any() bool {
-	return e.CCPA || e.COPPA || e.GDPR || e.GDPRGeo
+	return e.CCPA || e.COPPA || e.GDPR || e.GDPRGeo || e.LMT
 }
 
 // Apply cleans personally identifiable information from an OpenRTB bid request.
@@ -34,7 +35,7 @@ func (e Enforcement) getIPv6ScrubStrategy() ScrubStrategyIPV6 {
 		return ScrubStrategyIPV6Lowest32
 	}
 
-	if e.GDPR || e.CCPA {
+	if e.GDPR || e.CCPA || e.LMT {
 		return ScrubStrategyIPV6Lowest16
 	}
 
@@ -46,7 +47,7 @@ func (e Enforcement) getGeoScrubStrategy() ScrubStrategyGeo {
 		return ScrubStrategyGeoFull
 	}
 
-	if e.GDPRGeo || e.CCPA {
+	if e.GDPRGeo || e.CCPA || e.LMT {
 		return ScrubStrategyGeoReducedPrecision
 	}
 
@@ -63,7 +64,7 @@ func (e Enforcement) getUserScrubStrategy(ampGDPRException bool) ScrubStrategyUs
 	}
 
 	// If no user scrubbing is needed, then return none, else scrub ID (COPPA checked above)
-	if e.CCPA || e.GDPR {
+	if e.CCPA || e.GDPR || e.LMT {
 		return ScrubStrategyUserID
 	}
 
diff --git a/privacy/lmt/policy.go b/privacy/lmt/policy.go
new file mode 100644
index 00000000..79425bf5
--- /dev/null
+++ b/privacy/lmt/policy.go
@@ -0,0 +1,33 @@
+package lmt
+
+import (
+	"github.com/mxmCherry/openrtb"
+)
+
+const (
+	trackingUnrestricted = 0
+	trackingRestricted   = 1
+)
+
+// Policy represents the LMT (Limit Ad Tracking) policy for an OpenRTB bid request.
+type Policy struct {
+	Signal         int
+	SignalProvided bool
+}
+
+// ReadPolicy extracts the LMT (Limit Ad Tracking) policy from an OpenRTB bid request.
+func ReadPolicy(req *openrtb.BidRequest) Policy {
+	policy := Policy{}
+
+	if req != nil && req.Device != nil && req.Device.Lmt != nil {
+		policy.Signal = int(*req.Device.Lmt)
+		policy.SignalProvided = true
+	}
+
+	return policy
+}
+
+// ShouldEnforce returns true when the LMT (Limit Ad Tracking) policy is in effect.
+func (p Policy) ShouldEnforce() bool {
+	return p.SignalProvided && p.Signal == trackingRestricted
+}
diff --git a/static/bidder-info/adtarget.yaml b/static/bidder-info/adtarget.yaml
new file mode 100644
index 00000000..d52f18ac
--- /dev/null
+++ b/static/bidder-info/adtarget.yaml
@@ -0,0 +1,11 @@
+maintainer:
+  email: "kamil@adtarget.com.tr"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
diff --git a/static/bidder-params/adtarget.json b/static/bidder-params/adtarget.json
new file mode 100644
index 00000000..195bf2dd
--- /dev/null
+++ b/static/bidder-params/adtarget.json
@@ -0,0 +1,26 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Adtarget Adapter Params",
+  "description": "A schema which validates params accepted by the Adtarget adapter",
+
+  "type": "object",
+  "properties": {
+    "placementId": {
+      "type": "integer",
+      "description": "An ID which identifies this placement of the impression"
+    },
+    "siteId": {
+      "type": "integer",
+      "description": "An ID which identifies the site selling the impression"
+    },
+    "aid": {
+      "type": "integer",
+      "description": "An ID which identifies the channel"
+    },
+    "bidFloor": {
+      "type": "number",
+      "description": "BidFloor, US Dollars"
+    }
+  },
+  "required": ["aid"]
+}
diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go
index 5dccf855..751d2aab 100755
--- a/usersync/usersyncers/syncer.go
+++ b/usersync/usersyncers/syncer.go
@@ -12,6 +12,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/admixer"
 	"github.com/prebid/prebid-server/adapters/adocean"
 	"github.com/prebid/prebid-server/adapters/adpone"
+	"github.com/prebid/prebid-server/adapters/adtarget"
 	"github.com/prebid/prebid-server/adapters/adtelligent"
 	"github.com/prebid/prebid-server/adapters/advangelists"
 	"github.com/prebid/prebid-server/adapters/aja"
@@ -84,6 +85,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdmixer, admixer.NewAdmixerSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdOcean, adocean.NewAdOceanSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdpone, adpone.NewadponeSyncer)
+	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdtarget, adtarget.NewAdtargetSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdtelligent, adtelligent.NewAdtelligentSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAdvangelists, advangelists.NewAdvangelistsSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderAJA, aja.NewAJASyncer)


Test patch:
diff --git a/adapters/adtarget/adtarget_test.go b/adapters/adtarget/adtarget_test.go
new file mode 100644
index 00000000..93732988
--- /dev/null
+++ b/adapters/adtarget/adtarget_test.go
@@ -0,0 +1,11 @@
+package adtarget
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+)
+
+func TestJsonSamples(t *testing.T) {
+	adapterstest.RunJSONBidderTest(t, "adtargettest", NewAdtargetBidder("http://ghb.console.adtarget.com.tr/pbs/ortb"))
+}
diff --git a/adapters/adtarget/adtargettest/exemplary/media-type-mapping.json b/adapters/adtarget/adtargettest/exemplary/media-type-mapping.json
new file mode 100644
index 00000000..518268d4
--- /dev/null
+++ b/adapters/adtarget/adtargettest/exemplary/media-type-mapping.json
@@ -0,0 +1,88 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "aid": 1000
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "adtarget": {
+                  "aid": 1000
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test-bid-id",
+                  "impid": "test-imp-id",
+                  "price": 3.5,
+                  "w": 900,
+                  "h": 250
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "test-bid-id",
+            "impid": "test-imp-id",
+            "price": 3.5,
+            "w": 900,
+            "h": 250
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/adtarget/adtargettest/exemplary/simple-banner.json b/adapters/adtarget/adtargettest/exemplary/simple-banner.json
new file mode 100644
index 00000000..b63739bd
--- /dev/null
+++ b/adapters/adtarget/adtargettest/exemplary/simple-banner.json
@@ -0,0 +1,62 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "aid": 1000,
+            "siteId": 1234,
+            "bidFloor": 20
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "banner": {
+                "format": [
+                  {"w":300,"h":250},
+                  {"w":300,"h":600}
+                ]
+              },
+              "bidfloor": 20,
+              "ext": {
+                "adtarget": {
+                  "aid": 1000,
+                  "siteId": 1234,
+                  "bidFloor": 20
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/adtarget/adtargettest/exemplary/simple-video.json b/adapters/adtarget/adtargettest/exemplary/simple-video.json
new file mode 100644
index 00000000..4dc4547d
--- /dev/null
+++ b/adapters/adtarget/adtargettest/exemplary/simple-video.json
@@ -0,0 +1,55 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "aid": 1000
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "adtarget": {
+                  "aid": 1000
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/adtarget/adtargettest/params/race/banner.json b/adapters/adtarget/adtargettest/params/race/banner.json
new file mode 100644
index 00000000..1d6658c7
--- /dev/null
+++ b/adapters/adtarget/adtargettest/params/race/banner.json
@@ -0,0 +1,3 @@
+{
+  "aid": 350975
+}
diff --git a/adapters/adtarget/adtargettest/params/race/video.json b/adapters/adtarget/adtargettest/params/race/video.json
new file mode 100644
index 00000000..fe4207ef
--- /dev/null
+++ b/adapters/adtarget/adtargettest/params/race/video.json
@@ -0,0 +1,3 @@
+{
+  "aid": 331133
+}
diff --git a/adapters/adtarget/adtargettest/supplemental/audio.json b/adapters/adtarget/adtargettest/supplemental/audio.json
new file mode 100644
index 00000000..e2148e9d
--- /dev/null
+++ b/adapters/adtarget/adtargettest/supplemental/audio.json
@@ -0,0 +1,25 @@
+{
+  "mockBidRequest": {
+    "id": "unsupported-audio-request",
+    "imp": [
+      {
+        "id": "unsupported-audio-imp",
+        "audio": {
+          "mimes": ["video/mp4"]
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 1
+          }
+        }
+      }
+    ]
+  },
+
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "ignoring imp id=unsupported-audio-imp, Adtarget supports only Video and Banner",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/adtarget/adtargettest/supplemental/explicit-dimensions.json b/adapters/adtarget/adtargettest/supplemental/explicit-dimensions.json
new file mode 100644
index 00000000..a4e48746
--- /dev/null
+++ b/adapters/adtarget/adtargettest/supplemental/explicit-dimensions.json
@@ -0,0 +1,58 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ],
+          "w": 100,
+          "h": 400
+        },
+        "ext": {
+          "bidder": {
+            "aid": 1000
+          }
+        }
+      }
+    ]
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ],
+                "w": 100,
+                "h": 400
+              },
+              "ext": {
+                "adtarget": {
+                 "aid": 1000
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ]
+}
diff --git a/adapters/adtarget/adtargettest/supplemental/native.json b/adapters/adtarget/adtargettest/supplemental/native.json
new file mode 100644
index 00000000..3d9aa663
--- /dev/null
+++ b/adapters/adtarget/adtargettest/supplemental/native.json
@@ -0,0 +1,25 @@
+{
+  "mockBidRequest": {
+    "id": "unsupported-native-request",
+    "imp": [
+      {
+        "id": "unsupported-native-imp",
+        "native": {
+          "ver": "1.1"
+        },
+        "ext": {
+          "bidder": {
+            "placementId": 1
+          }
+        }
+      }
+    ]
+  },
+
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "ignoring imp id=unsupported-native-imp, Adtarget supports only Video and Banner",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/adtarget/adtargettest/supplemental/wrong-impression-ext.json b/adapters/adtarget/adtargettest/supplemental/wrong-impression-ext.json
new file mode 100644
index 00000000..1986dfaf
--- /dev/null
+++ b/adapters/adtarget/adtargettest/supplemental/wrong-impression-ext.json
@@ -0,0 +1,26 @@
+{
+  "mockBidRequest": {
+    "id": "unsupported-native-request",
+    "imp": [
+      {
+        "id": "unsupported-native-imp",
+        "video": {
+         "w": 100,
+          "h": 200
+        },
+        "ext": {
+          "bidder": {
+            "aid": "some string instead of int"
+          }
+        }
+      }
+    ]
+  },
+
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "ignoring imp id=unsupported-native-imp, error while decoding impExt, err: json: cannot unmarshal string into Go struct field ExtImpAdtarget.aid of type int",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json b/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json
new file mode 100644
index 00000000..0dffdb2b
--- /dev/null
+++ b/adapters/adtarget/adtargettest/supplemental/wrong-impression-mapping.json
@@ -0,0 +1,77 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "aid": 1000
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ghb.console.adtarget.com.tr/pbs/ortb?aid=1000",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "adtarget": {
+                  "aid": 1000
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "test-bid-id",
+                  "impid": "SOME-WRONG-IMP-ID",
+                  "price": 3.5,
+                  "w": 900,
+                  "h": 250
+                }
+              ]
+            }
+          ]
+        }
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "ignoring bid id=test-bid-id, request doesn't contain any impression with id=SOME-WRONG-IMP-ID",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/adtarget/params_test.go b/adapters/adtarget/params_test.go
new file mode 100644
index 00000000..b128d11c
--- /dev/null
+++ b/adapters/adtarget/params_test.go
@@ -0,0 +1,60 @@
+package adtarget
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// This file actually intends to test static/bidder-params/adtarget.json
+// These also validate the format of the external API: request.imp[i].ext.adtarget
+// TestValidParams makes sure that the adtarget schema accepts all imp.ext fields which we intend to support.
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, validParam := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderAdtarget, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected adtarget params: %s", validParam)
+		}
+	}
+}
+
+// TestInvalidParams makes sure that the adtarget schema rejects all the imp.ext fields we don't support.
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, invalidParam := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderAdtarget, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"aid":123}`,
+	`{"aid":123,"placementId":1234}`,
+	`{"aid":123,"siteId":4321}`,
+	`{"aid":123,"siteId":0,"bidFloor":0}`,
+}
+
+var invalidParams = []string{
+	``,
+	`null`,
+	`true`,
+	`5`,
+	`4.2`,
+	`[]`,
+	`{}`,
+	`{"aid":"123"}`,
+	`{"aid":"0"}`,
+	`{"aid":"123","placementId":"123"}`,
+	`{"aid":123, "placementId":"123", "siteId":"321"}`,
+}
diff --git a/adapters/adtarget/usersync_test.go b/adapters/adtarget/usersync_test.go
new file mode 100644
index 00000000..3ab2ed5b
--- /dev/null
+++ b/adapters/adtarget/usersync_test.go
@@ -0,0 +1,37 @@
+package adtarget
+
+import (
+	"fmt"
+	"github.com/prebid/prebid-server/privacy/ccpa"
+	"testing"
+	"text/template"
+
+	"github.com/prebid/prebid-server/privacy"
+	"github.com/prebid/prebid-server/privacy/gdpr"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAdtargetSyncer(t *testing.T) {
+	syncURL := "//sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir=localhost%2Fsetuid%3Fbidder%3Dadtarget%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7Buid%7D"
+	fmt.Println("adtarget sync")
+	syncURLTemplate := template.Must(
+		template.New("sync-template").Parse(syncURL),
+	)
+
+	syncer := NewAdtargetSyncer(syncURLTemplate)
+	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{
+		GDPR: gdpr.Policy{
+			Signal:  "0",
+			Consent: "123",
+		},
+		CCPA: ccpa.Policy{
+			Value: "1-YY",
+		},
+	})
+
+	assert.NoError(t, err)
+	assert.Equal(t, "//sync.console.adtarget.com.tr/csync?t=p&ep=0&gdpr=0&gdpr_consent=123&us_privacy=1-YY&redir=localhost%2Fsetuid%3Fbidder%3Dadtarget%26gdpr%3D0%26gdpr_consent%3D123%26uid%3D%7Buid%7D", syncInfo.URL)
+	assert.Equal(t, "redirect", syncInfo.Type)
+	assert.EqualValues(t, 0, syncer.GDPRVendorID())
+	assert.Equal(t, false, syncInfo.SupportCORS)
+}
diff --git a/adapters/triplelift/triplelift_test.go b/adapters/triplelift/triplelift_test.go
index 2d7ed04f..6fd2b506 100644
--- a/adapters/triplelift/triplelift_test.go
+++ b/adapters/triplelift/triplelift_test.go
@@ -6,5 +6,5 @@ import (
 )
 
 func TestJsonSamples(t *testing.T) {
-	adapterstest.RunJSONBidderTest(t, "triplelifttest", NewTripleliftBidder(nil, "http://tlx.3lift.net/s2s/auction?supplier_id=20"))
+	adapterstest.RunJSONBidderTest(t, "triplelifttest", NewTripleliftBidder(nil, "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20"))
 }
diff --git a/adapters/triplelift/triplelifttest/exemplary/optional-params.json b/adapters/triplelift/triplelifttest/exemplary/optional-params.json
index 0851bc09..90c8da5b 100644
--- a/adapters/triplelift/triplelifttest/exemplary/optional-params.json
+++ b/adapters/triplelift/triplelifttest/exemplary/optional-params.json
@@ -28,7 +28,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20",
+        "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/triplelift/triplelifttest/exemplary/simple-banner.json b/adapters/triplelift/triplelifttest/exemplary/simple-banner.json
index ff680037..156e07e3 100644
--- a/adapters/triplelift/triplelifttest/exemplary/simple-banner.json
+++ b/adapters/triplelift/triplelifttest/exemplary/simple-banner.json
@@ -27,7 +27,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20",
+        "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/triplelift/triplelifttest/exemplary/simple-video.json b/adapters/triplelift/triplelifttest/exemplary/simple-video.json
index 185446bd..846c62b4 100644
--- a/adapters/triplelift/triplelifttest/exemplary/simple-video.json
+++ b/adapters/triplelift/triplelifttest/exemplary/simple-video.json
@@ -33,7 +33,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20",
+        "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20",
         "body": {
           "id": "test-request-id",
           "imp": [
@@ -85,7 +85,7 @@
                   "adomain": [
                     "foo.com"
                   ],
-                  "iurl": "http://tlx.3lift.net/s2s/auction?supplier_id=20",
+                  "iurl": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20",
                   "cid": "958",
                   "crid": "29681110",
                   "h": 250,
@@ -122,7 +122,7 @@
             "adomain": [
               "foo.com"
             ],
-            "iurl": "http://tlx.3lift.net/s2s/auction?supplier_id=20",
+            "iurl": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20",
             "cid": "958",
             "crid": "29681110",
             "w": 300,
diff --git a/adapters/triplelift/triplelifttest/supplemental/badresponseext.json b/adapters/triplelift/triplelifttest/supplemental/badresponseext.json
index 324c0582..6c09448f 100644
--- a/adapters/triplelift/triplelifttest/supplemental/badresponseext.json
+++ b/adapters/triplelift/triplelifttest/supplemental/badresponseext.json
@@ -27,7 +27,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20",
+        "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/triplelift/triplelifttest/supplemental/badstatuscode.json b/adapters/triplelift/triplelifttest/supplemental/badstatuscode.json
index 15799616..f24eb799 100644
--- a/adapters/triplelift/triplelifttest/supplemental/badstatuscode.json
+++ b/adapters/triplelift/triplelifttest/supplemental/badstatuscode.json
@@ -27,7 +27,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20",
+        "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/adapters/triplelift/triplelifttest/supplemental/notgoodstatuscode.json b/adapters/triplelift/triplelifttest/supplemental/notgoodstatuscode.json
index 963db593..bdcc0e3a 100644
--- a/adapters/triplelift/triplelifttest/supplemental/notgoodstatuscode.json
+++ b/adapters/triplelift/triplelifttest/supplemental/notgoodstatuscode.json
@@ -27,7 +27,7 @@
   "httpCalls": [
     {
       "expectedRequest": {
-        "uri": "http://tlx.3lift.net/s2s/auction?supplier_id=20",
+        "uri": "http://tlx.3lift.net/s2s/auction?sra=1&supplier_id=20",
         "body": {
           "id": "test-request-id",
           "imp": [
diff --git a/config/config_test.go b/config/config_test.go
index 92794d79..2b291fe9 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -43,6 +43,8 @@ gdpr:
   non_standard_publishers: ["siteID","fake-site-id","appID","agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA"]
 ccpa:
   enforce: true
+lmt:
+  enforce: true
 host_cookie:
   cookie_name: userid
   family: prebid
@@ -240,6 +242,7 @@ func TestFullConfig(t *testing.T) {
 	cmpBools(t, "cfg.GDPR.NonStandardPublisherMap", found, false)
 
 	cmpBools(t, "ccpa.enforce", cfg.CCPA.Enforce, true)
+	cmpBools(t, "lmt.enforce", cfg.LMT.Enforce, true)
 
 	//Assert the NonStandardPublishers was correctly unmarshalled
 	cmpStrings(t, "blacklisted_apps", cfg.BlacklistedApps[0], "spamAppID")
@@ -426,6 +429,14 @@ func TestCookieSizeError(t *testing.T) {
 	}
 }
 
+func TestValidateDebug(t *testing.T) {
+	cfg := newDefaultConfig(t)
+	cfg.Debug.TimeoutNotification.SamplingRate = 1.1
+
+	err := cfg.validate()
+	assert.NotNil(t, err, "cfg.debug.timeout_notification.sampling_rate should not be allowed to be greater than 1.0, but it was allowed")
+}
+
 func newDefaultConfig(t *testing.T) *Configuration {
 	v := viper.New()
 	SetupViper(v, "")
diff --git a/config/util/loggers_test.go b/config/util/loggers_test.go
new file mode 100644
index 00000000..4bfab967
--- /dev/null
+++ b/config/util/loggers_test.go
@@ -0,0 +1,32 @@
+package util
+
+import (
+	"bytes"
+	"fmt"
+	"math/rand"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLogRandomSample(t *testing.T) {
+
+	const expected string = `This is test line 2
+This is test line 3
+`
+
+	myRand := rand.New(rand.NewSource(1337))
+	var buf bytes.Buffer
+
+	mylogger := func(msg string, args ...interface{}) {
+		buf.WriteString(fmt.Sprintf(fmt.Sprintln(msg), args...))
+	}
+
+	logRandomSampleImpl("This is test line 1", mylogger, 0.5, myRand.Float32)
+	logRandomSampleImpl("This is test line 2", mylogger, 0.5, myRand.Float32)
+	logRandomSampleImpl("This is test line 3", mylogger, 0.5, myRand.Float32)
+	logRandomSampleImpl("This is test line 4", mylogger, 0.5, myRand.Float32)
+	logRandomSampleImpl("This is test line 5", mylogger, 0.5, myRand.Float32)
+
+	assert.EqualValues(t, expected, buf.String())
+}
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 9dc81eb1..289db3f4 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -122,9 +122,8 @@ func TestAMPPageInfo(t *testing.T) {
 }
 
 func TestGDPRConsent(t *testing.T) {
-	consent := "BONV8oqONXwgmADACHENAO7pqzAAppY"
+	consent := "BOu5On0Ou5On0ADACHENAO7pqzAAppY"
 	existingConsent := "BONV8oqONXwgmADACHENAO7pqzAAppY"
-
 	digitrust := &openrtb_ext.ExtUserDigiTrust{
 		ID:   "anyDigitrustID",
 		KeyV: 1,
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index fdd6b3a4..c3b9267b 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -915,7 +915,7 @@ func TestCurrencyTrunc(t *testing.T) {
 	assert.ElementsMatch(t, errL, []error{&expectedError})
 }
 
-func TestCCPAInvalidValueWarning(t *testing.T) {
+func TestCCPAInvalid(t *testing.T) {
 	deps := &endpointDeps{
 		&nobidExchange{},
 		newParamsValidator(t),
@@ -943,21 +943,23 @@ func TestCCPAInvalidValueWarning(t *testing.T) {
 					W: &ui,
 					H: &ui,
 				},
-				Ext: json.RawMessage("{\"appnexus\": {\"placementId\": 5667}}"),
+				Ext: json.RawMessage(`{"appnexus": {"placementId": 5667}}`),
 			},
 		},
 		Site: &openrtb.Site{
 			ID: "myID",
 		},
 		Regs: &openrtb.Regs{
-			Ext: json.RawMessage("{\"us_privacy\":\"invalid by length\"}"),
+			Ext: json.RawMessage(`{"us_privacy":"invalid by length"}`),
 		},
 	}
 
 	errL := deps.validateRequest(&req)
 
-	expectedError := errortypes.Warning{Message: "CCPA value is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)"}
-	assert.ElementsMatch(t, errL, []error{&expectedError})
+	expectedWarning := errortypes.InvalidPrivacyConsent{Message: "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)"}
+	assert.ElementsMatch(t, errL, []error{&expectedWarning})
+
+	assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request")
 }
 
 // nobidExchange is a well-behaved exchange which always bids "no bid".
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index 5ba34068..631cb277 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -1,6 +1,7 @@
 package openrtb2
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"errors"
@@ -860,12 +861,12 @@ func TestParseVideoRequestWithUserAgentAndHeader(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Failed to fetch a valid request: %v", err)
 	}
-
 	headers := http.Header{}
 	headers.Add("User-Agent", "TestHeader")
 
 	deps := mockDeps(t, ex)
-	req, valErr, podErr := deps.parseVideoRequest(reqData, headers)
+	reqBody := string(getRequestPayload(t, reqData))
+	req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers)
 
 	assert.Equal(t, "TestHeaderSample", req.Device.UA, "Header should be taken from original request")
 	assert.Equal(t, []error(nil), valErr, "No validation errors should be returned")
@@ -883,7 +884,8 @@ func TestParseVideoRequestWithUserAgentAndEmptyHeader(t *testing.T) {
 	headers := http.Header{}
 
 	deps := mockDeps(t, ex)
-	req, valErr, podErr := deps.parseVideoRequest(reqData, headers)
+	reqBody := string(getRequestPayload(t, reqData))
+	req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers)
 
 	assert.Equal(t, "TestHeaderSample", req.Device.UA, "Header should be taken from original request")
 	assert.Equal(t, []error(nil), valErr, "No validation errors should be returned")
@@ -902,7 +904,8 @@ func TestParseVideoRequestWithoutUserAgentWithHeader(t *testing.T) {
 	headers.Add("User-Agent", "TestHeader")
 
 	deps := mockDeps(t, ex)
-	req, valErr, podErr := deps.parseVideoRequest(reqData, headers)
+	reqBody := string(getRequestPayload(t, reqData))
+	req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers)
 
 	assert.Equal(t, "TestHeader", req.Device.UA, "Device.ua should be taken from request header")
 	assert.Equal(t, []error(nil), valErr, "No validation errors should be returned")
@@ -920,7 +923,8 @@ func TestParseVideoRequestWithoutUserAgentAndEmptyHeader(t *testing.T) {
 	headers := http.Header{}
 
 	deps := mockDeps(t, ex)
-	req, valErr, podErr := deps.parseVideoRequest(reqData, headers)
+	reqBody := string(getRequestPayload(t, reqData))
+	req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers)
 
 	assert.Equal(t, "", req.Device.UA, "Device.ua should be empty")
 	assert.Equal(t, []error(nil), valErr, "No validation errors should be returned")
@@ -942,7 +946,8 @@ func TestParseVideoRequestWithEncodedUserAgentInHeader(t *testing.T) {
 	headers.Add("User-Agent", uaEncoded)
 
 	deps := mockDeps(t, ex)
-	req, valErr, podErr := deps.parseVideoRequest(reqData, headers)
+	reqBody := string(getRequestPayload(t, reqData))
+	req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers)
 
 	assert.Equal(t, uaDecoded, req.Device.UA, "Device.ua should be taken from request header")
 	assert.Equal(t, []error(nil), valErr, "No validation errors should be returned")
@@ -963,7 +968,8 @@ func TestParseVideoRequestWithDecodedUserAgentInHeader(t *testing.T) {
 	headers.Add("User-Agent", uaDecoded)
 
 	deps := mockDeps(t, ex)
-	req, valErr, podErr := deps.parseVideoRequest(reqData, headers)
+	reqBody := string(getRequestPayload(t, reqData))
+	req, valErr, podErr := deps.parseVideoRequest([]byte(reqBody), headers)
 
 	assert.Equal(t, uaDecoded, req.Device.UA, "Device.ua should be taken from request header")
 	assert.Equal(t, []error(nil), valErr, "No validation errors should be returned")
@@ -1036,6 +1042,71 @@ func TestCreateImpressionTemplate(t *testing.T) {
 	assert.Equal(t, res.Video.PlaybackMethod, []openrtb.PlaybackMethod{7, 8}, "Incorrect video playback method")
 }
 
+func TestCCPA(t *testing.T) {
+	testCases := []struct {
+		description         string
+		testFilePath        string
+		expectConsentString bool
+	}{
+		{
+			description:         "Missing Consent",
+			testFilePath:        "sample-requests/video/video_valid_sample.json",
+			expectConsentString: false,
+		},
+		{
+			description:         "Valid Consent",
+			testFilePath:        "sample-requests/video/video_valid_sample_ccpa_valid.json",
+			expectConsentString: true,
+		},
+		{
+			description:         "Malformed Consent",
+			testFilePath:        "sample-requests/video/video_valid_sample_ccpa_malformed.json",
+			expectConsentString: false,
+		},
+	}
+
+	for _, test := range testCases {
+		// Load Test Request
+		requestContainerBytes, err := ioutil.ReadFile(test.testFilePath)
+		if err != nil {
+			t.Fatalf("%s: Failed to fetch a valid request: %v", test.description, err)
+		}
+		requestBytes := getRequestPayload(t, requestContainerBytes)
+
+		// Create HTTP Request + Response Recorder
+		httpRequest := httptest.NewRequest("POST", "/openrtb2/video", bytes.NewReader(requestBytes))
+		httpResponseRecorder := httptest.NewRecorder()
+
+		// Run Test
+		ex := &mockExchangeVideo{}
+		mockDeps(t, ex).VideoAuctionEndpoint(httpResponseRecorder, httpRequest, nil)
+
+		// Validate Request To Exchange
+		// - An error should never be generated for CCPA problems.
+		if ex.lastRequest == nil {
+			t.Fatalf("%s: The request never made it into the exchange.", test.description)
+		}
+		extRegs := &openrtb_ext.ExtRegs{}
+		if err = json.Unmarshal(ex.lastRequest.Regs.Ext, extRegs); err != nil {
+			t.Fatalf("%s: Failed to unmarshal reg.ext in request to the exchange: %v", test.description, err)
+		}
+		if test.expectConsentString {
+			assert.Len(t, extRegs.USPrivacy, 4, test.description+":consent")
+		} else {
+			assert.Empty(t, extRegs.USPrivacy, test.description+":consent")
+		}
+
+		// Validate HTTP Response
+		responseBytes := httpResponseRecorder.Body.Bytes()
+		response := &openrtb_ext.BidResponseVideo{}
+		if err := json.Unmarshal(responseBytes, response); err != nil {
+			t.Fatalf("%s: Unable to unmarshal response.", test.description)
+		}
+		assert.Len(t, ex.lastRequest.Imp, 11, test.description+":imps")
+		assert.Len(t, response.AdPods, 5, test.description+":adpods")
+	}
+}
+
 func mockDepsWithMetrics(t *testing.T, ex *mockExchangeVideo) (*endpointDeps, *pbsmetrics.Metrics, *mockAnalyticsModule) {
 	theMetrics := pbsmetrics.NewMetrics(metrics.NewRegistry(), openrtb_ext.BidderList(), config.DisabledMetrics{})
 	mockModule := &mockAnalyticsModule{}
@@ -1166,3 +1237,19 @@ var testVideoStoredImpData = map[string]json.RawMessage{
 var testVideoStoredRequestData = map[string]json.RawMessage{
 	"80ce30c53c16e6ede735f123ef6e32361bfc7b22": json.RawMessage(`{"accountid": "11223344", "site": {"page": "mygame.foo.com"}}`),
 }
+
+func loadValidRequest(t *testing.T) *openrtb_ext.BidRequestVideo {
+	reqData, err := ioutil.ReadFile("sample-requests/video/video_valid_sample.json")
+	if err != nil {
+		t.Fatalf("Failed to fetch a valid request: %v", err)
+	}
+
+	reqBody := getRequestPayload(t, reqData)
+
+	reqVideo := &openrtb_ext.BidRequestVideo{}
+	if err := json.Unmarshal(reqBody, reqVideo); err != nil {
+		t.Fatalf("Failed to unmarshal the request: %v", err)
+	}
+
+	return reqVideo
+}
diff --git a/exchange/adapter_map_test.go b/exchange/adapter_map_test.go
index a732f357..f472ab1d 100644
--- a/exchange/adapter_map_test.go
+++ b/exchange/adapter_map_test.go
@@ -7,11 +7,12 @@ import (
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config"
 )
 
 func TestNewAdapterMap(t *testing.T) {
 	cfg := &config.Configuration{Adapters: blankAdapterConfig(openrtb_ext.BidderList())}
-	adapterMap := newAdapterMap(nil, cfg, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()))
+	adapterMap := newAdapterMap(nil, cfg, adapters.ParseBidderInfos(cfg.Adapters, "../static/bidder-info", openrtb_ext.BidderList()), &metricsConfig.DummyMetricsEngine{})
 	for _, bidderName := range openrtb_ext.BidderMap {
 		if bidder, ok := adapterMap[bidderName]; bidder == nil || !ok {
 			t.Errorf("adapterMap missing expected Bidder: %s", string(bidderName))
@@ -38,7 +39,7 @@ func TestNewAdapterMapDisabledAdapters(t *testing.T) {
 			}
 		}
 	}
-	adapterMap := newAdapterMap(nil, &config.Configuration{Adapters: cfgAdapters}, adapters.ParseBidderInfos(cfgAdapters, "../static/bidder-info", bidderList))
+	adapterMap := newAdapterMap(nil, &config.Configuration{Adapters: cfgAdapters}, adapters.ParseBidderInfos(cfgAdapters, "../static/bidder-info", bidderList), &metricsConfig.DummyMetricsEngine{})
 	for _, bidderName := range openrtb_ext.BidderMap {
 		if bidder, ok := adapterMap[bidderName]; bidder == nil || !ok {
 			if inList(bidderList, bidderName) {
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index f20b431c..fa04e6a4 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -12,8 +12,10 @@ import (
 
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currencies"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config"
 	"github.com/stretchr/testify/assert"
 
 	nativeRequests "github.com/mxmCherry/openrtb/native/request"
@@ -64,7 +66,7 @@ func TestSingleBidder(t *testing.T) {
 		},
 		bidResponse: mockBidderResponse,
 	}
-	bidder := adaptBidder(bidderImpl, server.Client())
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 	currencyConverter := currencies.NewRateConverterDefault()
 	seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", bidAdjustment, currencyConverter.Rates(), &adapters.ExtraRequestInfo{})
 
@@ -152,7 +154,7 @@ func TestMultiBidder(t *testing.T) {
 			}},
 		bidResponse: mockBidderResponse,
 	}
-	bidder := adaptBidder(bidderImpl, server.Client())
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 	currencyConverter := currencies.NewRateConverterDefault()
 	seatBid, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{})
 
@@ -510,7 +512,7 @@ func TestMultiCurrencies(t *testing.T) {
 		)
 
 		// Execute:
-		bidder := adaptBidder(bidderImpl, server.Client())
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 		currencyConverter := currencies.NewRateConverter(
 			&http.Client{},
 			mockedHTTPServer.URL,
@@ -658,7 +660,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) {
 		}
 
 		// Execute:
-		bidder := adaptBidder(bidderImpl, server.Client())
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 		currencyConverter := currencies.NewRateConverterDefault()
 		seatBid, errs := bidder.requestBid(
 			context.Background(),
@@ -824,7 +826,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) {
 		}
 
 		// Execute:
-		bidder := adaptBidder(bidderImpl, server.Client())
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 		currencyConverter := currencies.NewRateConverter(
 			&http.Client{},
 			mockedHTTPServer.URL,
@@ -939,7 +941,7 @@ func TestServerCallDebugging(t *testing.T) {
 			Headers: http.Header{},
 		},
 	}
-	bidder := adaptBidder(bidderImpl, server.Client())
+	bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 	currencyConverter := currencies.NewRateConverterDefault()
 
 	bids, _ := bidder.requestBid(
@@ -1051,7 +1053,7 @@ func TestMobileNativeTypes(t *testing.T) {
 			},
 			bidResponse: tc.mockBidderResponse,
 		}
-		bidder := adaptBidder(bidderImpl, server.Client())
+		bidder := adaptBidder(bidderImpl, server.Client(), &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 		currencyConverter := currencies.NewRateConverterDefault()
 
 		seatBids, _ := bidder.requestBid(
@@ -1072,7 +1074,7 @@ func TestMobileNativeTypes(t *testing.T) {
 }
 
 func TestErrorReporting(t *testing.T) {
-	bidder := adaptBidder(&bidRejector{}, nil)
+	bidder := adaptBidder(&bidRejector{}, nil, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 	currencyConverter := currencies.NewRateConverterDefault()
 	bids, errs := bidder.requestBid(context.Background(), &openrtb.BidRequest{}, "test", 1.0, currencyConverter.Rates(), &adapters.ExtraRequestInfo{})
 	if bids != nil {
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index e9b2127e..4f329962 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -731,7 +731,17 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 	if len(errs) != 0 {
 		t.Fatalf("%s: Failed to parse aliases", filename)
 	}
-	ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, spec.EnforceCCPA)
+
+	privacyConfig := config.Privacy{
+		CCPA: config.CCPA{
+			Enforce: spec.EnforceCCPA,
+		},
+		LMT: config.LMT{
+			Enforce: spec.EnforceLMT,
+		},
+	}
+
+	ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig)
 	biddersInAuction := findBiddersInAuction(t, filename, &spec.IncomingRequest.OrtbRequest)
 	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
 	if error != nil {
@@ -816,7 +826,7 @@ func extractResponseTimes(t *testing.T, context string, bid *openrtb.BidResponse
 	}
 }
 
-func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, enforceCCPA bool) Exchange {
+func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy) Exchange {
 	adapters := make(map[openrtb_ext.BidderName]adaptedBidder)
 	for _, bidderName := range openrtb_ext.BidderMap {
 		if spec, ok := expectations[string(bidderName)]; ok {
@@ -854,7 +864,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 		gDPR:                gdpr.AlwaysAllow{},
 		currencyConverter:   currencies.NewRateConverterDefault(),
 		UsersyncIfAmbiguous: false,
-		enforceCCPA:         enforceCCPA,
+		privacyConfig:       privacyConfig,
 	}
 }
 
@@ -1620,6 +1630,7 @@ type exchangeSpec struct {
 	OutgoingRequests map[string]*bidderSpec `json:"outgoingRequests"`
 	Response         exchangeResponse       `json:"response,omitempty"`
 	EnforceCCPA      bool                   `json:"enforceCcpa"`
+	EnforceLMT       bool                   `json:"enforceLmt"`
 	DebugLog         *DebugLog              `json:"debuglog,omitempty"`
 }
 
diff --git a/exchange/exchangetest/lmt-featureflag-off.json b/exchange/exchangetest/lmt-featureflag-off.json
new file mode 100644
index 00000000..9a15c879
--- /dev/null
+++ b/exchange/exchangetest/lmt-featureflag-off.json
@@ -0,0 +1,63 @@
+{
+    "enforceLmt": false,
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "my-imp-id",
+                "video": {
+                    "mimes": ["video/mp4"]
+                },
+                "ext": {
+                    "appnexus": {
+                        "placementId": 1
+                    }
+                }
+            }],
+            "device": {
+                "lmt": 1
+            },
+            "user": {
+                "id": "some-id",
+                "buyeruid": "some-buyer-id"
+            }
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "my-imp-id",
+                        "video": {
+                            "mimes": ["video/mp4"]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            }
+                        }
+                    }],
+                    "device": {
+                        "lmt": 1
+                    },
+                    "user": {
+                        "id": "some-id",
+                        "buyeruid": "some-buyer-id"
+                    }
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "errors": ["appnexus-error"]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/exchangetest/lmt-featureflag-on.json b/exchange/exchangetest/lmt-featureflag-on.json
new file mode 100644
index 00000000..440f8c76
--- /dev/null
+++ b/exchange/exchangetest/lmt-featureflag-on.json
@@ -0,0 +1,61 @@
+{
+    "enforceLmt": true,
+    "incomingRequest": {
+        "ortbRequest": {
+            "id": "some-request-id",
+            "site": {
+                "page": "test.somepage.com"
+            },
+            "imp": [{
+                "id": "my-imp-id",
+                "video": {
+                    "mimes": ["video/mp4"]
+                },
+                "ext": {
+                    "appnexus": {
+                        "placementId": 1
+                    }
+                }
+            }],
+            "device": {
+                "lmt": 1
+            },
+            "user": {
+                "id": "some-id",
+                "buyeruid": "some-buyer-id"
+            }
+        }
+    },
+    "outgoingRequests": {
+        "appnexus": {
+            "expectRequest": {
+                "ortbRequest": {
+                    "id": "some-request-id",
+                    "site": {
+                        "page": "test.somepage.com"
+                    },
+                    "imp": [{
+                        "id": "my-imp-id",
+                        "video": {
+                            "mimes": ["video/mp4"]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": 1
+                            }
+                        }
+                    }],
+                    "device": {
+                        "lmt": 1
+                    },
+                    "user": {
+                    }
+                },
+                "bidAdjustment": 1.0
+            },
+            "mockResponse": {
+                "errors": ["appnexus-error"]
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index f8630968..72de1d42 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -8,12 +8,14 @@ import (
 	"testing"
 	"time"
 
+	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currencies"
 
 	"github.com/prebid/prebid-server/gdpr"
 
 	"github.com/prebid/prebid-server/pbsmetrics"
 	metricsConf "github.com/prebid/prebid-server/pbsmetrics/config"
+	metricsConfig "github.com/prebid/prebid-server/pbsmetrics/config"
 
 	"github.com/mxmCherry/openrtb"
 	"github.com/prebid/prebid-server/adapters"
@@ -132,7 +134,7 @@ func buildAdapterMap(bids map[openrtb_ext.BidderName][]*openrtb.Bid, mockServerU
 		adapterMap[bidder] = adaptBidder(&mockTargetingBidder{
 			mockServerURL: mockServerURL,
 			bids:          bids,
-		}, client)
+		}, client, &config.Configuration{}, &metricsConfig.DummyMetricsEngine{})
 	}
 	return adapterMap
 }
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index acbf25ff..4dad3f54 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/mxmCherry/openrtb"
+	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/prebid/prebid-server/pbsmetrics"
 	"github.com/stretchr/testify/assert"
@@ -69,8 +70,17 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 			applyCOPPA: false, consentedVendors: map[string]bool{"appnexus": true, "brightroll": true}},
 	}
 
+	privacyConfig := config.Privacy{
+		CCPA: config.CCPA{
+			Enforce: true,
+		},
+		LMT: config.LMT{
+			Enforce: true,
+		},
+	}
+
 	for _, test := range testCases {
-		reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, true)
+		reqByBidders, _, err := cleanOpenRTBRequests(context.Background(), test.req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
 		if test.hasError {
 			assert.NotNil(t, err, "Error shouldn't be nil")
 		} else {
@@ -99,9 +109,80 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		req := newCCPABidRequest(t)
+		req := newBidRequest(t)
+		req.Regs = &openrtb.Regs{
+			Ext: json.RawMessage(`{"us_privacy":"1-Y-"}`),
+		}
 
-		results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, test.enforceCCPA)
+		privacyConfig := config.Privacy{
+			CCPA: config.CCPA{
+				Enforce: test.enforceCCPA,
+			},
+		}
+
+		results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
+		result := results["appnexus"]
+
+		assert.Nil(t, errs)
+
+		if test.expectDataScrub {
+			assert.Equal(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.Equal(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
+		} else {
+			assert.NotEqual(t, result.User.BuyerUID, "", test.description+":User.BuyerUID")
+			assert.NotEqual(t, result.Device.DIDMD5, "", test.description+":Device.DIDMD5")
+		}
+	}
+}
+
+func TestCleanOpenRTBRequestsLMT(t *testing.T) {
+	var (
+		enabled  int8 = 1
+		disabled int8 = 0
+	)
+	testCases := []struct {
+		description     string
+		lmt             *int8
+		enforceLMT      bool
+		expectDataScrub bool
+	}{
+		{
+			description:     "Feature Flag Enabled - OpenTRB Enabled",
+			lmt:             &enabled,
+			enforceLMT:      true,
+			expectDataScrub: true,
+		},
+		{
+			description:     "Feature Flag Disabled - OpenTRB Enabled",
+			lmt:             &enabled,
+			enforceLMT:      false,
+			expectDataScrub: false,
+		},
+		{
+			description:     "Feature Flag Enabled - OpenTRB Disabled",
+			lmt:             &disabled,
+			enforceLMT:      true,
+			expectDataScrub: false,
+		},
+		{
+			description:     "Feature Flag Disabled - OpenTRB Disabled",
+			lmt:             &disabled,
+			enforceLMT:      false,
+			expectDataScrub: false,
+		},
+	}
+
+	for _, test := range testCases {
+		req := newBidRequest(t)
+		req.Device.Lmt = test.lmt
+
+		privacyConfig := config.Privacy{
+			LMT: config.LMT{
+				Enforce: test.enforceLMT,
+			},
+		}
+
+		results, _, errs := cleanOpenRTBRequests(context.Background(), req, &emptyUsersync{}, map[openrtb_ext.BidderName]*pbsmetrics.AdapterLabels{}, pbsmetrics.Labels{}, &permissionsMock{}, true, privacyConfig)
 		result := results["appnexus"]
 
 		assert.Nil(t, errs)
@@ -163,8 +244,7 @@ func newAdapterAliasBidRequest(t *testing.T) *openrtb.BidRequest {
 	}
 }
 
-func newCCPABidRequest(t *testing.T) *openrtb.BidRequest {
-	dnt := int8(1)
+func newBidRequest(t *testing.T) *openrtb.BidRequest {
 	return &openrtb.BidRequest{
 		Site: &openrtb.Site{
 			Page:   "www.some.domain.com",
@@ -178,7 +258,6 @@ func newCCPABidRequest(t *testing.T) *openrtb.BidRequest {
 			UA:       "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36",
 			IFA:      "ifa",
 			IP:       "132.173.230.74",
-			DNT:      &dnt,
 			Language: "EN",
 		},
 		Source: &openrtb.Source{
@@ -189,9 +268,6 @@ func newCCPABidRequest(t *testing.T) *openrtb.BidRequest {
 			BuyerUID: "their-id",
 			Ext:      json.RawMessage(`{"digitrust":{"id":"digi-id","keyv":1,"pref":1}}`),
 		},
-		Regs: &openrtb.Regs{
-			Ext: json.RawMessage(`{"us_privacy":"1-Y-"}`),
-		},
 		Imp: []openrtb.Imp{{
 			ID: "some-imp-id",
 			Banner: &openrtb.Banner{
diff --git a/pbsmetrics/go_metrics_test.go b/pbsmetrics/go_metrics_test.go
index 253ff69e..25f75e77 100644
--- a/pbsmetrics/go_metrics_test.go
+++ b/pbsmetrics/go_metrics_test.go
@@ -53,6 +53,9 @@ func TestNewMetrics(t *testing.T) {
 
 	ensureContains(t, registry, "queued_requests.video.rejected", m.RequestsQueueTimer[ReqTypeVideo][false])
 	ensureContains(t, registry, "queued_requests.video.accepted", m.RequestsQueueTimer[ReqTypeVideo][true])
+
+	ensureContains(t, registry, "timeout_notification.ok", m.TimeoutNotificationSuccess)
+	ensureContains(t, registry, "timeout_notification.failed", m.TimeoutNotificationFailure)
 }
 
 func TestRecordBidType(t *testing.T) {
diff --git a/pbsmetrics/prometheus/prometheus_test.go b/pbsmetrics/prometheus/prometheus_test.go
index e4d6a4f7..24c50492 100644
--- a/pbsmetrics/prometheus/prometheus_test.go
+++ b/pbsmetrics/prometheus/prometheus_test.go
@@ -923,6 +923,27 @@ func TestRecordRequestQueueTimeMetric(t *testing.T) {
 	}
 }
 
+func TestTimeoutNotifications(t *testing.T) {
+	m := createMetricsForTesting()
+
+	m.RecordTimeoutNotice(true)
+	m.RecordTimeoutNotice(true)
+	m.RecordTimeoutNotice(false)
+
+	assertCounterVecValue(t, "", "timeout_notifications:ok", m.timeout_notifications,
+		float64(2),
+		prometheus.Labels{
+			successLabel: requestSuccessful,
+		})
+
+	assertCounterVecValue(t, "", "timeout_notifications:fail", m.timeout_notifications,
+		float64(1),
+		prometheus.Labels{
+			successLabel: requestFailed,
+		})
+
+}
+
 func assertCounterValue(t *testing.T, description, name string, counter prometheus.Counter, expected float64) {
 	m := dto.Metric{}
 	counter.Write(&m)
diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go
index 740f95a8..e9b4c452 100644
--- a/privacy/ccpa/policy_test.go
+++ b/privacy/ccpa/policy_test.go
@@ -112,6 +112,48 @@ func TestWrite(t *testing.T) {
 			request:     &openrtb.BidRequest{},
 			expected:    &openrtb.BidRequest{},
 		},
+		{
+			description: "Disabled - Nil Request",
+			policy:      Policy{Value: ""},
+			request:     nil,
+			expected:    nil,
+		},
+		{
+			description: "Disabled - Empty Regs.Ext",
+			policy:      Policy{Value: ""},
+			request:     &openrtb.BidRequest{Regs: &openrtb.Regs{}},
+			expected:    &openrtb.BidRequest{Regs: &openrtb.Regs{}},
+		},
+		{
+			description: "Disabled - Remove From Request",
+			policy:      Policy{Value: ""},
+			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"us_privacy":"toBeRemoved"}`)}},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{}},
+		},
+		{
+			description: "Disabled - Remove From Request, Leave Other req Values",
+			policy:      Policy{Value: ""},
+			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				COPPA: 42,
+				Ext:   json.RawMessage(`{"us_privacy":"toBeRemoved"}`)}},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				COPPA: 42}},
+		},
+		{
+			description: "Disabled - Remove From Request, Leave Other req.ext Values",
+			policy:      Policy{Value: ""},
+			request: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"existing":"any","us_privacy":"toBeRemoved"}`)}},
+			expected: &openrtb.BidRequest{Regs: &openrtb.Regs{
+				Ext: json.RawMessage(`{"existing":"any"}`)}},
+		},
+		{
+			description: "Enabled - Nil Request",
+			policy:      Policy{Value: "anyValue"},
+			request:     nil,
+			expected:    nil,
+		},
 		{
 			description: "Enabled With Nil Request Regs Object",
 			policy:      Policy{Value: "anyValue"},
diff --git a/privacy/enforcement_test.go b/privacy/enforcement_test.go
index 0e82648d..968c6354 100644
--- a/privacy/enforcement_test.go
+++ b/privacy/enforcement_test.go
@@ -21,6 +21,7 @@ func TestAny(t *testing.T) {
 				COPPA:   false,
 				GDPR:    false,
 				GDPRGeo: false,
+				LMT:     false,
 			},
 			expected: false,
 		},
@@ -31,6 +32,7 @@ func TestAny(t *testing.T) {
 				COPPA:   true,
 				GDPR:    true,
 				GDPRGeo: true,
+				LMT:     true,
 			},
 			expected: true,
 		},
@@ -41,16 +43,7 @@ func TestAny(t *testing.T) {
 				COPPA:   true,
 				GDPR:    false,
 				GDPRGeo: false,
-			},
-			expected: true,
-		},
-		{
-			description: "GDPRGeo only",
-			enforcement: Enforcement{
-				CCPA:    false,
-				COPPA:   false,
-				GDPR:    false,
-				GDPRGeo: true,
+				LMT:     true,
 			},
 			expected: true,
 		},
@@ -79,6 +72,7 @@ func TestApply(t *testing.T) {
 				COPPA:   true,
 				GDPR:    true,
 				GDPRGeo: true,
+				LMT:     true,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest32,
@@ -93,6 +87,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    false,
 				GDPRGeo: false,
+				LMT:     false,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
@@ -107,6 +102,7 @@ func TestApply(t *testing.T) {
 				COPPA:   true,
 				GDPR:    false,
 				GDPRGeo: false,
+				LMT:     false,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest32,
@@ -121,6 +117,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    true,
 				GDPRGeo: true,
+				LMT:     false,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
@@ -135,6 +132,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    true,
 				GDPRGeo: true,
+				LMT:     false,
 			},
 			ampGDPRException:   true,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
@@ -149,6 +147,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    false,
 				GDPRGeo: false,
+				LMT:     false,
 			},
 			ampGDPRException:   true,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
@@ -163,6 +162,7 @@ func TestApply(t *testing.T) {
 				COPPA:   true,
 				GDPR:    true,
 				GDPRGeo: true,
+				LMT:     false,
 			},
 			ampGDPRException:   true,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest32,
@@ -177,6 +177,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    true,
 				GDPRGeo: false,
+				LMT:     false,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
@@ -191,6 +192,7 @@ func TestApply(t *testing.T) {
 				COPPA:   false,
 				GDPR:    false,
 				GDPRGeo: true,
+				LMT:     false,
 			},
 			ampGDPRException:   false,
 			expectedDeviceIPv6: ScrubStrategyIPV6None,
@@ -198,6 +200,36 @@ func TestApply(t *testing.T) {
 			expectedUser:       ScrubStrategyUserNone,
 			expectedUserGeo:    ScrubStrategyGeoReducedPrecision,
 		},
+		{
+			description: "LMT Only",
+			enforcement: Enforcement{
+				CCPA:    false,
+				COPPA:   false,
+				GDPR:    false,
+				GDPRGeo: false,
+				LMT:     true,
+			},
+			ampGDPRException:   false,
+			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
+			expectedDeviceGeo:  ScrubStrategyGeoReducedPrecision,
+			expectedUser:       ScrubStrategyUserID,
+			expectedUserGeo:    ScrubStrategyGeoReducedPrecision,
+		},
+		{
+			description: "LMT Only, ampGDPRException",
+			enforcement: Enforcement{
+				CCPA:    false,
+				COPPA:   false,
+				GDPR:    false,
+				GDPRGeo: false,
+				LMT:     true,
+			},
+			ampGDPRException:   true,
+			expectedDeviceIPv6: ScrubStrategyIPV6Lowest16,
+			expectedDeviceGeo:  ScrubStrategyGeoReducedPrecision,
+			expectedUser:       ScrubStrategyUserID,
+			expectedUserGeo:    ScrubStrategyGeoReducedPrecision,
+		},
 	}
 
 	for _, test := range testCases {
@@ -229,6 +261,7 @@ func TestApplyNoneApplicable(t *testing.T) {
 		CCPA:  false,
 		COPPA: false,
 		GDPR:  false,
+		LMT:   false,
 	}
 	enforcement.apply(req, false, m)
 
diff --git a/privacy/lmt/policy_test.go b/privacy/lmt/policy_test.go
new file mode 100644
index 00000000..45de219a
--- /dev/null
+++ b/privacy/lmt/policy_test.go
@@ -0,0 +1,128 @@
+package lmt
+
+import (
+	"testing"
+
+	"github.com/mxmCherry/openrtb"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRead(t *testing.T) {
+	var one int8 = 1
+
+	testCases := []struct {
+		description    string
+		request        *openrtb.BidRequest
+		expectedPolicy Policy
+	}{
+		{
+			description: "Nil Request",
+			request:     nil,
+			expectedPolicy: Policy{
+				Signal:         0,
+				SignalProvided: false,
+			},
+		},
+		{
+			description: "Nil Device",
+			request: &openrtb.BidRequest{
+				Device: nil,
+			},
+			expectedPolicy: Policy{
+				Signal:         0,
+				SignalProvided: false,
+			},
+		},
+		{
+			description: "Nil Device.Lmt",
+			request: &openrtb.BidRequest{
+				Device: &openrtb.Device{
+					Lmt: nil,
+				},
+			},
+			expectedPolicy: Policy{
+				Signal:         0,
+				SignalProvided: false,
+			},
+		},
+		{
+			description: "Enabled",
+			request: &openrtb.BidRequest{
+				Device: &openrtb.Device{
+					Lmt: &one,
+				},
+			},
+			expectedPolicy: Policy{
+				Signal:         1,
+				SignalProvided: true,
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		p := ReadPolicy(test.request)
+		assert.Equal(t, test.expectedPolicy, p, test.description)
+	}
+}
+
+func TestShouldEnforce(t *testing.T) {
+	testCases := []struct {
+		description string
+		policy      Policy
+		expected    bool
+	}{
+		{
+			description: "Signal Not Provided - Zero",
+			policy: Policy{
+				Signal:         0,
+				SignalProvided: false,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Not Provided - One",
+			policy: Policy{
+				Signal:         1,
+				SignalProvided: false,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Not Provided - Other",
+			policy: Policy{
+				Signal:         42,
+				SignalProvided: false,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Provided - Zero",
+			policy: Policy{
+				Signal:         0,
+				SignalProvided: true,
+			},
+			expected: false,
+		},
+		{
+			description: "Signal Provided - One",
+			policy: Policy{
+				Signal:         1,
+				SignalProvided: true,
+			},
+			expected: true,
+		},
+		{
+			description: "Signal Provided - Other",
+			policy: Policy{
+				Signal:         42,
+				SignalProvided: true,
+			},
+			expected: false,
+		},
+	}
+
+	for _, test := range testCases {
+		result := test.policy.ShouldEnforce()
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index ddd067e8..c9ef382f 100755
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -21,6 +21,7 @@ func TestNewSyncerMap(t *testing.T) {
 			string(openrtb_ext.BidderAdmixer):          syncConfig,
 			string(openrtb_ext.BidderAdOcean):          syncConfig,
 			string(openrtb_ext.BidderAdpone):           syncConfig,
+			string(openrtb_ext.BidderAdtarget):         syncConfig,
 			string(openrtb_ext.BidderAdtelligent):      syncConfig,
 			string(openrtb_ext.BidderAdvangelists):     syncConfig,
 			string(openrtb_ext.BidderAJA):              syncConfig,