Repository: prebid/prebid-server
Pull request #2084 (2021-11-15 22:21:13)

Adform adapter lacked gross/net parameter support
The client version of the Adform adapter supports choosing between returning gross or net via the priceType parameter.

As the new "adf" adapter defaults to net while the old "adform" adapter defaulted to gross, this can make the transition to the new adapter become quite a challenge if you are using plenty of bid conversion functions and the PBS adapter doesn't support selecting gross or net.

This PR adds the same priceType support as the client has.

bjorn-lw: Needs more work
bjorn-lw: I'm a bit uncertain if I should preserve existing request.ext structure or if I can simply overwrite it, please advice.

Patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index bbb76d36..9a7ae430 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -14,6 +14,7 @@
 	},
 	"containerEnv": {
 		"GOPRIVATE": "${localEnv:GOPRIVATE}",
+		"PBS_GDPR_DEFAULT_VALUE": "0"
 	},
 	"runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
 
diff --git a/adapters/33across/33across.go b/adapters/33across/33across.go
index fb329a76..b6c16b20 100644
--- a/adapters/33across/33across.go
+++ b/adapters/33across/33across.go
@@ -54,6 +54,7 @@ type bidTtxExt struct {
 func (a *TtxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	var errs []error
 	var adapterRequests []*adapters.RequestData
+	var groupedImps = make(map[string][]openrtb2.Imp)
 
 	// Construct request extension common to all imps
 	// NOTE: not blocking adapter requests on errors
@@ -64,27 +65,39 @@ func (a *TtxAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapter
 	}
 	request.Ext = reqExt
 
-	// Break up multi-imp request into multiple external requests since we don't
-	// support SRA in our exchange server
+	// We only support SRA for requests containing same prod and
+	// zoneID, therefore group all imps accordingly and create a http
+	// request for each such group
 	for i := 0; i < len(request.Imp); i++ {
-		if adapterReq, err := a.makeRequest(*request, request.Imp[i]); err == nil {
-			adapterRequests = append(adapterRequests, adapterReq)
+		if impCopy, err := makeImps(request.Imp[i]); err == nil {
+			var impExt Ext
+
+			// Skip over imps whose extensions cannot be read since
+			// we cannot glean Prod or ZoneID which are required to
+			// group together. However let's not block request creation.
+			if err := json.Unmarshal(impCopy.Ext, &impExt); err == nil {
+				impKey := impExt.Ttx.Prod + impExt.Ttx.Zoneid
+				groupedImps[impKey] = append(groupedImps[impKey], impCopy)
+			} else {
+				errs = append(errs, err)
+			}
 		} else {
 			errs = append(errs, err)
 		}
 	}
 
+	for _, impList := range groupedImps {
+		if adapterReq, err := a.makeRequest(*request, impList); err == nil {
+			adapterRequests = append(adapterRequests, adapterReq)
+		} else {
+			errs = append(errs, err)
+		}
+	}
 	return adapterRequests, errs
 }
 
-func (a *TtxAdapter) makeRequest(request openrtb2.BidRequest, imp openrtb2.Imp) (*adapters.RequestData, error) {
-	impCopy, err := makeImps(imp)
-
-	if err != nil {
-		return nil, err
-	}
-
-	request.Imp = []openrtb2.Imp{*impCopy}
+func (a *TtxAdapter) makeRequest(request openrtb2.BidRequest, impList []openrtb2.Imp) (*adapters.RequestData, error) {
+	request.Imp = impList
 
 	// Last Step
 	reqJSON, err := json.Marshal(request)
@@ -103,23 +116,23 @@ func (a *TtxAdapter) makeRequest(request openrtb2.BidRequest, imp openrtb2.Imp)
 	}, nil
 }
 
-func makeImps(imp openrtb2.Imp) (*openrtb2.Imp, error) {
+func makeImps(imp openrtb2.Imp) (openrtb2.Imp, error) {
 	if imp.Banner == nil && imp.Video == nil {
-		return nil, &errortypes.BadInput{
+		return openrtb2.Imp{}, &errortypes.BadInput{
 			Message: fmt.Sprintf("Imp ID %s must have at least one of [Banner, Video] defined", imp.ID),
 		}
 	}
 
 	var bidderExt adapters.ExtImpBidder
 	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
-		return nil, &errortypes.BadInput{
+		return openrtb2.Imp{}, &errortypes.BadInput{
 			Message: err.Error(),
 		}
 	}
 
 	var ttxExt openrtb_ext.ExtImp33across
 	if err := json.Unmarshal(bidderExt.Bidder, &ttxExt); err != nil {
-		return nil, &errortypes.BadInput{
+		return openrtb2.Imp{}, &errortypes.BadInput{
 			Message: err.Error(),
 		}
 	}
@@ -135,7 +148,7 @@ func makeImps(imp openrtb2.Imp) (*openrtb2.Imp, error) {
 
 	impExtJSON, err := json.Marshal(impExt)
 	if err != nil {
-		return nil, &errortypes.BadInput{
+		return openrtb2.Imp{}, &errortypes.BadInput{
 			Message: err.Error(),
 		}
 	}
@@ -149,13 +162,13 @@ func makeImps(imp openrtb2.Imp) (*openrtb2.Imp, error) {
 		imp.Video = videoCopy
 
 		if err != nil {
-			return nil, &errortypes.BadInput{
+			return openrtb2.Imp{}, &errortypes.BadInput{
 				Message: err.Error(),
 			}
 		}
 	}
 
-	return &imp, nil
+	return imp, nil
 }
 
 func makeReqExt(request *openrtb2.BidRequest) ([]byte, error) {
diff --git a/adapters/adf/adf.go b/adapters/adf/adf.go
index 8014bd5b..bc54f78f 100644
--- a/adapters/adf/adf.go
+++ b/adapters/adf/adf.go
@@ -16,6 +16,11 @@ type adapter struct {
 	endpoint string
 }
 
+type adfRequestExt struct {
+	openrtb_ext.ExtRequest
+	PriceType string `json:"pt"`
+}
+
 // Builder builds a new instance of the Adf adapter for the given bidder with the given config.
 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) {
 	bidder := &adapter{
@@ -27,6 +32,7 @@ func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters
 func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	var errors []error
 	var validImps = make([]openrtb2.Imp, 0, len(request.Imp))
+	priceType := ""
 
 	for _, imp := range request.Imp {
 		var bidderExt adapters.ExtImpBidder
@@ -47,6 +53,30 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte
 
 		imp.TagID = adfImpExt.MasterTagID.String()
 		validImps = append(validImps, imp)
+
+		// If imps specify priceType they should all be the same. If they differ, only the first one will be used
+		if adfImpExt.PriceType != "" && priceType == "" {
+			priceType = adfImpExt.PriceType
+		}
+	}
+
+	if priceType != "" {
+		requestExt := adfRequestExt{}
+		var err error
+
+		if len(request.Ext) > 0 {
+			if err = json.Unmarshal(request.Ext, &requestExt); err != nil {
+				errors = append(errors, err)
+			}
+		}
+
+		if err == nil {
+			requestExt.PriceType = priceType
+
+			if request.Ext, err = json.Marshal(&requestExt); err != nil {
+				errors = append(errors, err)
+			}
+		}
 	}
 
 	request.Imp = validImps
diff --git a/adapters/adview/adview.go b/adapters/adview/adview.go
index c3e78b42..e9872981 100644
--- a/adapters/adview/adview.go
+++ b/adapters/adview/adview.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"strings"
 	"text/template"
 
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
@@ -58,6 +59,22 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapte
 		}
 	}
 
+	// Check if imp comes with bid floor amount defined in a foreign currency
+	if imp.BidFloor > 0 && imp.BidFloorCur != "" && strings.ToUpper(imp.BidFloorCur) != "USD" {
+		// Convert to US dollars
+		convertedValue, err := requestInfo.ConvertCurrency(imp.BidFloor, imp.BidFloorCur, "USD")
+		if err != nil {
+			return nil, []error{err}
+		}
+		// Update after conversion. All imp elements inside request.Imp are shallow copies
+		// therefore, their non-pointer values are not shared memory and are safe to modify.
+		imp.BidFloorCur = "USD"
+		imp.BidFloor = convertedValue
+	}
+
+	// Set the CUR of bid to USD after converting all floors
+	request.Cur = []string{"USD"}
+
 	url, err := a.buildEndpointURL(&advImpExt)
 	if err != nil {
 		return nil, []error{err}
@@ -100,7 +117,8 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R
 	}
 
 	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
-	bidResponse.Currency = response.Cur
+	bidResponse.Currency = "USD" //we just support USD for resp
+
 	var errors []error
 	for _, seatBid := range response.SeatBid {
 		for i, bid := range seatBid.Bid {
diff --git a/adapters/coinzilla/coinzilla.go b/adapters/coinzilla/coinzilla.go
new file mode 100644
index 00000000..8535133d
--- /dev/null
+++ b/adapters/coinzilla/coinzilla.go
@@ -0,0 +1,85 @@
+package coinzilla
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/errortypes"
+)
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) {
+	bidder := &adapter{
+		endpoint: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+type adapter struct {
+	endpoint string
+}
+
+func (adapter *adapter) MakeRequests(openRTBRequest *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) (requestsToBidder []*adapters.RequestData, errs []error) {
+	if len(openRTBRequest.Imp) == 0 {
+		return nil, []error{&errortypes.BadInput{
+			Message: "No impression in the bid request",
+		}}
+	}
+	openRTBRequestJSON, err := json.Marshal(openRTBRequest)
+	if err != nil {
+		errs = append(errs, err)
+		return nil, errs
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+	headers.Add("x-openrtb-version", "2.5")
+
+	requestToBidder := &adapters.RequestData{
+		Method:  "POST",
+		Uri:     adapter.endpoint,
+		Body:    openRTBRequestJSON,
+		Headers: headers,
+	}
+	requestsToBidder = append(requestsToBidder, requestToBidder)
+
+	return requestsToBidder, errs
+}
+
+func (adapter *adapter) MakeBids(openRTBRequest *openrtb2.BidRequest, requestToBidder *adapters.RequestData, bidderRawResponse *adapters.ResponseData) (bidderResponse *adapters.BidderResponse, errs []error) {
+	switch bidderRawResponse.StatusCode {
+	case http.StatusOK:
+		break
+	case http.StatusNoContent:
+		return nil, nil
+	default:
+		err := &errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Unexpected code: %d. Run with request.debug = 1", bidderRawResponse.StatusCode),
+		}
+		return nil, []error{err}
+	}
+
+	var openRTBBidderResponse openrtb2.BidResponse
+	if err := json.Unmarshal(bidderRawResponse.Body, &openRTBBidderResponse); err != nil {
+		return nil, []error{err}
+	}
+
+	bidsCapacity := len(openRTBBidderResponse.SeatBid[0].Bid)
+	bidderResponse = adapters.NewBidderResponseWithBidsCapacity(bidsCapacity)
+	bidderResponse.Currency = openRTBBidderResponse.Cur
+	var typedBid *adapters.TypedBid
+	for _, seatBid := range openRTBBidderResponse.SeatBid {
+		for i := range seatBid.Bid {
+			typedBid = &adapters.TypedBid{Bid: &seatBid.Bid[i], BidType: "banner"}
+			bidderResponse.Bids = append(bidderResponse.Bids, typedBid)
+		}
+	}
+
+	return bidderResponse, nil
+}
diff --git a/adapters/openrtb_util.go b/adapters/openrtb_util.go
new file mode 100644
index 00000000..5587cac7
--- /dev/null
+++ b/adapters/openrtb_util.go
@@ -0,0 +1,61 @@
+package adapters
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func ExtractAdapterReqBidderParams(bidRequest *openrtb2.BidRequest) (map[string]json.RawMessage, error) {
+	if bidRequest == nil {
+		return nil, errors.New("error bidRequest should not be nil")
+	}
+
+	reqExt := &openrtb_ext.ExtRequest{}
+	if len(bidRequest.Ext) > 0 {
+		err := json.Unmarshal(bidRequest.Ext, &reqExt)
+		if err != nil {
+			return nil, fmt.Errorf("error decoding Request.ext : %s", err.Error())
+		}
+	}
+
+	if reqExt.Prebid.BidderParams == nil {
+		return nil, nil
+	}
+
+	var bidderParams map[string]json.RawMessage
+	err := json.Unmarshal(reqExt.Prebid.BidderParams, &bidderParams)
+	if err != nil {
+		return nil, err
+	}
+
+	return bidderParams, nil
+}
+
+func ExtractReqExtBidderParams(bidReq *openrtb2.BidRequest) (map[string]map[string]json.RawMessage, error) {
+	if bidReq == nil {
+		return nil, errors.New("error bidRequest should not be nil")
+	}
+
+	reqExt := &openrtb_ext.ExtRequest{}
+	if len(bidReq.Ext) > 0 {
+		err := json.Unmarshal(bidReq.Ext, &reqExt)
+		if err != nil {
+			return nil, fmt.Errorf("error decoding Request.ext : %s", err.Error())
+		}
+	}
+
+	if reqExt.Prebid.BidderParams == nil {
+		return nil, nil
+	}
+
+	var bidderParams map[string]map[string]json.RawMessage
+	err := json.Unmarshal(reqExt.Prebid.BidderParams, &bidderParams)
+	if err != nil {
+		return nil, err
+	}
+
+	return bidderParams, nil
+}
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index 19024f4a..799d3364 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -26,6 +26,11 @@ type pubmaticBidExt struct {
 	VideoCreativeInfo *pubmaticBidExtVideo `json:"video,omitempty"`
 }
 
+type pubmaticWrapperExt struct {
+	ProfileID int `json:"profile,omitempty"`
+	VersionID int `json:"version,omitempty"`
+}
+
 type pubmaticBidExtVideo struct {
 	Duration *int `json:"duration,omitempty"`
 }
@@ -56,16 +61,51 @@ const (
 func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
 	errs := make([]error, 0, len(request.Imp))
 
-	wrapExt := ""
 	pubID := ""
+	var wrapperExt *pubmaticWrapperExt
+	extractWrapperExtFromImp := true
+	extractPubIDFromImp := true
+
+	wrapperExt, err := extractPubmaticWrapperExtFromRequest(request)
+	if err != nil {
+		return nil, []error{err}
+	}
+	if wrapperExt != nil && wrapperExt.ProfileID != 0 && wrapperExt.VersionID != 0 {
+		extractWrapperExtFromImp = false
+	}
 
 	for i := 0; i < len(request.Imp); i++ {
-		err := parseImpressionObject(&request.Imp[i], &wrapExt, &pubID)
+		wrapperExtFromImp, pubIDFromImp, err := parseImpressionObject(&request.Imp[i], extractWrapperExtFromImp, extractPubIDFromImp)
+
 		// If the parsing is failed, remove imp and add the error.
 		if err != nil {
 			errs = append(errs, err)
 			request.Imp = append(request.Imp[:i], request.Imp[i+1:]...)
 			i--
+			continue
+		}
+
+		if extractWrapperExtFromImp {
+			if wrapperExtFromImp != nil {
+				if wrapperExt == nil {
+					wrapperExt = &pubmaticWrapperExt{}
+				}
+				if wrapperExt.ProfileID == 0 {
+					wrapperExt.ProfileID = wrapperExtFromImp.ProfileID
+				}
+				if wrapperExt.VersionID == 0 {
+					wrapperExt.VersionID = wrapperExtFromImp.VersionID
+				}
+
+				if wrapperExt != nil && wrapperExt.ProfileID != 0 && wrapperExt.VersionID != 0 {
+					extractWrapperExtFromImp = false
+				}
+			}
+		}
+
+		if extractPubIDFromImp && pubIDFromImp != "" {
+			pubID = pubIDFromImp
+			extractPubIDFromImp = false
 		}
 	}
 
@@ -74,9 +114,14 @@ func (a *PubmaticAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ad
 		return nil, errs
 	}
 
-	if wrapExt != "" {
-		rawExt := fmt.Sprintf("{\"wrapper\": %s}", wrapExt)
-		request.Ext = json.RawMessage(rawExt)
+	if wrapperExt != nil {
+		reqExt := make(map[string]interface{})
+		reqExt["wrapper"] = wrapperExt
+		rawExt, err := json.Marshal(reqExt)
+		if err != nil {
+			return nil, []error{err}
+		}
+		request.Ext = rawExt
 	}
 
 	if request.Site != nil {
@@ -179,10 +224,13 @@ func assignBannerWidthAndHeight(banner *openrtb2.Banner, w, h int64) *openrtb2.B
 }
 
 // parseImpressionObject parse the imp to get it ready to send to pubmatic
-func parseImpressionObject(imp *openrtb2.Imp, wrapExt *string, pubID *string) error {
+func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractPubIDFromImp bool) (*pubmaticWrapperExt, string, error) {
+	var wrapExt *pubmaticWrapperExt
+	var pubID string
+
 	// PubMatic supports banner and video impressions.
 	if imp.Banner == nil && imp.Video == nil {
-		return fmt.Errorf("Invalid MediaType. PubMatic only supports Banner and Video. Ignoring ImpID=%s", imp.ID)
+		return wrapExt, pubID, fmt.Errorf("Invalid MediaType. PubMatic only supports Banner and Video. Ignoring ImpID=%s", imp.ID)
 	}
 
 	if imp.Audio != nil {
@@ -191,36 +239,34 @@ func parseImpressionObject(imp *openrtb2.Imp, wrapExt *string, pubID *string) er
 
 	var bidderExt ExtImpBidderPubmatic
 	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
-		return err
+		return wrapExt, pubID, err
 	}
 
 	var pubmaticExt openrtb_ext.ExtImpPubmatic
 	if err := json.Unmarshal(bidderExt.Bidder, &pubmaticExt); err != nil {
-		return err
+		return wrapExt, pubID, err
 	}
 
-	if *pubID == "" {
-		*pubID = strings.TrimSpace(pubmaticExt.PublisherId)
+	if extractPubIDFromImp {
+		pubID = strings.TrimSpace(pubmaticExt.PublisherId)
 	}
 
 	// Parse Wrapper Extension only once per request
-	if *wrapExt == "" && len(pubmaticExt.WrapExt) != 0 {
-		var wrapExtMap map[string]int
-		err := json.Unmarshal([]byte(pubmaticExt.WrapExt), &wrapExtMap)
+	if extractWrapperExtFromImp && len(pubmaticExt.WrapExt) != 0 {
+		err := json.Unmarshal([]byte(pubmaticExt.WrapExt), &wrapExt)
 		if err != nil {
-			return fmt.Errorf("Error in Wrapper Parameters = %v  for ImpID = %v WrapperExt = %v", err.Error(), imp.ID, string(pubmaticExt.WrapExt))
+			return wrapExt, pubID, fmt.Errorf("Error in Wrapper Parameters = %v  for ImpID = %v WrapperExt = %v", err.Error(), imp.ID, string(pubmaticExt.WrapExt))
 		}
-		*wrapExt = string(pubmaticExt.WrapExt)
 	}
 
 	if err := validateAdSlot(strings.TrimSpace(pubmaticExt.AdSlot), imp); err != nil {
-		return err
+		return wrapExt, pubID, err
 	}
 
 	if imp.Banner != nil {
 		bannerCopy, err := assignBannerSize(imp.Banner)
 		if err != nil {
-			return err
+			return wrapExt, pubID, err
 		}
 		imp.Banner = bannerCopy
 	}
@@ -253,7 +299,26 @@ func parseImpressionObject(imp *openrtb2.Imp, wrapExt *string, pubID *string) er
 		}
 	}
 
-	return nil
+	return wrapExt, pubID, nil
+}
+
+// extractPubmaticWrapperExtFromRequest parse the imp to get it ready to send to pubmatic
+func extractPubmaticWrapperExtFromRequest(request *openrtb2.BidRequest) (*pubmaticWrapperExt, error) {
+	var wrpExt pubmaticWrapperExt
+	reqExtBidderParams, err := adapters.ExtractAdapterReqBidderParams(request)
+	if err != nil {
+		return nil, err
+	}
+
+	//get request ext bidder params
+	if wrapperObj, present := reqExtBidderParams["wrapper"]; present && len(wrapperObj) != 0 {
+		err = json.Unmarshal(wrapperObj, &wrpExt)
+		if err != nil {
+			return nil, err
+		}
+		return &wrpExt, nil
+	}
+	return nil, nil
 }
 
 func addKeywordsToExt(keywords []*openrtb_ext.ExtImpPubmaticKeyVal, extMap map[string]interface{}) {
diff --git a/adapters/videobyte/videobyte.go b/adapters/videobyte/videobyte.go
new file mode 100644
index 00000000..a4c0df61
--- /dev/null
+++ b/adapters/videobyte/videobyte.go
@@ -0,0 +1,158 @@
+package videobyte
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
+)
+
+type adapter struct {
+	endpoint string
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) {
+	bidder := &adapter{
+		endpoint: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	impressions := request.Imp
+	adapterRequests := make([]*adapters.RequestData, 0, len(impressions))
+	var errs []error
+
+	for _, impression := range impressions {
+		impExt, err := parseExt(&impression)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		request.Imp = []openrtb2.Imp{impression}
+		body, err := json.Marshal(request)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+
+		adapterRequests = append(adapterRequests, &adapters.RequestData{
+			Method:  http.MethodPost,
+			Uri:     a.endpoint + "?" + getParams(impExt).Encode(),
+			Body:    body,
+			Headers: getHeaders(request),
+		})
+	}
+
+	request.Imp = impressions
+	return adapterRequests, errs
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if response.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if response.StatusCode == http.StatusBadRequest {
+		return nil, []error{&errortypes.BadInput{
+			Message: fmt.Sprintf("Bad user input: HTTP status %d. Run with request.debug = 1 for more info", response.StatusCode),
+		}}
+	}
+
+	if response.StatusCode != http.StatusOK {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
+		}}
+	}
+
+	var ortbResponse openrtb2.BidResponse
+	err := json.Unmarshal(response.Body, &ortbResponse)
+	if err != nil {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: "Bad Server Response",
+		}}
+	}
+
+	impIdToImp := make(map[string]*openrtb2.Imp)
+	for i := range internalRequest.Imp {
+		imp := internalRequest.Imp[i]
+		impIdToImp[imp.ID] = &imp
+	}
+
+	bidderResponse := adapters.NewBidderResponseWithBidsCapacity(1)
+	for _, seatBid := range ortbResponse.SeatBid {
+		for i := range seatBid.Bid {
+			bid := seatBid.Bid[i]
+			bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
+				Bid:     &bid,
+				BidType: getMediaTypeForImp(impIdToImp[bid.ImpID]),
+			})
+		}
+	}
+
+	return bidderResponse, nil
+}
+
+func getMediaTypeForImp(imp *openrtb2.Imp) openrtb_ext.BidType {
+	if imp != nil && imp.Banner != nil {
+		return openrtb_ext.BidTypeBanner
+	}
+	return openrtb_ext.BidTypeVideo
+}
+
+func getParams(impExt *openrtb_ext.ExtImpVideoByte) url.Values {
+	params := url.Values{}
+	params.Add("source", "pbs")
+	params.Add("pid", impExt.PublisherId)
+	if impExt.PlacementId != "" {
+		params.Add("placementId", impExt.PlacementId)
+	}
+	if impExt.NetworkId != "" {
+		params.Add("nid", impExt.NetworkId)
+	}
+	return params
+}
+
+func getHeaders(request *openrtb2.BidRequest) http.Header {
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	if request.Site != nil {
+		if request.Site.Domain != "" {
+			headers.Add("Origin", request.Site.Domain)
+		}
+		if request.Site.Ref != "" {
+			headers.Set("Referer", request.Site.Ref)
+		}
+	}
+	return headers
+}
+
+func parseExt(imp *openrtb2.Imp) (*openrtb_ext.ExtImpVideoByte, error) {
+	var bidderExt adapters.ExtImpBidder
+
+	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
+		return nil, &errortypes.BadInput{
+			Message: fmt.Sprintf("Ignoring imp id=%s, error while decoding extImpBidder, err: %s", imp.ID, err),
+		}
+	}
+
+	impExt := openrtb_ext.ExtImpVideoByte{}
+	err := json.Unmarshal(bidderExt.Bidder, &impExt)
+	if err != nil {
+		return nil, &errortypes.BadInput{
+			Message: fmt.Sprintf("Ignoring imp id=%s, error while decoding impExt, err: %s", imp.ID, err),
+		}
+	}
+
+	return &impExt, nil
+}
diff --git a/config/bidderinfo.go b/config/bidderinfo.go
index 750604c3..12e51a3c 100644
--- a/config/bidderinfo.go
+++ b/config/bidderinfo.go
@@ -68,6 +68,9 @@ type Syncer struct {
 	// endpoint in the Prebid.js project.
 	Redirect *SyncerEndpoint `yaml:"redirect" mapstructure:"redirect"`
 
+	// ExternalURL is available as a macro to the RedirectURL template.
+	ExternalURL string `yaml:"externalUrl" mapstructure:"external_url"`
+
 	// SupportCORS identifies if CORS is supported for the user syncing endpoints.
 	SupportCORS *bool `yaml:"supportCors" mapstructure:"support_cors"`
 }
@@ -99,14 +102,14 @@ func (s *Syncer) Override(original *Syncer) *Syncer {
 
 	if original == nil {
 		copy.IFrame = s.IFrame.Override(nil)
+		copy.Redirect = s.Redirect.Override(nil)
 	} else {
 		copy.IFrame = s.IFrame.Override(original.IFrame)
+		copy.Redirect = s.Redirect.Override(original.Redirect)
 	}
 
-	if original == nil {
-		copy.Redirect = s.Redirect.Override(nil)
-	} else {
-		copy.Redirect = s.Redirect.Override(original.Redirect)
+	if s.ExternalURL != "" {
+		copy.ExternalURL = s.ExternalURL
 	}
 
 	if s.SupportCORS != nil {
@@ -162,8 +165,8 @@ type SyncerEndpoint struct {
 	// `{{.ExternalURL}}/setuid?bidder={{.SyncerKey}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&f={{.SyncType}}&uid={{.UserMacro}}`
 	RedirectURL string `yaml:"redirectUrl" mapstructure:"redirect_url"`
 
-	// ExternalURL is available as a macro to the RedirectURL template. If not specified, the host configuration
-	// value is used.
+	// ExternalURL is available as a macro to the RedirectURL template. If not specified, either the syncer configuration
+	// value or the host configuration value is used.
 	ExternalURL string `yaml:"externalUrl" mapstructure:"external_url"`
 
 	// UserMacro is available as a macro to the RedirectURL template. This value is specific to the bidder server
diff --git a/config/config.go b/config/config.go
index 83585146..84ed6109 100644
--- a/config/config.go
+++ b/config/config.go
@@ -806,6 +806,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.bidscube.endpoint", "http://supply.bidscube.com/?c=o&m=rtb")
 	v.SetDefault("adapters.bmtm.endpoint", "https://one.elitebidder.com/api/pbs")
 	v.SetDefault("adapters.brightroll.endpoint", "http://east-bid.ybp.yahoo.com/bid/appnexuspbs")
+	v.SetDefault("adapters.coinzilla.endpoint", "http://request-global.czilladx.com/serve/prebid-server.php")
 	v.SetDefault("adapters.colossus.endpoint", "http://colossusssp.com/?c=o&m=rtb")
 	v.SetDefault("adapters.connectad.endpoint", "http://bidder.connectad.io/API?src=pbs")
 	v.SetDefault("adapters.consumable.endpoint", "https://e.serverbid.com/api/v2")
@@ -895,6 +896,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.unruly.endpoint", "https://targeting.unrulymedia.com/unruly_prebid_server")
 	v.SetDefault("adapters.valueimpression.endpoint", "https://rtb.valueimpression.com/endpoint")
 	v.SetDefault("adapters.verizonmedia.disabled", true)
+	v.SetDefault("adapters.videobyte.endpoint", "https://x.videobyte.com/ortbhb")
 	v.SetDefault("adapters.viewdeos.endpoint", "http://ghb.sync.viewdeos.com/pbs/ortb")
 	v.SetDefault("adapters.visx.endpoint", "https://t.visx.net/s2s_bid?wrapperType=s2s_prebid_standard:0.1.0")
 	v.SetDefault("adapters.vrtcal.endpoint", "http://rtb.vrtcal.com/bidder_prebid.vap?ssp=1804")
@@ -1107,6 +1109,7 @@ func setBidderDefaults(v *viper.Viper, bidder string) {
 	v.BindEnv(adapterCfgPrefix + ".usersync.redirect.redirect_url")
 	v.BindEnv(adapterCfgPrefix + ".usersync.redirect.external_url")
 	v.BindEnv(adapterCfgPrefix + ".usersync.redirect.user_macro")
+	v.BindEnv(adapterCfgPrefix + ".usersync.external_url")
 	v.BindEnv(adapterCfgPrefix + ".usersync.support_cors")
 }
 
diff --git a/currency/validation.go b/currency/validation.go
new file mode 100644
index 00000000..7a0e2aa0
--- /dev/null
+++ b/currency/validation.go
@@ -0,0 +1,34 @@
+package currency
+
+import (
+	"fmt"
+
+	"golang.org/x/text/currency"
+
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// ValidateCustomRates throws a bad input error if any of the 3-digit currency codes found in
+// the bidRequest.ext.prebid.currency field is invalid, malfomed or does not represent any actual
+// currency. No error is thrown if bidRequest.ext.prebid.currency is invalid or empty.
+func ValidateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) error {
+	if bidReqCurrencyRates == nil {
+		return nil
+	}
+
+	for fromCurrency, rates := range bidReqCurrencyRates.ConversionRates {
+		// Check if fromCurrency is a valid 3-letter currency code
+		if _, err := currency.ParseISO(fromCurrency); err != nil {
+			return &errortypes.BadInput{Message: fmt.Sprintf("currency code %s is not recognized or malformed", fromCurrency)}
+		}
+
+		// Check if currencies mapped to fromCurrency are valid 3-letter currency codes
+		for toCurrency := range rates {
+			if _, err := currency.ParseISO(toCurrency); err != nil {
+				return &errortypes.BadInput{Message: fmt.Sprintf("currency code %s is not recognized or malformed", toCurrency)}
+			}
+		}
+	}
+	return nil
+}
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index ec9e4152..2b05e9aa 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -23,8 +23,10 @@ import (
 	nativeRequests "github.com/mxmCherry/openrtb/v15/native1/request"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	accountService "github.com/prebid/prebid-server/account"
+	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/exchange"
 	"github.com/prebid/prebid-server/metrics"
@@ -39,7 +41,6 @@ import (
 	"github.com/prebid/prebid-server/util/iputil"
 	"github.com/prebid/prebid-server/util/uuidutil"
 	"golang.org/x/net/publicsuffix"
-	"golang.org/x/text/currency"
 )
 
 const storedRequestTimeoutMillis = 50
@@ -290,6 +291,11 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_
 		return
 	}
 
+	if err := mergeBidderParams(req); err != nil {
+		errs = []error{err}
+		return
+	}
+
 	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
 	deps.setFieldsImplicitly(httpRequest, req.BidRequest)
 
@@ -323,6 +329,117 @@ func parseTimeout(requestJson []byte, defaultTimeout time.Duration) time.Duratio
 	return defaultTimeout
 }
 
+// mergeBidderParams merges bidder parameters passed at req.ext level with imp[].ext level.
+// Preference is given to parameters at imp[].ext level over req.ext level.
+// Parameters at req.ext level are propagated to adapters as is without any validation.
+func mergeBidderParams(req *openrtb_ext.RequestWrapper) error {
+	reqBidderParams, err := adapters.ExtractReqExtBidderParams(req.BidRequest)
+	if err != nil {
+		return err
+	}
+
+	impCpy := make([]openrtb2.Imp, 0, len(req.BidRequest.Imp))
+	for _, imp := range req.BidRequest.Imp {
+		updatedImp := imp
+
+		if len(imp.Ext) == 0 {
+			impCpy = append(impCpy, updatedImp)
+			continue
+		}
+
+		var impExt map[string]map[string]json.RawMessage
+		err := json.Unmarshal(imp.Ext, &impExt)
+		if err != nil {
+			return err
+		}
+
+		//merges bidder parameters passed at req.ext level with imp[].ext level.
+		err = addMissingReqExtParamsInImpExt(impExt, reqBidderParams)
+		if err != nil {
+			return err
+		}
+
+		//merges bidder parameters passed at req.ext level with imp[].ext.prebid.bidder level.
+		err = addMissingReqExtParamsInImpExtPrebid(impExt, reqBidderParams)
+		if err != nil {
+			return err
+		}
+
+		iExt, err := json.Marshal(impExt)
+		if err != nil {
+			return fmt.Errorf("error marshalling imp[].ext : %s", err.Error())
+		}
+		updatedImp.Ext = iExt
+		impCpy = append(impCpy, updatedImp)
+	}
+
+	req.BidRequest.Imp = impCpy
+	return nil
+}
+
+// addMissingReqExtParamsInImpExtPrebid merges bidder parameters passed at req.ext level with imp[].ext.prebid.bidder level.
+func addMissingReqExtParamsInImpExtPrebid(impExtBidder map[string]map[string]json.RawMessage, reqExtParams map[string]map[string]json.RawMessage) error {
+	var bidderParams map[string]json.RawMessage
+	if impExtBidder["prebid"] != nil && impExtBidder["prebid"]["bidder"] != nil {
+		err := json.Unmarshal(impExtBidder["prebid"]["bidder"], &bidderParams)
+		if err != nil {
+			return err
+		}
+	}
+
+	if len(bidderParams) != 0 {
+		for bidder, bidderExt := range bidderParams {
+			if !isBidderToValidate(bidder) {
+				continue
+			}
+
+			var params map[string]json.RawMessage
+			err := json.Unmarshal(bidderExt, &params)
+
+			for key, value := range reqExtParams[bidder] {
+				if _, present := params[key]; !present {
+					params[key] = value
+				}
+			}
+
+			paramsJson, err := json.Marshal(params)
+			if err != nil {
+				return err
+			}
+			bidderParams[bidder] = paramsJson
+		}
+
+		bidderParamsJson, err := json.Marshal(bidderParams)
+		if err != nil {
+			return err
+		}
+		impExtBidder["prebid"]["bidder"] = bidderParamsJson
+	}
+
+	return nil
+}
+
+// addMissingReqExtParamsInImpExt merges bidder parameters passed at req.ext level with imp[].ext level.
+func addMissingReqExtParamsInImpExt(impExtBidder map[string]map[string]json.RawMessage, reqExtParams map[string]map[string]json.RawMessage) error {
+	for bidder, bidderExt := range impExtBidder {
+		if !isBidderToValidate(bidder) {
+			continue
+		}
+
+		wasModified := false
+		for key, value := range reqExtParams[bidder] {
+			if _, present := bidderExt[key]; !present {
+				bidderExt[key] = value
+				wasModified = true
+			}
+		}
+		if wasModified {
+			impExtBidder[bidder] = bidderExt
+		}
+	}
+	return nil
+}
+
 func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper) []error {
 	errL := []error{}
 	if req.ID == "" {
@@ -377,7 +494,7 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper) []err
 			return []error{err}
 		}
 
-		if err := validateCustomRates(reqPrebid.CurrencyConversions); err != nil {
+		if err := currency.ValidateCustomRates(reqPrebid.CurrencyConversions); err != nil {
 			return []error{err}
 		}
 	}
@@ -475,30 +592,6 @@ func validateSChains(sChains []*openrtb_ext.ExtRequestPrebidSChain) error {
 	return err
 }
 
-// validateCustomRates throws a bad input error if any of the 3-digit currency codes found in
-// the bidRequest.ext.prebid.currency field is invalid, malfomed or does not represent any actual
-// currency. No error is thrown if bidRequest.ext.prebid.currency is invalid or empty.
-func validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) error {
-	if bidReqCurrencyRates == nil {
-		return nil
-	}
-
-	for fromCurrency, rates := range bidReqCurrencyRates.ConversionRates {
-		// Check if fromCurrency is a valid 3-letter currency code
-		if _, err := currency.ParseISO(fromCurrency); err != nil {
-			return &errortypes.BadInput{Message: fmt.Sprintf("currency code %s is not recognized or malformed", fromCurrency)}
-		}
-
-		// Check if currencies mapped to fromCurrency are valid 3-letter currency codes
-		for toCurrency := range rates {
-			if _, err := currency.ParseISO(toCurrency); err != nil {
-				return &errortypes.BadInput{Message: fmt.Sprintf("currency code %s is not recognized or malformed", toCurrency)}
-			}
-		}
-	}
-	return nil
-}
-
 func (deps *endpointDeps) validateEidPermissions(prebid *openrtb_ext.ExtRequestPrebidData, aliases map[string]string) error {
 	if prebid == nil {
 		return nil
@@ -1077,6 +1170,8 @@ func isBidderToValidate(bidder string) bool {
 		return false
 	case openrtb_ext.BidderReservedSKAdN:
 		return false
+	case openrtb_ext.BidderReservedBidder:
+		return false
 	default:
 		return true
 	}
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json b/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json
index 76dc1c2b..5baaa09d 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/cache-nothing.json
@@ -27,5 +27,5 @@
     }
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.ext is invalid: request.ext.prebid.cache requires one of the \"bids\" or \"vastxml\" properties\n"
+  "expectedErrorMessage": "Invalid request: error decoding Request.ext : request.ext.prebid.cache requires one of the \"bids\" or \"vastxml\" properties\n"
 }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-invalid-params.json b/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-invalid-params.json
index d9ed59c8..b1e9b784 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-invalid-params.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/imp-ext-invalid-params.json
@@ -20,5 +20,5 @@
     }
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.imp[0].ext.appnexus failed validation.\n(root): Invalid type. Expected: object, given: string\n"
+  "expectedErrorMessage": "Invalid request: json: cannot unmarshal string into Go value of type map[string]json.RawMessage"
 }
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json
new file mode 100644
index 00000000..274c2c70
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-backward-compatible-merge.json
@@ -0,0 +1,101 @@
+{
+  "description": "add missing bidder-params from req.ext.prebid.bidderparams to imp[].ext for backward compatibility",
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "pmp": {
+          "deals": [
+            {
+              "id": "some-deal-id"
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 111111
+          },
+          "pubmatic": {
+            "wrapper": {
+              "profile": 1234
+            }
+          },
+          "prebid": {
+            "bidder": {}
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "targeting": {
+          "pricegranularity": "low"
+        },
+        "cache": {
+          "bids": {}
+        },
+        "bidderparams": {
+          "appnexus": {
+            "placementId": 222222
+          },
+          "pubmatic": {
+            "publisherId": "5678",
+            "wrapper": {
+              "profile": 5678,
+              "version": 2
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedReqExt": {
+    "prebid": {
+      "targeting": {
+        "pricegranularity": "low"
+      },
+      "cache": {
+        "bids": {}
+      },
+      "bidderparams": {
+        "appnexus": {
+          "placementId": 222222
+        },
+        "pubmatic": {
+          "publisherId": "5678",
+          "wrapper": {
+            "profile": 5678,
+            "version": 2
+          }
+        }
+      }
+    }
+  },
+  "expectedImpExt": {
+    "appnexus": {
+      "placementId": 111111
+    },
+    "pubmatic": {
+      "publisherId": "5678",
+      "wrapper": {
+        "profile": 1234
+      }
+    },
+    "prebid": {
+      "bidder": {}
+    }
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge-reserved-keyword.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge-reserved-keyword.json
new file mode 100644
index 00000000..982f86cb
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge-reserved-keyword.json
@@ -0,0 +1,116 @@
+{
+  "description": "add missing bidder-params from req.ext.prebid.bidderparams to imp[].ext.prebid.bidder for reserved keyword",
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "pmp": {
+          "deals": [
+            {
+              "id": "some-deal-id"
+            }
+          ]
+        },
+        "ext": {
+          "prebid": {
+            "bidder": {
+              "bidder": {
+                "param1": 111111
+              },
+              "appnexus": {
+                "placementId": 111111
+              },
+              "pubmatic": {
+                "publisherId": "1234",
+                "wrapper": {
+                  "profile": 1234
+                }
+              }
+            }
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "targeting": {
+          "pricegranularity": "low"
+        },
+        "cache": {
+          "bids": {}
+        },
+        "bidderparams": {
+          "bidder": {
+            "param2": 111111
+          },
+          "appnexus": {
+            "placementId": 222222
+          },
+          "pubmatic": {
+            "publisherId": "5678",
+            "wrapper": {
+              "profile": 5678,
+              "version": 2
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedReqExt": {
+    "prebid": {
+      "targeting": {
+        "pricegranularity": "low"
+      },
+      "cache": {
+        "bids": {}
+      },
+      "bidderparams": {
+        "bidder": {
+          "param2": 111111
+        },
+        "appnexus": {
+          "placementId": 222222
+        },
+        "pubmatic": {
+          "publisherId": "5678",
+          "wrapper": {
+            "profile": 5678,
+            "version": 2
+          }
+        }
+      }
+    }
+  },
+  "expectedImpExt": {
+    "prebid": {
+      "bidder": {
+        "bidder": {
+          "param1": 111111
+        },
+        "appnexus": {
+          "placementId": 111111
+        },
+        "pubmatic": {
+          "publisherId": "1234",
+          "wrapper": {
+            "profile": 1234
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json
new file mode 100644
index 00000000..22026f56
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params-merge.json
@@ -0,0 +1,104 @@
+{
+  "description": "add missing bidder-params from req.ext.prebid.bidderparams to imp[].ext.prebid.bidder with preference for imp[].ext.prebid.bidder params",
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "pmp": {
+          "deals": [
+            {
+              "id": "some-deal-id"
+            }
+          ]
+        },
+        "ext": {
+          "prebid": {
+            "bidder": {
+              "appnexus": {
+                "placementId": 111111
+              },
+              "pubmatic": {
+                "publisherId": "1234",
+                "wrapper": {
+                  "profile": 1234
+                }
+              }
+            }
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "targeting": {
+          "pricegranularity": "low"
+        },
+        "cache": {
+          "bids": {}
+        },
+        "bidderparams": {
+          "appnexus": {
+            "placementId": 222222
+          },
+          "pubmatic": {
+            "publisherId": "5678",
+            "wrapper": {
+              "profile": 5678,
+              "version": 2
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedReqExt": {
+    "prebid": {
+      "targeting": {
+        "pricegranularity": "low"
+      },
+      "cache": {
+        "bids": {}
+      },
+      "bidderparams": {
+        "appnexus": {
+          "placementId": 222222
+        },
+        "pubmatic": {
+          "publisherId": "5678",
+          "wrapper": {
+            "profile": 5678,
+            "version": 2
+          }
+        }
+      }
+    }
+  },
+  "expectedImpExt": {
+    "prebid": {
+      "bidder": {
+        "appnexus": {
+          "placementId": 111111
+        },
+        "pubmatic": {
+          "publisherId": "1234",
+          "wrapper": {
+            "profile": 1234
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json
new file mode 100644
index 00000000..9d4bcd0d
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/valid-whole/supplementary/req-ext-bidder-params.json
@@ -0,0 +1,98 @@
+{
+  "description": "add missing bidder-params from req.ext.prebid.bidderparams to imp[].ext.prebid.bidder",
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "test.somepage.com"
+    },
+    "imp": [
+      {
+        "id": "my-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "pmp": {
+          "deals": [
+            {
+              "id": "some-deal-id"
+            }
+          ]
+        },
+        "ext": {
+          "prebid": {
+            "bidder": {
+              "appnexus": {},
+              "pubmatic": {}
+            }
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "targeting": {
+          "pricegranularity": "low"
+        },
+        "cache": {
+          "bids": {}
+        },
+        "bidderparams": {
+          "appnexus": {
+            "placementId": 12883451
+          },
+          "pubmatic": {
+            "publisherId": "1234",
+            "wrapper": {
+              "profile": 1234,
+              "version": 2
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedReqExt": {
+    "prebid": {
+      "targeting": {
+        "pricegranularity": "low"
+      },
+      "cache": {
+        "bids": {}
+      },
+      "bidderparams": {
+        "appnexus": {
+          "placementId": 12883451
+        },
+        "pubmatic": {
+          "publisherId": "1234",
+          "wrapper": {
+            "profile": 1234,
+            "version": 2
+          }
+        }
+      }
+    }
+  },
+  "expectedImpExt": {
+    "prebid": {
+      "bidder": {
+        "appnexus": {
+          "placementId": 12883451
+        },
+        "pubmatic": {
+          "publisherId": "1234",
+          "wrapper": {
+            "profile": 1234,
+            "version": 2
+          }
+        }
+      }
+    }
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 33d662c2..29faaca8 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -41,6 +41,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/bidscube"
 	"github.com/prebid/prebid-server/adapters/bmtm"
 	"github.com/prebid/prebid-server/adapters/brightroll"
+	"github.com/prebid/prebid-server/adapters/coinzilla"
 	"github.com/prebid/prebid-server/adapters/colossus"
 	"github.com/prebid/prebid-server/adapters/connectad"
 	"github.com/prebid/prebid-server/adapters/consumable"
@@ -121,6 +122,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/unicorn"
 	"github.com/prebid/prebid-server/adapters/unruly"
 	"github.com/prebid/prebid-server/adapters/valueimpression"
+	"github.com/prebid/prebid-server/adapters/videobyte"
 	"github.com/prebid/prebid-server/adapters/visx"
 	"github.com/prebid/prebid-server/adapters/vrtcal"
 	"github.com/prebid/prebid-server/adapters/yahoossp"
@@ -177,6 +179,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderBidsCube:          bidscube.Builder,
 		openrtb_ext.BidderBmtm:              bmtm.Builder,
 		openrtb_ext.BidderBrightroll:        brightroll.Builder,
+		openrtb_ext.BidderCoinzilla:         coinzilla.Builder,
 		openrtb_ext.BidderColossus:          colossus.Builder,
 		openrtb_ext.BidderConnectAd:         connectad.Builder,
 		openrtb_ext.BidderConsumable:        consumable.Builder,
@@ -261,6 +264,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderUnruly:            unruly.Builder,
 		openrtb_ext.BidderValueImpression:   valueimpression.Builder,
 		openrtb_ext.BidderVerizonMedia:      yahoossp.Builder,
+		openrtb_ext.BidderVideoByte:         videobyte.Builder,
 		openrtb_ext.BidderViewdeos:          adtelligent.Builder,
 		openrtb_ext.BidderVisx:              visx.Builder,
 		openrtb_ext.BidderVrtcal:            vrtcal.Builder,
diff --git a/exchange/utils.go b/exchange/utils.go
index 5a68ba45..23ffe5e5 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -11,6 +11,7 @@ import (
 	"github.com/prebid/go-gdpr/vendorconsent"
 
 	"github.com/buger/jsonparser"
+	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/firstpartydata"
 	"github.com/prebid/prebid-server/gdpr"
@@ -222,6 +223,11 @@ func getAuctionBidderRequests(req AuctionRequest,
 		return nil, []error{err}
 	}
 
+	bidderParamsInReqExt, err := adapters.ExtractReqExtBidderParams(req.BidRequest)
+	if err != nil {
+		return nil, []error{err}
+	}
+
 	var sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain
 
 	// Quick extra wrapper until RequestWrapper makes its way into CleanRequests
@@ -232,21 +238,33 @@ func getAuctionBidderRequests(req AuctionRequest,
 		}
 	}
 
-	reqExt, err := getExtJson(req.BidRequest, requestExt)
-	if err != nil {
-		return nil, []error{err}
-	}
-
 	var errs []error
 	for bidder, imps := range impsByBidder {
 		coreBidder := resolveBidder(bidder, aliases)
 
 		reqCopy := *req.BidRequest
 		reqCopy.Imp = imps
-		reqCopy.Ext = reqExt
 
 		prepareSource(&reqCopy, bidder, sChainsByBidder)
 
+		if len(bidderParamsInReqExt) != 0 {
+
+			// Update bidder-params(requestExt.Prebid.BidderParams) for the bidder to only contain bidder-params for
+			// this bidder only and remove bidder-params for all other bidders from requestExt.Prebid.BidderParams
+			params, err := getBidderParamsForBidder(bidderParamsInReqExt, bidder)
+			if err != nil {
+				return nil, []error{err}
+			}
+
+			requestExt.Prebid.BidderParams = params
+		}
+
+		reqExt, err := getExtJson(req.BidRequest, requestExt)
+		if err != nil {
+			return nil, []error{err}
+		}
+		reqCopy.Ext = reqExt
+
 		if err := removeUnpermissionedEids(&reqCopy, bidder, requestExt); err != nil {
 			errs = append(errs, fmt.Errorf("unable to enforce request.ext.prebid.data.eidpermissions because %v", err))
 			continue
@@ -278,6 +296,18 @@ func getAuctionBidderRequests(req AuctionRequest,
 	return bidderRequests, errs
 }
 
+func getBidderParamsForBidder(bidderParamsInReqExt map[string]map[string]json.RawMessage, bidder string) (json.RawMessage, error) {
+	var params json.RawMessage
+	if bidderParams, ok := bidderParamsInReqExt[bidder]; ok {
+		var err error
+		params, err = json.Marshal(bidderParams)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return params, nil
+}
+
 func getExtJson(req *openrtb2.BidRequest, unpackedExt *openrtb_ext.ExtRequest) (json.RawMessage, error) {
 	if len(req.Ext) == 0 || unpackedExt == nil {
 		return json.RawMessage(``), nil
@@ -309,6 +339,9 @@ func prepareSource(req *openrtb2.BidRequest, bidder string, sChainsByBidder map[
 	// set source
 	if req.Source == nil {
 		req.Source = &openrtb2.Source{}
+	} else {
+		sourceCopy := *req.Source
+		req.Source = &sourceCopy
 	}
 	schain := openrtb_ext.ExtRequestPrebidSChain{
 		SChain: *selectedSChain,
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index d5c424c5..770811a9 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -34,6 +34,7 @@ const (
 	BidderReservedGeneral BidderName = "general" // Reserved for non-bidder specific messages when using a map keyed on the bidder name.
 	BidderReservedPrebid  BidderName = "prebid"  // Reserved for Prebid Server configuration.
 	BidderReservedSKAdN   BidderName = "skadn"   // Reserved for Apple's SKAdNetwork OpenRTB extension.
+	BidderReservedBidder  BidderName = "bidder"  // Reserved for passing bidder parameters.
 )
 
 // IsBidderNameReserved returns true if the specified name is a case insensitive match for a reserved bidder name.
@@ -62,6 +63,10 @@ func IsBidderNameReserved(name string) bool {
 		return true
 	}
 
+	if strings.EqualFold(name, string(BidderReservedBidder)) {
+		return true
+	}
+
 	return false
 }
 
@@ -111,6 +116,7 @@ const (
 	BidderBidsCube          BidderName = "bidscube"
 	BidderBmtm              BidderName = "bmtm"
 	BidderBrightroll        BidderName = "brightroll"
+	BidderCoinzilla         BidderName = "coinzilla"
 	BidderColossus          BidderName = "colossus"
 	BidderConnectAd         BidderName = "connectad"
 	BidderConsumable        BidderName = "consumable"
@@ -195,6 +201,7 @@ const (
 	BidderUnruly            BidderName = "unruly"
 	BidderValueImpression   BidderName = "valueimpression"
 	BidderVerizonMedia      BidderName = "verizonmedia"
+	BidderVideoByte         BidderName = "videobyte"
 	BidderVisx              BidderName = "visx"
 	BidderViewdeos          BidderName = "viewdeos"
 	BidderVrtcal            BidderName = "vrtcal"
@@ -250,6 +257,7 @@ func CoreBidderNames() []BidderName {
 		BidderBidsCube,
 		BidderBmtm,
 		BidderBrightroll,
+		BidderCoinzilla,
 		BidderColossus,
 		BidderConnectAd,
 		BidderConsumable,
@@ -334,6 +342,7 @@ func CoreBidderNames() []BidderName {
 		BidderUnruly,
 		BidderValueImpression,
 		BidderVerizonMedia,
+		BidderVideoByte,
 		BidderViewdeos,
 		BidderVisx,
 		BidderVrtcal,
diff --git a/openrtb_ext/imp_adf.go b/openrtb_ext/imp_adf.go
index 6a8f9afa..e4f5283b 100644
--- a/openrtb_ext/imp_adf.go
+++ b/openrtb_ext/imp_adf.go
@@ -8,4 +8,5 @@ type ExtImpAdf struct {
 	MasterTagID       json.Number `json:"mid,omitempty"`
 	InventorySourceID int         `json:"inv,omitempty"`
 	PlacementName     string      `json:"mname,omitempty"`
+	PriceType         string      `json:"priceType,omitempty"`
 }
diff --git a/openrtb_ext/imp_videobyte.go b/openrtb_ext/imp_videobyte.go
new file mode 100644
index 00000000..9101e9f2
--- /dev/null
+++ b/openrtb_ext/imp_videobyte.go
@@ -0,0 +1,8 @@
+package openrtb_ext
+
+// ExtImpVideoByte defines the contract for bidrequest.imp[i].ext.videobyte
+type ExtImpVideoByte struct {
+	PublisherId string `json:"pubId"`
+	PlacementId string `json:"placementId"`
+	NetworkId   string `json:"nid"`
+}
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index d399715f..a65e8d3c 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -37,6 +37,7 @@ type ExtRequestPrebid struct {
 	StoredRequest        *ExtStoredRequest         `json:"storedrequest,omitempty"`
 	SupportDeals         bool                      `json:"supportdeals,omitempty"`
 	Targeting            *ExtRequestTargeting      `json:"targeting,omitempty"`
+	BidderParams         json.RawMessage           `json:"bidderparams,omitempty"`
 
 	// NoSale specifies bidders with whom the publisher has a legal relationship where the
 	// passing of personally identifiable information doesn't constitute a sale per CCPA law.
diff --git a/static/bidder-info/33across.yaml b/static/bidder-info/33across.yaml
index 1ee5366f..bdda5a7e 100644
--- a/static/bidder-info/33across.yaml
+++ b/static/bidder-info/33across.yaml
@@ -8,5 +8,5 @@ capabilities:
     - video
 userSync:
   iframe:
-    url: "https://ic.tynt.com/r/d?m=xch&rt=html&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&ru={{.RedirectURL}}&id=zzz000000000002zzz"
+    url: "https://ssc-cms.33across.com/ps/?m=xch&rt=html&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&ru={{.RedirectURL}}&id=zzz000000000002zzz"
     userMacro: "33XUSERID33X"
\ No newline at end of file
diff --git a/static/bidder-info/adyoulike.yaml b/static/bidder-info/adyoulike.yaml
index a8769ddd..0853cf10 100644
--- a/static/bidder-info/adyoulike.yaml
+++ b/static/bidder-info/adyoulike.yaml
@@ -9,6 +9,10 @@ capabilities:
       - video
       - native
 userSync:
+  default: iframe
+  iframe:
+    url: "http://visitor.omnitagjs.com/visitor/isync?uid=19340f4f097d16f41f34fc0274981ca4&name=PrebidServer&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url={{.RedirectURL}}"
+    userMacro: "[BUYER_USERID]"
   redirect:
     url: "http://visitor.omnitagjs.com/visitor/bsync?uid=19340f4f097d16f41f34fc0274981ca4&name=PrebidServer&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&url={{.RedirectURL}}"
     userMacro: "[BUYER_USERID]"
diff --git a/static/bidder-info/coinzilla.yaml b/static/bidder-info/coinzilla.yaml
new file mode 100644
index 00000000..dc63d32c
--- /dev/null
+++ b/static/bidder-info/coinzilla.yaml
@@ -0,0 +1,10 @@
+maintainer:
+  email: "technical@sevio.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+  site:
+    mediaTypes:
+      - banner
+
diff --git a/static/bidder-info/openx.yaml b/static/bidder-info/openx.yaml
index c48d7de2..4b456014 100644
--- a/static/bidder-info/openx.yaml
+++ b/static/bidder-info/openx.yaml
@@ -12,6 +12,10 @@ capabilities:
       - video
 modifyingVastXmlAllowed: true
 userSync:
+  default: "iframe"
+  iframe:
+    url: "https://u.openx.net/w/1.0/cm?id=891039ac-a916-42bb-a651-4be9e3b201da&ph=a3aece0c-9e80-4316-8deb-faf804779bd1&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r={{.RedirectURL}}"
+    userMacro: "{OPENX_ID}"
   redirect:
     url: "https://rtb.openx.net/sync/prebid?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r={{.RedirectURL}}"
     userMacro: "${UID}"
diff --git a/static/bidder-info/unruly.yaml b/static/bidder-info/unruly.yaml
index 7e018a0a..9b1ba9c6 100644
--- a/static/bidder-info/unruly.yaml
+++ b/static/bidder-info/unruly.yaml
@@ -11,6 +11,6 @@ capabilities:
       - banner
       - video
 userSync:
-  iframe:
-    url: "https://usermatch.targeting.unrulymedia.com/pbsync?gdpr={{.GDPR}}&consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&rurl={{.RedirectURL}}"
-    userMacro: "$UID"
+  redirect:
+    url: "https://sync.1rx.io/usersync2/rmphb?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: "[RX_UUID]"
diff --git a/static/bidder-info/videobyte.yaml b/static/bidder-info/videobyte.yaml
new file mode 100644
index 00000000..fc0e767e
--- /dev/null
+++ b/static/bidder-info/videobyte.yaml
@@ -0,0 +1,16 @@
+maintainer:
+  email: "prebid@videobyte.com"
+gvlVendorID: 1011
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
+userSync:
+  redirect:
+    url: "https://x.videobyte.com/usync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb={{.RedirectURL}}"
+    userMacro: "$UID"
diff --git a/static/bidder-params/adf.json b/static/bidder-params/adf.json
index 4a1a1182..e165fa85 100644
--- a/static/bidder-params/adf.json
+++ b/static/bidder-params/adf.json
@@ -16,6 +16,11 @@
     "mname": {
       "type": ["string"],
       "description": "A Name which identifies the placement selling the impression"
+    },
+    "priceType": {
+      "type": ["string"],
+      "description": "gross or net. Default is net.",
+      "pattern": "gross|net"
     }
   },
   "anyOf":[
diff --git a/static/bidder-params/adform.json b/static/bidder-params/adform.json
index 906241ff..e112f122 100644
--- a/static/bidder-params/adform.json
+++ b/static/bidder-params/adform.json
@@ -16,6 +16,11 @@
     "mname": {
       "type": ["string"],
       "description": "A Name which identifies the placement selling the impression"
+    },
+    "priceType": {
+      "type": ["string"],
+      "description": "gross or net. Default is net.",
+      "pattern": "gross|net"
     }
   },
   "anyOf":[
diff --git a/static/bidder-params/coinzilla.json b/static/bidder-params/coinzilla.json
new file mode 100644
index 00000000..8268999f
--- /dev/null
+++ b/static/bidder-params/coinzilla.json
@@ -0,0 +1,14 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Coinzilla Adapter Params",
+  "description": "A schema which validates params accepted by the Coinzilla adapter",
+  "type": "object",
+  "properties": {
+    "placementId": {
+      "type": "string",
+      "minLength": 1,
+      "description": "Placement Id or Website Id"
+    }
+  },
+  "required": ["placementId"]
+}
diff --git a/static/bidder-params/videobyte.json b/static/bidder-params/videobyte.json
new file mode 100644
index 00000000..a9388e26
--- /dev/null
+++ b/static/bidder-params/videobyte.json
@@ -0,0 +1,22 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "VideoByte Adapter Params",
+  "description": "A schema which validates params accepted by the VideoByte adapter",
+
+  "type": "object",
+  "properties": {
+    "pubId": {
+      "type": "string",
+      "description": "Publisher ID"
+    },
+    "placementId": {
+      "type": "string",
+      "description": "Placement ID"
+    },
+    "nid": {
+      "type": "string",
+      "description": "Network ID"
+    }
+  },
+  "required": ["pubId"]
+}
diff --git a/usersync/syncer.go b/usersync/syncer.go
index 2db220a2..c944cf0b 100644
--- a/usersync/syncer.go
+++ b/usersync/syncer.go
@@ -86,7 +86,7 @@ func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer) (Syncer,
 
 	if syncerConfig.IFrame != nil {
 		var err error
-		syncer.iframe, err = buildTemplate(syncerConfig.Key, setuidSyncTypeIFrame, hostConfig, *syncerConfig.IFrame)
+		syncer.iframe, err = buildTemplate(syncerConfig.Key, setuidSyncTypeIFrame, hostConfig, syncerConfig.ExternalURL, *syncerConfig.IFrame)
 		if err != nil {
 			return nil, fmt.Errorf("iframe %v", err)
 		}
@@ -97,7 +97,7 @@ func NewSyncer(hostConfig config.UserSync, syncerConfig config.Syncer) (Syncer,
 
 	if syncerConfig.Redirect != nil {
 		var err error
-		syncer.redirect, err = buildTemplate(syncerConfig.Key, setuidSyncTypeRedirect, hostConfig, *syncerConfig.Redirect)
+		syncer.redirect, err = buildTemplate(syncerConfig.Key, setuidSyncTypeRedirect, hostConfig, syncerConfig.ExternalURL, *syncerConfig.Redirect)
 		if err != nil {
 			return nil, fmt.Errorf("redirect %v", err)
 		}
@@ -147,16 +147,13 @@ var (
 	macroRegex             = regexp.MustCompile(`{{\s*\..*?\s*}}`)
 )
 
-func buildTemplate(key, syncTypeValue string, hostConfig config.UserSync, syncerEndpoint config.SyncerEndpoint) (*template.Template, error) {
+func buildTemplate(key, syncTypeValue string, hostConfig config.UserSync, syncerExternalURL string, syncerEndpoint config.SyncerEndpoint) (*template.Template, error) {
 	redirectTemplate := syncerEndpoint.RedirectURL
 	if redirectTemplate == "" {
 		redirectTemplate = hostConfig.RedirectURL
 	}
 
-	externalURL := syncerEndpoint.ExternalURL
-	if externalURL == "" {
-		externalURL = hostConfig.ExternalURL
-	}
+	externalURL := chooseExternalURL(syncerEndpoint.ExternalURL, syncerExternalURL, hostConfig.ExternalURL)
 
 	redirectURL := macroRegexSyncerKey.ReplaceAllLiteralString(redirectTemplate, key)
 	redirectURL = macroRegexSyncType.ReplaceAllLiteralString(redirectURL, syncTypeValue)
@@ -170,6 +167,19 @@ func buildTemplate(key, syncTypeValue string, hostConfig config.UserSync, syncer
 	return template.New(templateName).Parse(url)
 }
 
+// chooseExternalURL selects the external url to use for the template, where the most specific config wins.
+func chooseExternalURL(syncerEndpointURL, syncerURL, hostConfigURL string) string {
+	if syncerEndpointURL != "" {
+		return syncerEndpointURL
+	}
+
+	if syncerURL != "" {
+		return syncerURL
+	}
+
+	return hostConfigURL
+}
+
 // escapeTemplate url encodes a string template leaving the macro tags unaffected.
 func escapeTemplate(x string) string {
 	escaped := strings.Builder{}


Test patch:
diff --git a/adapters/33across/33acrosstest/exemplary/multi-imp-banner.json b/adapters/33across/33acrosstest/exemplary/multi-imp-banner.json
index b6d19f55..db03a001 100644
--- a/adapters/33across/33acrosstest/exemplary/multi-imp-banner.json
+++ b/adapters/33across/33acrosstest/exemplary/multi-imp-banner.json
@@ -25,6 +25,30 @@
             "productId": "inview"
           }
         }
+      },
+      {
+        "id": "test-imp-id3",
+        "banner": {
+          "format": [{"w": 728, "h": 90}]
+        },
+        "ext": {
+          "bidder": {
+            "siteId": "fake-site-id1",
+            "productId": "inview"
+          }
+        }
+      },
+      {
+        "id": "test-imp-id4",
+        "banner": {
+          "format": [{"w": 728, "h": 90}]
+        },
+        "ext": {
+          "bidder": {
+            "siteId": "fake-site-id",
+            "productId": "siab"
+          }
+        }
       }
     ],
     "site": {}
@@ -58,6 +82,18 @@
                   "zoneid": "fake-site-id"
                 }
               }
+            },
+            {
+              "id":"test-imp-id2",
+              "banner": {
+                "format": [{"w": 728, "h": 90}]
+              },
+              "ext": {
+                "ttx": {
+                  "prod": "inview",
+                  "zoneid": "fake-site-id"
+                }
+              }
             }
           ],
           "site": {}
@@ -70,7 +106,8 @@
           "seatbid": [
             {
               "seat": "ttx",
-              "bid": [{
+              "bid": [
+                {
                 "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
                 "impid": "test-imp-id1",
                 "price": 0.500000,
@@ -83,7 +120,22 @@
                     "mediaType": "banner"
                   }
                 }
-              }]
+                },
+                {
+                  "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                  "impid": "test-imp-id2",
+                  "price": 0.600000,
+                  "adm": "some-test-ad",
+                  "crid": "crid_10",
+                  "h": 90,
+                  "w": 728,
+                  "ext": {
+                    "ttx": {
+                      "mediaType": "banner"
+                    }
+                  }
+                }
+              ]
             }
           ],
           "cur": "USD"
@@ -107,13 +159,74 @@
           "id": "test-request-id",
           "imp": [
             {
-              "id":"test-imp-id2",
+              "id":"test-imp-id3",
               "banner": {
                 "format": [{"w": 728, "h": 90}]
               },
               "ext": {
                 "ttx": {
                   "prod": "inview",
+                  "zoneid": "fake-site-id1"
+                }
+              }
+            }
+          ],
+          "site": {}
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "ttx",
+              "bid": [
+                {
+                  "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+                  "impid": "test-imp-id3",
+                  "price": 0.500000,
+                  "adm": "some-test-ad",
+                  "crid": "crid_10",
+                  "h": 90,
+                  "w": 728,
+                  "ext": {
+                    "ttx": {
+                      "mediaType": "banner"
+                    }
+                  }
+                }
+              ]
+            }
+          ],
+          "cur": "USD"
+        }
+      }
+    },
+    {
+      "expectedRequest": {
+        "uri": "http://ssc.33across.com",
+        "body": {
+          "ext": {
+            "ttx": {
+              "caller": [
+                {
+                  "name": "Prebid-Server",
+                  "version": "n/a"
+                }
+              ]
+            }
+          },
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id":"test-imp-id4",
+              "banner": {
+                "format": [{"w": 728, "h": 90}]
+              },
+              "ext": {
+                "ttx": {
+                  "prod": "siab",
                   "zoneid": "fake-site-id"
                 }
               }
@@ -129,10 +242,11 @@
           "seatbid": [
             {
               "seat": "ttx",
-              "bid": [{
+              "bid": [
+                {
                 "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
-                "impid": "test-imp-id2",
-                "price": 0.600000,
+                  "impid": "test-imp-id4",
+                  "price": 0.500000,
                 "adm": "some-test-ad",
                 "crid": "crid_10",
                 "h": 90,
@@ -142,7 +256,8 @@
                     "mediaType": "banner"
                   }
                 }
-              }]
+                }
+              ]
             }
           ],
           "cur": "USD"
@@ -171,6 +286,23 @@
             }
           },
           "type": "banner"
+        },
+        {
+          "bid": {
+            "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+            "impid": "test-imp-id2",
+            "price": 0.6,
+            "adm": "some-test-ad",
+            "crid": "crid_10",
+            "w": 728,
+            "h": 90,
+            "ext": {
+              "ttx": {
+                "mediaType": "banner"
+              }
+            }
+          },
+          "type": "banner"
         }
       ]
     },
@@ -180,8 +312,30 @@
         {
           "bid": {
             "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
-            "impid": "test-imp-id2",
-            "price": 0.6,
+            "impid": "test-imp-id3",
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "crid": "crid_10",
+            "w": 728,
+            "h": 90,
+            "ext": {
+              "ttx": {
+                "mediaType": "banner"
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    },
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
+            "impid": "test-imp-id4",
+            "price": 0.5,
             "adm": "some-test-ad",
             "crid": "crid_10",
             "w": 728,
diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go
index 1ec2fc08..6d2533df 100644
--- a/adapters/adapterstest/test_json.go
+++ b/adapters/adapterstest/test_json.go
@@ -10,6 +10,8 @@ import (
 	"github.com/mitchellh/copystructure"
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/currency"
+	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
 	"github.com/yudai/gojsondiff"
 	"github.com/yudai/gojsondiff/formatter"
@@ -103,7 +105,40 @@ func loadFile(filename string) (*testSpec, error) {
 //
 // More assertions will almost certainly be added in the future, as bugs come up.
 func runSpec(t *testing.T, filename string, spec *testSpec, bidder adapters.Bidder, isAmpTest, isVideoTest bool) {
-	reqInfo := adapters.ExtraRequestInfo{}
+	reqInfo := getTestExtraRequestInfo(t, filename, spec, isAmpTest, isVideoTest)
+	requests := testMakeRequestsImpl(t, filename, spec, bidder, reqInfo)
+
+	testMakeBidsImpl(t, filename, spec, bidder, requests)
+}
+
+// getTestExtraRequestInfo builds the ExtraRequestInfo object that will be passed to testMakeRequestsImpl
+func getTestExtraRequestInfo(t *testing.T, filename string, spec *testSpec, isAmpTest, isVideoTest bool) *adapters.ExtraRequestInfo {
+	t.Helper()
+
+	var reqInfo adapters.ExtraRequestInfo
+
+	// If test request.ext defines its own currency rates, add currency conversion to reqInfo
+	reqWrapper := &openrtb_ext.RequestWrapper{}
+	reqWrapper.BidRequest = &spec.BidRequest
+
+	reqExt, err := reqWrapper.GetRequestExt()
+	assert.NoError(t, err, "Could not unmarshall test request ext. %s", filename)
+
+	reqPrebid := reqExt.GetPrebid()
+	if reqPrebid != nil && reqPrebid.CurrencyConversions != nil && len(reqPrebid.CurrencyConversions.ConversionRates) > 0 {
+		err = currency.ValidateCustomRates(reqPrebid.CurrencyConversions)
+		assert.NoError(t, err, "Error validating currency rates in the test request: %s", filename)
+
+		// Get currency rates conversions from the test request.ext
+		conversions := currency.NewRates(reqPrebid.CurrencyConversions.ConversionRates)
+
+		// Create return adapters.ExtraRequestInfo object
+		reqInfo = adapters.NewExtraRequestInfo(conversions)
+	} else {
+		reqInfo = adapters.ExtraRequestInfo{}
+	}
+
+	// Set PbsEntryPoint if either isAmpTest or isVideoTest is true
 	if isAmpTest {
 		// simulates AMP entry point
 		reqInfo.PbsEntryPoint = "amp"
@@ -111,9 +146,7 @@ func runSpec(t *testing.T, filename string, spec *testSpec, bidder adapters.Bidd
 		reqInfo.PbsEntryPoint = "video"
 	}
 
-	requests := testMakeRequestsImpl(t, filename, spec, bidder, &reqInfo)
-
-	testMakeBidsImpl(t, filename, spec, bidder, requests)
+	return &reqInfo
 }
 
 type testSpec struct {
@@ -190,8 +223,15 @@ func assertMakeRequestsOutput(t *testing.T, filename string, actual []*adapters.
 	if len(expected) != len(actual) {
 		t.Fatalf("%s: MakeRequests had wrong request count. Expected %d, got %d", filename, len(expected), len(actual))
 	}
-	for i := 0; i < len(actual); i++ {
-		diffHttpRequests(t, fmt.Sprintf("%s: httpRequest[%d]", filename, i), actual[i], &(expected[i].Request))
+
+	for i := 0; i < len(expected); i++ {
+		var err error
+		for j := 0; j < len(actual); j++ {
+			if err = diffHttpRequests(fmt.Sprintf("%s: httpRequest[%d]", filename, i), actual[j], &(expected[i].Request)); err == nil {
+				break
+			}
+		}
+		assert.NoError(t, err, fmt.Sprintf("%s Expected RequestData was not returned by adapters' MakeRequests() implementation: httpRequest[%d]", filename, i))
 	}
 }
 
@@ -242,19 +282,30 @@ func assertMakeBidsOutput(t *testing.T, filename string, bidderResponse *adapter
 
 // diffHttpRequests compares the actual HTTP request data to the expected one.
 // It assumes that the request bodies are JSON
-func diffHttpRequests(t *testing.T, description string, actual *adapters.RequestData, expected *httpRequest) {
+func diffHttpRequests(description string, actual *adapters.RequestData, expected *httpRequest) error {
+
 	if actual == nil {
-		t.Errorf("Bidders cannot return nil HTTP calls. %s was nil.", description)
-		return
+		return fmt.Errorf("Bidders cannot return nil HTTP calls. %s was nil.", description)
+	}
+
+	if expected.Uri != actual.Uri {
+		return fmt.Errorf(`%s.uri "%s" does not match expected "%s."`, description, actual.Uri, expected.Uri)
 	}
 
-	diffStrings(t, fmt.Sprintf("%s.uri", description), actual.Uri, expected.Uri)
 	if expected.Headers != nil {
-		actualHeader, _ := json.Marshal(actual.Headers)
-		expectedHeader, _ := json.Marshal(expected.Headers)
-		diffJson(t, description, actualHeader, expectedHeader)
+		actualHeader, err := json.Marshal(actual.Headers)
+		if err != nil {
+			return fmt.Errorf(`%s actual.Headers could not be marshalled. Error: %s"`, description, err.Error())
+		}
+		expectedHeader, err := json.Marshal(expected.Headers)
+		if err != nil {
+			return fmt.Errorf(`%s expected.Headers could not be marshalled. Error: %s"`, description, err.Error())
+		}
+		if err := diffJson(description, actualHeader, expectedHeader); err != nil {
+			return err
+		}
 	}
-	diffJson(t, description, actual.Body, expected.Body)
+	return diffJson(description, actual.Body, expected.Body)
 }
 
 func diffBids(t *testing.T, description string, actual *adapters.TypedBid, expected *expectedBid) {
@@ -263,60 +314,54 @@ func diffBids(t *testing.T, description string, actual *adapters.TypedBid, expec
 		return
 	}
 
-	diffStrings(t, fmt.Sprintf("%s.type", description), string(actual.BidType), string(expected.Type))
-	diffOrtbBids(t, fmt.Sprintf("%s.bid", description), actual.Bid, expected.Bid)
+	assert.Equal(t, string(expected.Type), string(actual.BidType), fmt.Sprintf(`%s.type "%s" does not match expected "%s."`, description, string(actual.BidType), string(expected.Type)))
+	assert.NoError(t, diffOrtbBids(fmt.Sprintf("%s.bid", description), actual.Bid, expected.Bid))
 }
 
 // diffOrtbBids compares the actual Bid made by the adapter to the expectation from the JSON file.
-func diffOrtbBids(t *testing.T, description string, actual *openrtb2.Bid, expected json.RawMessage) {
+func diffOrtbBids(description string, actual *openrtb2.Bid, expected json.RawMessage) error {
 	if actual == nil {
-		t.Errorf("Bidders cannot return nil Bids. %s was nil.", description)
-		return
+		return fmt.Errorf("Bidders cannot return nil Bids. %s was nil.", description)
 	}
 
 	actualJson, err := json.Marshal(actual)
 	if err != nil {
-		t.Fatalf("%s failed to marshal actual Bid into JSON. %v", description, err)
+		return fmt.Errorf("%s failed to marshal actual Bid into JSON. %v", description, err)
 	}
 
-	diffJson(t, description, actualJson, expected)
-}
-
-func diffStrings(t *testing.T, description string, actual string, expected string) {
-	if actual != expected {
-		t.Errorf(`%s "%s" does not match expected "%s."`, description, actual, expected)
-	}
+	return diffJson(description, actualJson, expected)
 }
 
 // diffJson compares two JSON byte arrays for structural equality. It will produce an error if either
 // byte array is not actually JSON.
-func diffJson(t *testing.T, description string, actual []byte, expected []byte) {
+func diffJson(description string, actual []byte, expected []byte) error {
 	if len(actual) == 0 && len(expected) == 0 {
-		return
+		return nil
 	}
 	if len(actual) == 0 || len(expected) == 0 {
-		t.Fatalf("%s json diff failed. Expected %d bytes in body, but got %d.", description, len(expected), len(actual))
+		return fmt.Errorf("%s json diff failed. Expected %d bytes in body, but got %d.", description, len(expected), len(actual))
 	}
 	diff, err := gojsondiff.New().Compare(actual, expected)
 	if err != nil {
-		t.Fatalf("%s json diff failed. %v", description, err)
+		return fmt.Errorf("%s json diff failed. %v", description, err)
 	}
 
 	if diff.Modified() {
 		var left interface{}
 		if err := json.Unmarshal(actual, &left); err != nil {
-			t.Fatalf("%s json did not match, but unmarshalling failed. %v", description, err)
+			return fmt.Errorf("%s json did not match, but unmarshalling failed. %v", description, err)
 		}
 		printer := formatter.NewAsciiFormatter(left, formatter.AsciiFormatterConfig{
 			ShowArrayIndex: true,
 		})
 		output, err := printer.Format(diff)
 		if err != nil {
-			t.Errorf("%s did not match, but diff formatting failed. %v", description, err)
+			return fmt.Errorf("%s did not match, but diff formatting failed. %v", description, err)
 		} else {
-			t.Errorf("%s json did not match expected.\n\n%s", description, output)
+			return fmt.Errorf("%s json did not match expected.\n\n%s", description, output)
 		}
 	}
+	return nil
 }
 
 // testMakeRequestsImpl asserts the resulting values of the bidder's `MakeRequests()` implementation
diff --git a/adapters/adf/adftest/exemplary/single-banner-pricetype-gross-extend-ext.json b/adapters/adf/adftest/exemplary/single-banner-pricetype-gross-extend-ext.json
new file mode 100644
index 00000000..0cd03c75
--- /dev/null
+++ b/adapters/adf/adftest/exemplary/single-banner-pricetype-gross-extend-ext.json
@@ -0,0 +1,122 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "ext":{
+      "prebid":{
+        "aliases":{
+          "adfalias": "adf"
+        }
+      }
+    },
+    "imp": [{
+      "id": "test-imp-id",
+      "ext": {
+        "bidder": {
+          "mid": "828782",
+          "priceType": "gross"
+        }
+      },
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }]
+      }
+    }],
+    "site": {
+      "publisher": {
+        "id": "1"
+      },
+      "page": "some-page-url"
+    },
+    "device": {
+      "w": 1920,
+      "h": 800
+    }
+  },
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "https://adx.adform.net/adx/openrtb",
+      "body": {
+        "id": "test-request-id",
+        "ext": {
+          "prebid":{
+            "aliases":{
+              "adfalias": "adf"
+            }
+          },
+          "pt": "gross"
+        },
+        "imp": [{
+          "id": "test-imp-id",
+          "ext": {
+            "bidder": {
+              "mid": "828782",
+              "priceType": "gross"
+            }
+          },
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }]
+          },
+          "tagid": "828782"
+        }],
+        "site": {
+          "publisher": {
+            "id": "1"
+          },
+          "page": "some-page-url"
+        },
+        "device": {
+          "w": 1920,
+          "h": 800
+        }
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": {
+        "id": "test-request-id",
+        "seatbid": [{
+          "bid": [{
+            "id": "test-bid-id",
+            "impid": "test-imp-id",
+            "price": 10,
+            "adm": "{banner html}",
+            "adomain": [ "test.com" ],
+            "crid": "test-creative-id",
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          }]
+        }],
+        "cur": "USD"
+      }
+    }
+  }],
+  "expectedBidResponses": [{
+    "currency": "USD",
+    "bids": [
+      {
+        "bid": {
+          "id": "test-bid-id",
+          "impid": "test-imp-id",
+          "price": 10,
+          "adm": "{banner html}",
+          "crid": "test-creative-id",
+          "adomain": [ "test.com" ],
+          "ext": {
+            "prebid": {
+              "type": "banner"
+            }
+          }
+        },
+        "type": "banner"
+      }
+    ]
+  }]
+}
diff --git a/adapters/adf/adftest/exemplary/single-banner-pricetype-gross.json b/adapters/adf/adftest/exemplary/single-banner-pricetype-gross.json
new file mode 100644
index 00000000..2fb7cf24
--- /dev/null
+++ b/adapters/adf/adftest/exemplary/single-banner-pricetype-gross.json
@@ -0,0 +1,112 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [{
+      "id": "test-imp-id",
+      "ext": {
+        "bidder": {
+          "mid": "828782",
+          "priceType": "gross"
+        }
+      },
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }]
+      }
+    }],
+    "site": {
+      "publisher": {
+        "id": "1"
+      },
+      "page": "some-page-url"
+    },
+    "device": {
+      "w": 1920,
+      "h": 800
+    }
+  },
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "https://adx.adform.net/adx/openrtb",
+      "body": {
+        "id": "test-request-id",
+        "ext": {
+          "prebid": {
+          },
+          "pt": "gross"
+        },
+        "imp": [{
+          "id": "test-imp-id",
+          "ext": {
+            "bidder": {
+              "mid": "828782",
+              "priceType": "gross"
+            }
+          },
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }]
+          },
+          "tagid": "828782"
+        }],
+        "site": {
+          "publisher": {
+            "id": "1"
+          },
+          "page": "some-page-url"
+        },
+        "device": {
+          "w": 1920,
+          "h": 800
+        }
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": {
+        "id": "test-request-id",
+        "seatbid": [{
+          "bid": [{
+            "id": "test-bid-id",
+            "impid": "test-imp-id",
+            "price": 10,
+            "adm": "{banner html}",
+            "adomain": [ "test.com" ],
+            "crid": "test-creative-id",
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          }]
+        }],
+        "cur": "USD"
+      }
+    }
+  }],
+  "expectedBidResponses": [{
+    "currency": "USD",
+    "bids": [
+      {
+        "bid": {
+          "id": "test-bid-id",
+          "impid": "test-imp-id",
+          "price": 10,
+          "adm": "{banner html}",
+          "crid": "test-creative-id",
+          "adomain": [ "test.com" ],
+          "ext": {
+            "prebid": {
+              "type": "banner"
+            }
+          }
+        },
+        "type": "banner"
+      }
+    ]
+  }]
+}
diff --git a/adapters/adf/adftest/exemplary/single-banner-pricetype-net.json b/adapters/adf/adftest/exemplary/single-banner-pricetype-net.json
new file mode 100644
index 00000000..cb4b5f8a
--- /dev/null
+++ b/adapters/adf/adftest/exemplary/single-banner-pricetype-net.json
@@ -0,0 +1,112 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [{
+      "id": "test-imp-id",
+      "ext": {
+        "bidder": {
+          "mid": "828782",
+          "priceType": "net"
+        }
+      },
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }]
+      }
+    }],
+    "site": {
+      "publisher": {
+        "id": "1"
+      },
+      "page": "some-page-url"
+    },
+    "device": {
+      "w": 1920,
+      "h": 800
+    }
+  },
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "https://adx.adform.net/adx/openrtb",
+      "body": {
+        "id": "test-request-id",
+        "ext": {
+          "prebid": {
+          },
+          "pt": "net"
+        },
+        "imp": [{
+          "id": "test-imp-id",
+          "ext": {
+            "bidder": {
+              "mid": "828782",
+              "priceType": "net"
+            }
+          },
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }]
+          },
+          "tagid": "828782"
+        }],
+        "site": {
+          "publisher": {
+            "id": "1"
+          },
+          "page": "some-page-url"
+        },
+        "device": {
+          "w": 1920,
+          "h": 800
+        }
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": {
+        "id": "test-request-id",
+        "seatbid": [{
+          "bid": [{
+            "id": "test-bid-id",
+            "impid": "test-imp-id",
+            "price": 10,
+            "adm": "{banner html}",
+            "adomain": [ "test.com" ],
+            "crid": "test-creative-id",
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          }]
+        }],
+        "cur": "USD"
+      }
+    }
+  }],
+  "expectedBidResponses": [{
+    "currency": "USD",
+    "bids": [
+      {
+        "bid": {
+          "id": "test-bid-id",
+          "impid": "test-imp-id",
+          "price": 10,
+          "adm": "{banner html}",
+          "crid": "test-creative-id",
+          "adomain": [ "test.com" ],
+          "ext": {
+            "prebid": {
+              "type": "banner"
+            }
+          }
+        },
+        "type": "banner"
+      }
+    ]
+  }]
+}
diff --git a/adapters/adf/adftest/exemplary/two-banners-different-pricetypes-extend-ext.json b/adapters/adf/adftest/exemplary/two-banners-different-pricetypes-extend-ext.json
new file mode 100644
index 00000000..59e380d5
--- /dev/null
+++ b/adapters/adf/adftest/exemplary/two-banners-different-pricetypes-extend-ext.json
@@ -0,0 +1,151 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "ext":{
+      "prebid":{
+        "aliases":{
+          "adfalias": "adf"
+        }
+      }
+    },
+    "imp": [{
+      "id": "test-imp-id",
+      "ext": {
+        "bidder": {
+          "mid": "828782",
+          "priceType": "gross"
+        }
+      },
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }]
+      }
+    },{
+      "id": "test-imp-id2",
+      "ext": {
+        "bidder": {
+          "mid": "828783",
+          "priceType": "net"
+        }
+      },
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }]
+      }
+    }],
+    "site": {
+      "publisher": {
+        "id": "1"
+      },
+      "page": "some-page-url"
+    },
+    "device": {
+      "w": 1920,
+      "h": 800
+    }
+  },
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "https://adx.adform.net/adx/openrtb",
+      "body": {
+        "id": "test-request-id",
+        "ext": {
+          "prebid":{
+            "aliases":{
+              "adfalias": "adf"
+            }
+          },
+          "pt": "gross"
+        },
+        "imp": [{
+          "id": "test-imp-id",
+          "ext": {
+            "bidder": {
+              "mid": "828782",
+              "priceType": "gross"
+            }
+          },
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }]
+          },
+          "tagid": "828782"
+        },{
+          "id": "test-imp-id2",
+          "ext": {
+            "bidder": {
+              "mid": "828783",
+              "priceType": "net"
+            }
+          },
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }]
+          },
+          "tagid": "828783"
+        }],
+        "site": {
+          "publisher": {
+            "id": "1"
+          },
+          "page": "some-page-url"
+        },
+        "device": {
+          "w": 1920,
+          "h": 800
+        }
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": {
+        "id": "test-request-id",
+        "seatbid": [{
+          "bid": [{
+            "id": "test-bid-id",
+            "impid": "test-imp-id",
+            "price": 10,
+            "adm": "{banner html}",
+            "adomain": [ "test.com" ],
+            "crid": "test-creative-id",
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          }]
+        }],
+        "cur": "USD"
+      }
+    }
+  }],
+  "expectedBidResponses": [{
+    "currency": "USD",
+    "bids": [
+      {
+        "bid": {
+          "id": "test-bid-id",
+          "impid": "test-imp-id",
+          "price": 10,
+          "adm": "{banner html}",
+          "crid": "test-creative-id",
+          "adomain": [ "test.com" ],
+          "ext": {
+            "prebid": {
+              "type": "banner"
+            }
+          }
+        },
+        "type": "banner"
+      }
+    ]
+  }]
+}
diff --git a/adapters/adf/adftest/exemplary/two-banners-different-pricetypes.json b/adapters/adf/adftest/exemplary/two-banners-different-pricetypes.json
new file mode 100644
index 00000000..918ef3ca
--- /dev/null
+++ b/adapters/adf/adftest/exemplary/two-banners-different-pricetypes.json
@@ -0,0 +1,141 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [{
+      "id": "test-imp-id",
+      "ext": {
+        "bidder": {
+          "mid": "828782",
+          "priceType": "gross"
+        }
+      },
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }]
+      }
+    },{
+      "id": "test-imp-id2",
+      "ext": {
+        "bidder": {
+          "mid": "828783",
+          "priceType": "net"
+        }
+      },
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }]
+      }
+    }],
+    "site": {
+      "publisher": {
+        "id": "1"
+      },
+      "page": "some-page-url"
+    },
+    "device": {
+      "w": 1920,
+      "h": 800
+    }
+  },
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "https://adx.adform.net/adx/openrtb",
+      "body": {
+        "id": "test-request-id",
+        "ext": {
+          "prebid": {
+          },
+          "pt": "gross"
+        },
+        "imp": [{
+          "id": "test-imp-id",
+          "ext": {
+            "bidder": {
+              "mid": "828782",
+              "priceType": "gross"
+            }
+          },
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }]
+          },
+          "tagid": "828782"
+        },{
+          "id": "test-imp-id2",
+          "ext": {
+            "bidder": {
+              "mid": "828783",
+              "priceType": "net"
+            }
+          },
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }]
+          },
+          "tagid": "828783"
+        }],
+        "site": {
+          "publisher": {
+            "id": "1"
+          },
+          "page": "some-page-url"
+        },
+        "device": {
+          "w": 1920,
+          "h": 800
+        }
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": {
+        "id": "test-request-id",
+        "seatbid": [{
+          "bid": [{
+            "id": "test-bid-id",
+            "impid": "test-imp-id",
+            "price": 10,
+            "adm": "{banner html}",
+            "adomain": [ "test.com" ],
+            "crid": "test-creative-id",
+            "ext": {
+              "prebid": {
+                "type": "banner"
+              }
+            }
+          }]
+        }],
+        "cur": "USD"
+      }
+    }
+  }],
+  "expectedBidResponses": [{
+    "currency": "USD",
+    "bids": [
+      {
+        "bid": {
+          "id": "test-bid-id",
+          "impid": "test-imp-id",
+          "price": 10,
+          "adm": "{banner html}",
+          "crid": "test-creative-id",
+          "adomain": [ "test.com" ],
+          "ext": {
+            "prebid": {
+              "type": "banner"
+            }
+          }
+        },
+        "type": "banner"
+      }
+    ]
+  }]
+}
diff --git a/adapters/adf/params_test.go b/adapters/adf/params_test.go
index d075f914..d46c6528 100644
--- a/adapters/adf/params_test.go
+++ b/adapters/adf/params_test.go
@@ -46,6 +46,8 @@ var validParams = []string{
 	`{"inv":321,"mname":"12345"}`,
 	`{"mid":123,"inv":321,"mname":"pcl1"}`,
 	`{"mid":"123","inv":321,"mname":"pcl1"}`,
+	`{"mid":"123","priceType":"gross"}`,
+	`{"mid":"123","priceType":"net"}`,
 }
 
 var invalidParams = []string{
@@ -62,4 +64,5 @@ var invalidParams = []string{
 	`{"inv":321}`,
 	`{"inv":"321"}`,
 	`{"mname":"12345"}`,
+	`{"mid":"123","priceType":"GROSS"}`,
 }
diff --git a/adapters/adview/adviewtest/exemplary/banner-app-format.json b/adapters/adview/adviewtest/exemplary/banner-app-format.json
index 49956cdc..0ab49515 100644
--- a/adapters/adview/adviewtest/exemplary/banner-app-format.json
+++ b/adapters/adview/adviewtest/exemplary/banner-app-format.json
@@ -50,6 +50,7 @@
         "uri": "https://bid.adview.com/agent/thirdAdxService/accountid01",
         "body": {
           "id": "some-request-id",
+          "cur": ["USD"],
           "device": {
             "ua": "test-user-agent",
             "ip": "123.123.123.123",
@@ -134,6 +135,7 @@
   ],
   "expectedBidResponses": [
     {
+      "currency": "USD",
       "bids": [
         {
           "bid": {
diff --git a/adapters/adview/adviewtest/exemplary/banner-app.json b/adapters/adview/adviewtest/exemplary/banner-app.json
index e8697229..6aad1e8d 100644
--- a/adapters/adview/adviewtest/exemplary/banner-app.json
+++ b/adapters/adview/adviewtest/exemplary/banner-app.json
@@ -46,6 +46,7 @@
         "uri": "https://bid.adview.com/agent/thirdAdxService/accountid01",
         "body": {
           "id": "some-request-id",
+          "cur": ["USD"],
           "device": {
             "ua": "test-user-agent",
             "ip": "123.123.123.123",
@@ -124,6 +125,7 @@
   ],
   "expectedBidResponses": [
     {
+      "currency": "USD",
       "bids": [
         {
           "bid": {
diff --git a/adapters/adview/adviewtest/exemplary/native-app.json b/adapters/adview/adviewtest/exemplary/native-app.json
index df776a36..804494e5 100644
--- a/adapters/adview/adviewtest/exemplary/native-app.json
+++ b/adapters/adview/adviewtest/exemplary/native-app.json
@@ -46,6 +46,7 @@
         "uri": "https://bid.adview.com/agent/thirdAdxService/accountid01",
         "body": {
           "id": "some-request-id",
+          "cur": ["USD"],
           "device": {
             "ua": "test-user-agent",
             "ip": "123.123.123.123",
@@ -122,6 +123,7 @@
   ],
   "expectedBidResponses": [
     {
+      "currency": "USD",
       "bids":[
         {
           "bid":  {
diff --git a/adapters/adview/adviewtest/exemplary/video-app.json b/adapters/adview/adviewtest/exemplary/video-app.json
index 2136a8f7..57c9b855 100644
--- a/adapters/adview/adviewtest/exemplary/video-app.json
+++ b/adapters/adview/adviewtest/exemplary/video-app.json
@@ -51,6 +51,7 @@
         "uri": "https://bid.adview.com/agent/thirdAdxService/accountid01",
         "body": {
           "id": "some-request-id",
+          "cur": ["USD"],
           "device": {
             "ua": "test-user-agent",
             "ip": "123.123.123.123",
@@ -133,6 +134,7 @@
   ],
   "expectedBidResponses": [
     {
+      "currency": "USD",
       "bids":[
         {
           "bid":  {
diff --git a/adapters/adview/adviewtest/supplemental/bad-request.json b/adapters/adview/adviewtest/supplemental/bad-request.json
index 131e6d83..0f10fe79 100644
--- a/adapters/adview/adviewtest/supplemental/bad-request.json
+++ b/adapters/adview/adviewtest/supplemental/bad-request.json
@@ -20,6 +20,7 @@
         "uri": "https://bid.adview.com/agent/thirdAdxService/accountid01",
         "body": {
           "id": "test-request-id",
+          "cur": ["USD"],
           "imp": [{
             "ext": {
               "bidder": {
diff --git a/adapters/adview/adviewtest/supplemental/currency_rate_not_found.json b/adapters/adview/adviewtest/supplemental/currency_rate_not_found.json
new file mode 100644
index 00000000..61d1558c
--- /dev/null
+++ b/adapters/adview/adviewtest/supplemental/currency_rate_not_found.json
@@ -0,0 +1,44 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "bidfloor": 1.00,
+        "bidfloorcur": "JPY",
+        "ext":{
+          "bidder": {
+            "placementId": "posid00001",
+            "accountId": "accountid01"
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "MXN": {
+              "USD": 0.05
+            }
+          },
+          "usepbsrates": false
+        }
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Currency conversion rate not found: 'JPY' => 'USD'",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/adview/adviewtest/supplemental/empty-response.json b/adapters/adview/adviewtest/supplemental/empty-response.json
index 5bd86bc9..c080b18d 100644
--- a/adapters/adview/adviewtest/supplemental/empty-response.json
+++ b/adapters/adview/adviewtest/supplemental/empty-response.json
@@ -20,6 +20,7 @@
         "uri": "https://bid.adview.com/agent/thirdAdxService/accountid01",
         "body": {
           "id": "test-request-id",
+          "cur": ["USD"],
           "imp": [{
             "ext": {
               "bidder": {
diff --git a/adapters/adview/adviewtest/supplemental/nobid-response.json b/adapters/adview/adviewtest/supplemental/nobid-response.json
index f2c8864d..9e9ab678 100644
--- a/adapters/adview/adviewtest/supplemental/nobid-response.json
+++ b/adapters/adview/adviewtest/supplemental/nobid-response.json
@@ -20,6 +20,7 @@
         "uri": "https://bid.adview.com/agent/thirdAdxService/accountid01",
         "body": {
           "id": "test-request-id",
+          "cur": ["USD"],
           "imp": [{
             "ext": {
               "bidder": {
diff --git a/adapters/adview/adviewtest/supplemental/server-error.json b/adapters/adview/adviewtest/supplemental/server-error.json
index 7894be1b..de3bc9d3 100644
--- a/adapters/adview/adviewtest/supplemental/server-error.json
+++ b/adapters/adview/adviewtest/supplemental/server-error.json
@@ -20,6 +20,7 @@
         "uri": "https://bid.adview.com/agent/thirdAdxService/accountid01",
         "body": {
           "id": "test-request-id",
+          "cur": ["USD"],
           "imp": [{
             "ext": {
               "bidder": {
diff --git a/adapters/adview/adviewtest/supplemental/unparsable-response.json b/adapters/adview/adviewtest/supplemental/unparsable-response.json
index cef70ba8..18bc2e0f 100644
--- a/adapters/adview/adviewtest/supplemental/unparsable-response.json
+++ b/adapters/adview/adviewtest/supplemental/unparsable-response.json
@@ -20,6 +20,7 @@
         "uri": "https://bid.adview.com/agent/thirdAdxService/accountid01",
         "body": {
           "id": "test-request-id",
+          "cur": ["USD"],
           "imp": [{
             "ext": {
               "bidder": {
diff --git a/adapters/coinzilla/coinzilla_test.go b/adapters/coinzilla/coinzilla_test.go
new file mode 100644
index 00000000..7b0763e4
--- /dev/null
+++ b/adapters/coinzilla/coinzilla_test.go
@@ -0,0 +1,23 @@
+package coinzilla
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+const testsDir = "coinzillatest"
+const testsBidderEndpoint = "http://test-request.com/prebid"
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderCoinzilla, config.Adapter{
+		Endpoint: testsBidderEndpoint})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, testsDir, bidder)
+}
diff --git a/adapters/coinzilla/coinzillatest/exemplary/multi-banners.json b/adapters/coinzilla/coinzillatest/exemplary/multi-banners.json
new file mode 100644
index 00000000..dcbfe3e1
--- /dev/null
+++ b/adapters/coinzilla/coinzillatest/exemplary/multi-banners.json
@@ -0,0 +1,117 @@
+{
+    "mockBidRequest": {
+        "id": "testRequestIdMulti",
+        "site": {
+            "page": "https://publisher-website.com/web"
+        },
+        "imp": [{
+            "id": "testImpressionId",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                },{
+                    "w": 728,
+                    "h": 90
+                }]
+            },
+            "ext": {
+                "bidder": {
+                    "placementId": "testPlacementId"
+                }
+            }
+        }]
+    },
+
+    "httpCalls": [{
+        "expectedRequest": {
+            "uri": "http://test-request.com/prebid",
+            "body": {
+                "id": "testRequestIdMulti",
+                "site": {
+                    "page": "https://publisher-website.com/web"
+                },
+                "imp": [{
+                    "id": "testImpressionId",
+                    "banner": {
+                        "format": [{
+                            "w": 300,
+                            "h": 250
+                        },{
+                            "w": 728,
+                            "h": 90
+                        }]
+                    },
+                    "ext": {
+                        "bidder": {
+                            "placementId": "testPlacementId"
+                        }
+                    }
+                }]
+            }
+        },
+        "mockResponse": {
+            "status": 200,
+            "body": {
+                "id": "testRequestIdMulti",
+                "seatbid": [{
+                    "seat": "coinzilla",
+                    "bid": [{
+                        "id": "uniqueId#1",
+                        "impid": "testImpressionId",
+                        "price": 2.35,
+                        "adid": "advertiserUniqueId#1",
+                        "adm": "iframe-content#1",
+                        "cid": "campaignUniqueId#1",
+                        "crid": "creativeUniqueId#1",
+                        "h": 250,
+                        "w": 300
+                    },{
+                        "id": "uniqueId#2",
+                        "impid": "testImpressionId",
+                        "price": 1.93,
+                        "adid": "advertiserUniqueId#2",
+                        "adm": "iframe-content#2",
+                        "cid": "campaignUniqueId#2",
+                        "crid": "creativeUniqueId#2",
+                        "h": 90,
+                        "w": 728
+                    }]
+                }],
+                "cur": "USD"
+            }
+        }
+    }],
+
+    "expectedBidResponses": [{
+        "currency": "USD",
+        "bids": [{
+            "bid": {
+                "id": "uniqueId#1",
+                "impid": "testImpressionId",
+                "price": 2.35,
+                "adm": "iframe-content#1",
+                "adid": "advertiserUniqueId#1",
+                "cid": "campaignUniqueId#1",
+                "crid": "creativeUniqueId#1",
+                "w": 300,
+                "h": 250
+            },
+            "type": "banner"
+        },{
+            "bid": {
+                "id": "uniqueId#2",
+                "impid": "testImpressionId",
+                "price": 1.93,
+                "adm": "iframe-content#2",
+                "adid": "advertiserUniqueId#2",
+                "cid": "campaignUniqueId#2",
+                "crid": "creativeUniqueId#2",
+                "w": 728,
+                "h": 90
+            },
+            "type": "banner"
+        }
+        ]
+    }]
+}
diff --git a/adapters/coinzilla/coinzillatest/exemplary/multi-imp.json b/adapters/coinzilla/coinzillatest/exemplary/multi-imp.json
new file mode 100644
index 00000000..f17ee95d
--- /dev/null
+++ b/adapters/coinzilla/coinzillatest/exemplary/multi-imp.json
@@ -0,0 +1,137 @@
+{
+    "mockBidRequest": {
+        "id": "testRequestIdSingle",
+        "site": {
+            "page": "https://publisher-website.com/web"
+        },
+        "imp": [{
+            "id": "testImpressionIdFirst",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                }]
+            },
+            "ext": {
+                "bidder": {
+                    "placementId": "testPlacementIdFirst"
+                }
+            }
+        },{
+            "id": "testImpressionIdSecond",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                }]
+            },
+            "ext": {
+                "bidder": {
+                    "placementId": "testPlacementIdSecond"
+                }
+            }
+        }]
+    },
+
+    "httpCalls": [{
+        "expectedRequest": {
+            "uri": "http://test-request.com/prebid",
+            "body": {
+                "id": "testRequestIdSingle",
+                "site": {
+                    "page": "https://publisher-website.com/web"
+                },
+                "imp": [{
+                    "id": "testImpressionIdFirst",
+                    "banner": {
+                        "format": [{
+                            "w": 300,
+                            "h": 250
+                        }]
+                    },
+                    "ext": {
+                        "bidder": {
+                            "placementId": "testPlacementIdFirst"
+                        }
+                    }
+                },
+                    {
+                        "id": "testImpressionIdSecond",
+                        "banner": {
+                            "format": [{
+                                "w": 300,
+                                "h": 250
+                            }]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "testPlacementIdSecond"
+                            }
+                        }
+                    }]
+            }
+        },
+        "mockResponse": {
+            "status": 200,
+            "body": {
+                "id": "testRequestIdSingle",
+                "seatbid": [{
+                    "seat": "coinzilla",
+                    "bid": [{
+                        "id": "uniqueIdFirst",
+                        "impid": "testImpressionIdFirst",
+                        "price": 2.35,
+                        "adid": "advertiserUniqueIdFirst",
+                        "adm": "iframe-content",
+                        "cid": "campaignUniqueId",
+                        "crid": "creativeUniqueId",
+                        "h": 250,
+                        "w": 300
+                    },{
+                        "id": "uniqueIdSecond",
+                        "impid": "testImpressionIdSecond",
+                        "price": 2.35,
+                        "adid": "advertiserUniqueIdSecond",
+                        "adm": "iframe-content",
+                        "cid": "campaignUniqueId",
+                        "crid": "creativeUniqueId",
+                        "h": 250,
+                        "w": 300
+                    }]
+                }],
+                "cur": "USD"
+            }
+        }
+    }],
+
+    "expectedBidResponses": [{
+        "currency": "USD",
+        "bids": [{
+            "bid": {
+                "id": "uniqueIdFirst",
+                "impid": "testImpressionIdFirst",
+                "price": 2.35,
+                "adm": "iframe-content",
+                "adid": "advertiserUniqueIdFirst",
+                "cid": "campaignUniqueId",
+                "crid": "creativeUniqueId",
+                "w": 300,
+                "h": 250
+            },
+            "type": "banner"
+        },{
+            "bid": {
+                "id": "uniqueIdSecond",
+                "impid": "testImpressionIdSecond",
+                "price": 2.35,
+                "adm": "iframe-content",
+                "adid": "advertiserUniqueIdSecond",
+                "cid": "campaignUniqueId",
+                "crid": "creativeUniqueId",
+                "w": 300,
+                "h": 250
+            },
+            "type": "banner"
+        }]
+    }]
+}
diff --git a/adapters/coinzilla/coinzillatest/exemplary/single-banner.json b/adapters/coinzilla/coinzillatest/exemplary/single-banner.json
new file mode 100644
index 00000000..5d609266
--- /dev/null
+++ b/adapters/coinzilla/coinzillatest/exemplary/single-banner.json
@@ -0,0 +1,87 @@
+{
+    "mockBidRequest": {
+        "id": "testRequestIdSingle",
+        "site": {
+            "page": "https://publisher-website.com/web"
+        },
+        "imp": [{
+            "id": "testImpressionId",
+            "banner": {
+                "format": [{
+                    "w": 300,
+                    "h": 250
+                }]
+            },
+            "ext": {
+                "bidder": {
+                    "placementId": "testPlacementId"
+                }
+            }
+        }]
+    },
+
+    "httpCalls": [{
+        "expectedRequest": {
+            "uri": "http://test-request.com/prebid",
+            "body": {
+                "id": "testRequestIdSingle",
+                "site": {
+                    "page": "https://publisher-website.com/web"
+                },
+                "imp": [{
+                    "id": "testImpressionId",
+                    "banner": {
+                        "format": [{
+                            "w": 300,
+                            "h": 250
+                        }]
+                    },
+                    "ext": {
+                        "bidder": {
+                            "placementId": "testPlacementId"
+                        }
+                    }
+                }]
+            }
+        },
+        "mockResponse": {
+            "status": 200,
+            "body": {
+                "id": "testRequestIdSingle",
+                "seatbid": [{
+                    "seat": "coinzilla",
+                    "bid": [{
+                        "id": "uniqueId",
+                        "impid": "testImpressionId",
+                        "price": 2.35,
+                        "adid": "advertiserUniqueId",
+                        "adm": "iframe-content",
+                        "cid": "campaignUniqueId",
+                        "crid": "creativeUniqueId",
+                        "h": 250,
+                        "w": 300
+                    }]
+                }],
+                "cur": "USD"
+            }
+        }
+    }],
+
+    "expectedBidResponses": [{
+        "currency": "USD",
+        "bids": [{
+            "bid": {
+                "id": "uniqueId",
+                "impid": "testImpressionId",
+                "price": 2.35,
+                "adm": "iframe-content",
+                "adid": "advertiserUniqueId",
+                "cid": "campaignUniqueId",
+                "crid": "creativeUniqueId",
+                "w": 300,
+                "h": 250
+            },
+            "type": "banner"
+        }]
+    }]
+}
diff --git a/adapters/coinzilla/coinzillatest/supplemental/bad-request.json b/adapters/coinzilla/coinzillatest/supplemental/bad-request.json
new file mode 100644
index 00000000..3787ff71
--- /dev/null
+++ b/adapters/coinzilla/coinzillatest/supplemental/bad-request.json
@@ -0,0 +1,59 @@
+{
+  "mockBidRequest": {
+    "id": "testRequestIssues",
+    "site": {
+      "page": "https://publisher-website.com/web"
+    },
+    "imp": [{
+      "id": "testImpressionId",
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }]
+      },
+      "ext": {
+        "bidder": {
+          "placementId": "testPlacementId"
+        }
+      }
+    }]
+  },
+
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "http://test-request.com/prebid",
+      "body": {
+        "id": "testRequestIssues",
+        "site": {
+          "page": "https://publisher-website.com/web"
+        },
+        "imp": [{
+          "id": "testImpressionId",
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }]
+          },
+          "ext": {
+            "bidder": {
+              "placementId": "testPlacementId"
+            }
+          }
+        }]
+      }
+    },
+      "mockResponse": {
+        "status": 400,
+        "body": {}
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected code: 400. Run with request.debug = 1",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/coinzilla/coinzillatest/supplemental/no-bid.json b/adapters/coinzilla/coinzillatest/supplemental/no-bid.json
new file mode 100644
index 00000000..cff63b6b
--- /dev/null
+++ b/adapters/coinzilla/coinzillatest/supplemental/no-bid.json
@@ -0,0 +1,55 @@
+{
+  "mockBidRequest": {
+    "id": "testRequestNoBid",
+    "site": {
+      "page": "https://publisher-website.com/web"
+    },
+    "imp": [{
+      "id": "testImpressionId",
+      "banner": {
+        "format": [{
+          "w": 300,
+          "h": 250
+        }]
+      },
+      "ext": {
+        "bidder": {
+          "placementId": "testPlacementId"
+        }
+      }
+    }]
+  },
+
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "http://test-request.com/prebid",
+      "body": {
+        "id": "testRequestNoBid",
+        "site": {
+          "page": "https://publisher-website.com/web"
+        },
+        "imp": [{
+          "id": "testImpressionId",
+          "banner": {
+            "format": [{
+              "w": 300,
+              "h": 250
+            }]
+          },
+          "ext": {
+            "bidder": {
+              "placementId": "testPlacementId"
+            }
+          }
+        }]
+      }
+    },
+      "mockResponse": {
+        "status": 204,
+        "body": {}
+      }
+    }
+  ],
+
+  "expectedBidResponses": []
+}
diff --git a/adapters/coinzilla/params_test.go b/adapters/coinzilla/params_test.go
new file mode 100644
index 00000000..fa24b144
--- /dev/null
+++ b/adapters/coinzilla/params_test.go
@@ -0,0 +1,45 @@
+package coinzilla
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderCoinzilla, json.RawMessage(p)); err != nil {
+			t.Errorf("Schema rejected valid params: %s", p)
+		}
+	}
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderCoinzilla, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"placementId": "1sad231"}`,
+}
+
+var invalidParams = []string{
+	`{"placementId": 42}`,
+	`null`,
+	`{}`,
+	`{"placementId":""}`,
+}
diff --git a/adapters/impactify/impactifytest/exemplary/sample_banner.json b/adapters/impactify/impactifytest/exemplary/sample_banner.json
index f5d76c9e..2eeba02b 100644
--- a/adapters/impactify/impactifytest/exemplary/sample_banner.json
+++ b/adapters/impactify/impactifytest/exemplary/sample_banner.json
@@ -14,6 +14,8 @@
             }
           ]
         },
+        "bidfloor": 1.00,
+        "bidfloorcur": "MXN",
         "ext":{
           "bidder": {
             "appId": "impactify.io",
@@ -22,7 +24,19 @@
           }
         }
       }
-    ]
+    ],
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "MXN": {
+              "USD": 0.05
+            }
+          },
+          "usepbsrates": false
+        }
+      }
+    }
   },
 
   "httpCalls": [
@@ -45,6 +59,8 @@
                   }
                 ]
               },
+              "bidfloor": 0.05,
+              "bidfloorcur": "USD",
               "ext": {
                 "impactify": {
                   "appId": "impactify.io",
@@ -53,7 +69,19 @@
                 }
               }
             }
-          ]
+          ],
+          "ext": {
+            "prebid": {
+              "currency": {
+                "rates": {
+                  "MXN": {
+                    "USD": 0.05
+                  }
+                },
+                "usepbsrates": false
+              }
+            }
+          }
         }
       },
       "mockResponse": {
diff --git a/adapters/impactify/impactifytest/supplemental/currency_rate_not_found.json b/adapters/impactify/impactifytest/supplemental/currency_rate_not_found.json
new file mode 100644
index 00000000..d52ccd31
--- /dev/null
+++ b/adapters/impactify/impactifytest/supplemental/currency_rate_not_found.json
@@ -0,0 +1,45 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "bidfloor": 1.00,
+        "bidfloorcur": "JPY",
+        "ext":{
+          "bidder": {
+            "appId": "impactify.io",
+            "format": "screen",
+            "style": "impact"
+          }
+        }
+      }
+    ],
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "MXN": {
+              "USD": 0.05
+            }
+          },
+          "usepbsrates": false
+        }
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Currency conversion rate not found: 'JPY' => 'USD'",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/openrtb_util_test.go b/adapters/openrtb_util_test.go
new file mode 100644
index 00000000..e09149a7
--- /dev/null
+++ b/adapters/openrtb_util_test.go
@@ -0,0 +1,117 @@
+package adapters
+
+import (
+	"encoding/json"
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"reflect"
+	"testing"
+)
+
+func TestExtractAdapterReqBidderParams(t *testing.T) {
+	type args struct {
+		bidRequest *openrtb2.BidRequest
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    map[string]json.RawMessage
+		wantErr bool
+	}{
+		{
+			name: "extract bidder params from nil req",
+			args: args{
+				bidRequest: nil,
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name: "extract bidder params from nil req.Ext",
+			args: args{
+				bidRequest: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{}}`)},
+			},
+			want:    nil,
+			wantErr: false,
+		},
+		{
+			name: "extract bidder params from req.Ext for input request in adapter code",
+			args: args{
+				bidRequest: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"bidderparams": {"profile": 1234, "version": 1}}}`)},
+			},
+			want:    map[string]json.RawMessage{"profile": json.RawMessage(`1234`), "version": json.RawMessage(`1`)},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ExtractAdapterReqBidderParams(tt.args.bidRequest)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ExtractReqExtBidderParams() error = %v, wantErr = %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ExtractReqExtBidderParams() got = %v, want = %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestExtractReqExtBidderParams(t *testing.T) {
+	type args struct {
+		request *openrtb2.BidRequest
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    map[string]map[string]json.RawMessage
+		wantErr bool
+	}{
+		{
+			name: "extract bidder params from nil req",
+			args: args{
+				request: nil,
+			},
+			want:    nil,
+			wantErr: true,
+		},
+		{
+			name: "extract bidder params from nil req.Ext.prebid",
+			args: args{
+				request: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{}}`)},
+			},
+			want:    nil,
+			wantErr: false,
+		},
+		{
+			name: "extract bidder params from nil req.Ext",
+			args: args{
+				request: &openrtb2.BidRequest{Ext: nil},
+			},
+			want:    nil,
+			wantErr: false,
+		},
+		{
+			name: "extract bidder params from req.Ext for input request before adapter code",
+			args: args{
+				request: &openrtb2.BidRequest{Ext: json.RawMessage(`{"prebid":{"bidderparams": {"pubmatic": {"profile": 1234, "version": 1}, "appnexus": {"key1": 123, "key2": {"innerKey1":"innerValue1"} } }}}`)},
+			},
+			want: map[string]map[string]json.RawMessage{
+				"pubmatic": {"profile": json.RawMessage(`1234`), "version": json.RawMessage(`1`)},
+				"appnexus": {"key1": json.RawMessage(`123`), "key2": json.RawMessage(`{"innerKey1":"innerValue1"}`)},
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ExtractReqExtBidderParams(tt.args.request)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ExtractReqExtBidderParams() error = %v, wantErr = %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("ExtractReqExtBidderParams() got = %v, want = %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/adapters/pubmatic/pubmatictest/supplemental/nilReqExt.json b/adapters/pubmatic/pubmatictest/supplemental/nilReqExt.json
new file mode 100644
index 00000000..06fc80ae
--- /dev/null
+++ b/adapters/pubmatic/pubmatictest/supplemental/nilReqExt.json
@@ -0,0 +1,160 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "adSlot": "AdTag_Div1@300x250",
+            "publisherId": "  999  ",
+            "keywords": [
+              {
+                "key": "pmZoneID",
+                "value": [
+                  "Zone1",
+                  "Zone2"
+                ]
+              },
+              {
+                "key": "preference",
+                "value": [
+                  "sports",
+                  "movies"
+                ]
+              }
+            ],
+            "wrapper": {
+              "version": 1,
+              "profile": 5123
+            }
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+    },
+    "ext": {},
+    "site": {
+      "id": "siteID",
+      "publisher": {
+        "id": "1234"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "tagid": "AdTag_Div1",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ],
+                "h": 250,
+                "w": 300
+              },
+              "ext": {
+                "pmZoneId": "Zone1,Zone2",
+                "preference": "sports,movies"
+              }
+            }
+          ],
+          "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+          },
+          "site": {
+            "id": "siteID",
+            "publisher": {
+              "id": "999"
+            }
+          },
+          "ext": {
+            "wrapper": {
+              "profile": 5123,
+              "version": 1
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [
+                {
+                  "id": "7706636740145184841",
+                  "impid": "test-imp-id",
+                  "price": 0.500000,
+                  "adid": "29681110",
+                  "adm": "some-test-ad",
+                  "adomain": [
+                    "pubmatic.com"
+                  ],
+                  "crid": "29681110",
+                  "h": 250,
+                  "w": 300,
+                  "dealid": "test deal",
+                  "ext": {
+                    "dspid": 6,
+                    "deal_channel": 1
+                  }
+                }
+              ]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "7706636740145184841",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adid": "29681110",
+            "adm": "some-test-ad",
+            "adomain": [
+              "pubmatic.com"
+            ],
+            "crid": "29681110",
+            "w": 300,
+            "h": 250,
+            "dealid": "test deal",
+            "ext": {
+              "dspid": 6,
+              "deal_channel": 1
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/pubmatic/pubmatictest/supplemental/reqBidderParams.json b/adapters/pubmatic/pubmatictest/supplemental/reqBidderParams.json
new file mode 100644
index 00000000..5d4a1819
--- /dev/null
+++ b/adapters/pubmatic/pubmatictest/supplemental/reqBidderParams.json
@@ -0,0 +1,169 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "adSlot": "AdTag_Div1@300x250",
+            "publisherId": "  999  ",
+            "keywords": [
+              {
+                "key": "pmZoneID",
+                "value": [
+                  "Zone1",
+                  "Zone2"
+                ]
+              },
+              {
+                "key": "preference",
+                "value": [
+                  "sports",
+                  "movies"
+                ]
+              }
+            ],
+            "wrapper": {
+              "version": 1,
+              "profile": 5123
+            }
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+    },
+    "ext": {
+      "prebid": {
+        "bidderparams": {
+          "wrapper": {
+            "profile": 1234,
+            "version": 2
+          }
+        }
+      }
+    },
+    "site": {
+      "id": "siteID",
+      "publisher": {
+        "id": "1234"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "tagid": "AdTag_Div1",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ],
+                "h": 250,
+                "w": 300
+              },
+              "ext": {
+                "pmZoneId": "Zone1,Zone2",
+                "preference": "sports,movies"
+              }
+            }
+          ],
+          "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+          },
+          "site": {
+            "id": "siteID",
+            "publisher": {
+              "id": "999"
+            }
+          },
+          "ext": {
+            "wrapper": {
+              "profile": 1234,
+              "version": 2
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [
+                {
+                  "id": "7706636740145184841",
+                  "impid": "test-imp-id",
+                  "price": 0.500000,
+                  "adid": "29681110",
+                  "adm": "some-test-ad",
+                  "adomain": [
+                    "pubmatic.com"
+                  ],
+                  "crid": "29681110",
+                  "h": 250,
+                  "w": 300,
+                  "dealid": "test deal",
+                  "ext": {
+                    "dspid": 6,
+                    "deal_channel": 1
+                  }
+                }
+              ]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "7706636740145184841",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adid": "29681110",
+            "adm": "some-test-ad",
+            "adomain": [
+              "pubmatic.com"
+            ],
+            "crid": "29681110",
+            "w": 300,
+            "h": 250,
+            "dealid": "test deal",
+            "ext": {
+              "dspid": 6,
+              "deal_channel": 1
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/smaato/smaatotest/exemplary/simple-banner.json b/adapters/smaato/smaatotest/exemplary/simple-banner.json
index babce4f8..fee49ee9 100644
--- a/adapters/smaato/smaatotest/exemplary/simple-banner.json
+++ b/adapters/smaato/smaatotest/exemplary/simple-banner.json
@@ -31,6 +31,7 @@
             }
           ]
         },
+        "instl": 1,
         "bidfloor": 0.00123,
         "ext": {
           "bidder": {
@@ -82,6 +83,7 @@
               "id": "1C86242D-9535-47D6-9576-7B1FE87F282C",
               "tagid": "130563103",
               "bidfloor": 0.00123,
+              "instl": 1,
               "banner": {
                 "h": 50,
                 "w": 320,
diff --git a/adapters/smaato/smaatotest/exemplary/video.json b/adapters/smaato/smaatotest/exemplary/video.json
index 205c02ad..853a63b2 100644
--- a/adapters/smaato/smaatotest/exemplary/video.json
+++ b/adapters/smaato/smaatotest/exemplary/video.json
@@ -29,6 +29,7 @@
                         "rewarded": 0
                     }
                 },
+                "instl": 1,
                 "bidfloor": 0.00123,
                 "ext": {
                     "bidder": {
@@ -84,6 +85,7 @@
                             "id": "postbid_iframe",
                             "tagid": "130563103",
                             "bidfloor": 0.00123,
+                            "instl": 1,
                             "video": {
                                 "w": 1024,
                                 "h": 768,
diff --git a/adapters/videobyte/params_test.go b/adapters/videobyte/params_test.go
new file mode 100644
index 00000000..fa38836b
--- /dev/null
+++ b/adapters/videobyte/params_test.go
@@ -0,0 +1,55 @@
+package videobyte
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// This file actually intends to test static/bidder-params/videobyte.json
+//
+// These also validate the format of the external API: request.imp[i].ext.videobyte
+
+// TestValidParams makes sure that the videobyte 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.BidderVideoByte, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected videobyte params: %s", validParam)
+		}
+	}
+}
+
+// TestInvalidParams makes sure that the videobyte 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.BidderVideoByte, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"pubId": "123"}`,
+	`{"pubId": "123", "placementId": "4"}`,
+	`{"pubId": "123", "nid": "5"}`,
+	`{"pubId": "123", "placementId": "4", "nid": "5"}`,
+}
+
+var invalidParams = []string{
+	`{"invalidParam" : "a"}`,
+	`{}`,
+	`{"placementId": "4"}`,
+	`{"nid": "5"}`,
+	`{"placementId": "4", "nid": "5"}`,
+}
diff --git a/adapters/videobyte/videobyte_test.go b/adapters/videobyte/videobyte_test.go
new file mode 100644
index 00000000..9357a533
--- /dev/null
+++ b/adapters/videobyte/videobyte_test.go
@@ -0,0 +1,17 @@
+package videobyte
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/config"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder("videobyte", config.Adapter{Endpoint: "https://mock.videobyte.com"})
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+	adapterstest.RunJSONBidderTest(t, "videobytetest", bidder)
+}
diff --git a/adapters/videobyte/videobytetest/exemplary/banner.json b/adapters/videobyte/videobytetest/exemplary/banner.json
new file mode 100644
index 00000000..ecf733cc
--- /dev/null
+++ b/adapters/videobyte/videobytetest/exemplary/banner.json
@@ -0,0 +1,145 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId",
+            "placementId": "examplePlacementId",
+            "nid": "exampleNetworkId"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Referer": [
+            "http://referer.com/page-2"
+          ],
+          "Origin": [
+            "example.com"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?nid=exampleNetworkId&pid=examplePublisherId&placementId=examplePlacementId&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId",
+                  "placementId": "examplePlacementId",
+                  "nid": "exampleNetworkId"
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "http://example.com/page-1",
+            "ref": "http://referer.com/page-2"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+                  "crid": "72745",
+                  "adomain": [
+                    "ad-domain.com"
+                  ],
+                  "price": 3,
+                  "impid": "test-imp-id",
+                  "adid": "564",
+                  "adm": "<iframe bordercolor=\"#000000\" frameborder=\"0\" height=\"250\" hspace=\"0\" marginheight=\"0\" marginwidth=\"0\" scrolling=\"no\" srcdoc=\"contents\" vspace=\"0\" width=\"300\"></iframe>"
+                }
+              ],
+              "seat": "videobyte"
+            }
+          ],
+          "bidid": "test-request-id",
+          "id": "test-request-id"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+            "crid": "72745",
+            "adomain": [
+              "ad-domain.com"
+            ],
+            "price": 3,
+            "impid": "test-imp-id",
+            "adid": "564",
+            "adm": "<iframe bordercolor=\"#000000\" frameborder=\"0\" height=\"250\" hspace=\"0\" marginheight=\"0\" marginwidth=\"0\" scrolling=\"no\" srcdoc=\"contents\" vspace=\"0\" width=\"300\"></iframe>"
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/videobyte/videobytetest/exemplary/empty-placement-network.json b/adapters/videobyte/videobytetest/exemplary/empty-placement-network.json
new file mode 100644
index 00000000..ddda5aa2
--- /dev/null
+++ b/adapters/videobyte/videobytetest/exemplary/empty-placement-network.json
@@ -0,0 +1,149 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Referer": [
+            "http://referer.com/page-2"
+          ],
+          "Origin": [
+            "example.com"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?pid=examplePublisherId&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId"
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "http://example.com/page-1",
+            "ref": "http://referer.com/page-2"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+                  "crid": "72745",
+                  "adomain": [
+                    "ad-domain.com"
+                  ],
+                  "price": 3,
+                  "impid": "test-imp-id",
+                  "adid": "564",
+                  "adm": "<?xml version=\"1.0\"?><VAST version=\"2.0\"></VAST>"
+                }
+              ],
+              "seat": "videobyte"
+            }
+          ],
+          "bidid": "test-request-id",
+          "id": "test-request-id"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+            "crid": "72745",
+            "adomain": [
+              "ad-domain.com"
+            ],
+            "price": 3,
+            "impid": "test-imp-id",
+            "adid": "564",
+            "adm": "<?xml version=\"1.0\"?><VAST version=\"2.0\"></VAST>"
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/videobyte/videobytetest/exemplary/empty-site-domain-ref.json b/adapters/videobyte/videobytetest/exemplary/empty-site-domain-ref.json
new file mode 100644
index 00000000..e10a4aa5
--- /dev/null
+++ b/adapters/videobyte/videobytetest/exemplary/empty-site-domain-ref.json
@@ -0,0 +1,143 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId",
+            "placementId": "examplePlacementId",
+            "nid": "exampleNetworkId"
+          }
+        }
+      }
+    ],
+    "site": {
+      "page": "http://example.com/page-1"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?nid=exampleNetworkId&pid=examplePublisherId&placementId=examplePlacementId&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId",
+                  "placementId": "examplePlacementId",
+                  "nid": "exampleNetworkId"
+                }
+              }
+            }
+          ],
+          "site": {
+            "page": "http://example.com/page-1"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+                  "crid": "72745",
+                  "adomain": [
+                    "ad-domain.com"
+                  ],
+                  "price": 3,
+                  "impid": "test-imp-id",
+                  "adid": "564",
+                  "adm": "<?xml version=\"1.0\"?><VAST version=\"2.0\"></VAST>"
+                }
+              ],
+              "seat": "videobyte"
+            }
+          ],
+          "bidid": "test-request-id",
+          "id": "test-request-id"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+            "crid": "72745",
+            "adomain": [
+              "ad-domain.com"
+            ],
+            "price": 3,
+            "impid": "test-imp-id",
+            "adid": "564",
+            "adm": "<?xml version=\"1.0\"?><VAST version=\"2.0\"></VAST>"
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/videobyte/videobytetest/exemplary/multi-format.json b/adapters/videobyte/videobytetest/exemplary/multi-format.json
new file mode 100644
index 00000000..cf62ad79
--- /dev/null
+++ b/adapters/videobyte/videobytetest/exemplary/multi-format.json
@@ -0,0 +1,161 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "w": 300,
+          "h": 250
+        },
+        "video": {
+          "w": 300,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId",
+            "placementId": "examplePlacementId",
+            "nid": "exampleNetworkId"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Referer": [
+            "http://referer.com/page-2"
+          ],
+          "Origin": [
+            "example.com"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?nid=exampleNetworkId&pid=examplePublisherId&placementId=examplePlacementId&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "w": 300,
+                "h": 250
+              },
+              "video": {
+                "w": 300,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId",
+                  "placementId": "examplePlacementId",
+                  "nid": "exampleNetworkId"
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "http://example.com/page-1",
+            "ref": "http://referer.com/page-2"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+                  "crid": "72745",
+                  "adomain": [
+                    "ad-domain.com"
+                  ],
+                  "price": 3,
+                  "impid": "test-imp-id",
+                  "adid": "564",
+                  "adm": "<iframe bordercolor=\"#000000\" frameborder=\"0\" height=\"250\" hspace=\"0\" marginheight=\"0\" marginwidth=\"0\" scrolling=\"no\" srcdoc=\"contents\" vspace=\"0\" width=\"300\"></iframe>"
+                }
+              ],
+              "seat": "videobyte"
+            }
+          ],
+          "bidid": "test-request-id",
+          "id": "test-request-id"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+            "crid": "72745",
+            "adomain": [
+              "ad-domain.com"
+            ],
+            "price": 3,
+            "impid": "test-imp-id",
+            "adid": "564",
+            "adm": "<iframe bordercolor=\"#000000\" frameborder=\"0\" height=\"250\" hspace=\"0\" marginheight=\"0\" marginwidth=\"0\" scrolling=\"no\" srcdoc=\"contents\" vspace=\"0\" width=\"300\"></iframe>"
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/videobyte/videobytetest/exemplary/multi-imp.json b/adapters/videobyte/videobytetest/exemplary/multi-imp.json
new file mode 100644
index 00000000..547a86da
--- /dev/null
+++ b/adapters/videobyte/videobytetest/exemplary/multi-imp.json
@@ -0,0 +1,277 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id-1",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId-1",
+            "placementId": "examplePlacementId-1",
+            "nid": "exampleNetworkId-1"
+          }
+        }
+      },
+      {
+        "id": "test-imp-id-2",
+        "video": {
+          "w": 800,
+          "h": 150,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId-2",
+            "placementId": "examplePlacementId-2",
+            "nid": "exampleNetworkId-2"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Referer": [
+            "http://referer.com/page-2"
+          ],
+          "Origin": [
+            "example.com"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?nid=exampleNetworkId-1&pid=examplePublisherId-1&placementId=examplePlacementId-1&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id-1",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId-1",
+                  "placementId": "examplePlacementId-1",
+                  "nid": "exampleNetworkId-1"
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "http://example.com/page-1",
+            "ref": "http://referer.com/page-2"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+                  "crid": "72745",
+                  "adomain": [
+                    "ad-domain.com"
+                  ],
+                  "price": 3,
+                  "impid": "test-imp-id-1",
+                  "adid": "564",
+                  "adm": "<?xml version=\"1.0\"?><VAST version=\"2.0\"></VAST>"
+                }
+              ],
+              "seat": "videobyte"
+            }
+          ],
+          "bidid": "test-request-id",
+          "id": "test-request-id"
+        }
+      }
+    },
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Referer": [
+            "http://referer.com/page-2"
+          ],
+          "Origin": [
+            "example.com"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?nid=exampleNetworkId-2&pid=examplePublisherId-2&placementId=examplePlacementId-2&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id-2",
+              "video": {
+                "w": 800,
+                "h": 150,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId-2",
+                  "placementId": "examplePlacementId-2",
+                  "nid": "exampleNetworkId-2"
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "http://example.com/page-1",
+            "ref": "http://referer.com/page-2"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "fe69dd6d-e85c-4186-80a7-605b530bc23c",
+                  "crid": "72746",
+                  "adomain": [
+                    "ad-domain.com"
+                  ],
+                  "price": 4,
+                  "impid": "test-imp-id-2",
+                  "adid": "565",
+                  "adm": "<?xml version=\"1.0\"?><VAST version=\"2.0\"></VAST>"
+                }
+              ],
+              "seat": "videobyte"
+            }
+          ],
+          "bidid": "test-request-id",
+          "id": "test-request-id"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+            "crid": "72745",
+            "adomain": [
+              "ad-domain.com"
+            ],
+            "price": 3,
+            "impid": "test-imp-id-1",
+            "adid": "564",
+            "adm": "<?xml version=\"1.0\"?><VAST version=\"2.0\"></VAST>"
+          },
+          "type": "video"
+        }
+      ]
+    },
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "fe69dd6d-e85c-4186-80a7-605b530bc23c",
+            "crid": "72746",
+            "adomain": [
+              "ad-domain.com"
+            ],
+            "price": 4,
+            "impid": "test-imp-id-2",
+            "adid": "565",
+            "adm": "<?xml version=\"1.0\"?><VAST version=\"2.0\"></VAST>"
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/videobyte/videobytetest/exemplary/video.json b/adapters/videobyte/videobytetest/exemplary/video.json
new file mode 100644
index 00000000..7cb07808
--- /dev/null
+++ b/adapters/videobyte/videobytetest/exemplary/video.json
@@ -0,0 +1,153 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId",
+            "placementId": "examplePlacementId",
+            "nid": "exampleNetworkId"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Referer": [
+            "http://referer.com/page-2"
+          ],
+          "Origin": [
+            "example.com"
+          ],
+          "Accept": [
+            "application/json"
+          ],
+          "Content-Type": [
+            "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?nid=exampleNetworkId&pid=examplePublisherId&placementId=examplePlacementId&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId",
+                  "placementId": "examplePlacementId",
+                  "nid": "exampleNetworkId"
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "http://example.com/page-1",
+            "ref": "http://referer.com/page-2"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "cur": "USD",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+                  "crid": "72745",
+                  "adomain": [
+                    "ad-domain.com"
+                  ],
+                  "price": 3,
+                  "impid": "test-imp-id",
+                  "adid": "564",
+                  "adm": "<?xml version=\"1.0\"?><VAST version=\"2.0\"></VAST>"
+                }
+              ],
+              "seat": "videobyte"
+            }
+          ],
+          "bidid": "test-request-id",
+          "id": "test-request-id"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "fe69dd6d-e85c-4186-80a7-605b530bc23b",
+            "crid": "72745",
+            "adomain": [
+              "ad-domain.com"
+            ],
+            "price": 3,
+            "impid": "test-imp-id",
+            "adid": "564",
+            "adm": "<?xml version=\"1.0\"?><VAST version=\"2.0\"></VAST>"
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/videobyte/videobytetest/supplemental/invalid-imp-ext-bidder.json b/adapters/videobyte/videobytetest/supplemental/invalid-imp-ext-bidder.json
new file mode 100644
index 00000000..1b806ed5
--- /dev/null
+++ b/adapters/videobyte/videobytetest/supplemental/invalid-imp-ext-bidder.json
@@ -0,0 +1,45 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": "invalid-bidder-ext"
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Ignoring imp id=test-imp-id, error while decoding impExt, err: json: cannot unmarshal string into Go value of type openrtb_ext.ExtImpVideoByte",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/videobyte/videobytetest/supplemental/invalid-imp-ext.json b/adapters/videobyte/videobytetest/supplemental/invalid-imp-ext.json
new file mode 100644
index 00000000..199f4c55
--- /dev/null
+++ b/adapters/videobyte/videobytetest/supplemental/invalid-imp-ext.json
@@ -0,0 +1,43 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": "invalid-ext"
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Ignoring imp id=test-imp-id, error while decoding extImpBidder, err: json: cannot unmarshal string into Go value of type adapters.ExtImpBidder",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/videobyte/videobytetest/supplemental/invalid-response.json b/adapters/videobyte/videobytetest/supplemental/invalid-response.json
new file mode 100644
index 00000000..35bce9a7
--- /dev/null
+++ b/adapters/videobyte/videobytetest/supplemental/invalid-response.json
@@ -0,0 +1,116 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId",
+            "placementId": "examplePlacementId",
+            "nid": "exampleNetworkId"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Referer": [
+            "http://referer.com/page-2"
+          ],
+          "Origin": [
+            "example.com"
+          ],
+          "Accept": [
+           "application/json"
+          ],
+          "Content-Type": [
+           "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?nid=exampleNetworkId&pid=examplePublisherId&placementId=examplePlacementId&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId",
+                  "placementId": "examplePlacementId",
+                  "nid": "exampleNetworkId"
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "http://example.com/page-1",
+            "ref": "http://referer.com/page-2"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": "invalid-body"
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Bad Server Response",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/videobyte/videobytetest/supplemental/status-code-bad-request.json b/adapters/videobyte/videobytetest/supplemental/status-code-bad-request.json
new file mode 100644
index 00000000..09ad1bbb
--- /dev/null
+++ b/adapters/videobyte/videobytetest/supplemental/status-code-bad-request.json
@@ -0,0 +1,115 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId",
+            "placementId": "examplePlacementId",
+            "nid": "exampleNetworkId"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Referer": [
+            "http://referer.com/page-2"
+          ],
+          "Origin": [
+            "example.com"
+          ],
+          "Accept": [
+           "application/json"
+          ],
+          "Content-Type": [
+           "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?nid=exampleNetworkId&pid=examplePublisherId&placementId=examplePlacementId&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId",
+                  "placementId": "examplePlacementId",
+                  "nid": "exampleNetworkId"
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "http://example.com/page-1",
+            "ref": "http://referer.com/page-2"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 400
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Bad user input: HTTP status 400. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/videobyte/videobytetest/supplemental/status-code-no-content.json b/adapters/videobyte/videobytetest/supplemental/status-code-no-content.json
new file mode 100644
index 00000000..eaa61c74
--- /dev/null
+++ b/adapters/videobyte/videobytetest/supplemental/status-code-no-content.json
@@ -0,0 +1,111 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId",
+            "placementId": "examplePlacementId",
+            "nid": "exampleNetworkId"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Referer": [
+            "http://referer.com/page-2"
+          ],
+          "Origin": [
+            "example.com"
+          ],
+          "Accept": [
+           "application/json"
+          ],
+          "Content-Type": [
+           "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?nid=exampleNetworkId&pid=examplePublisherId&placementId=examplePlacementId&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId",
+                  "placementId": "examplePlacementId",
+                  "nid": "exampleNetworkId"
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "http://example.com/page-1",
+            "ref": "http://referer.com/page-2"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 204
+      }
+    }
+  ],
+  "expectedBidResponses": [],
+  "expectedMakeBidsErrors": []
+}
diff --git a/adapters/videobyte/videobytetest/supplemental/status-code-other-error.json b/adapters/videobyte/videobytetest/supplemental/status-code-other-error.json
new file mode 100644
index 00000000..d8a1c1f4
--- /dev/null
+++ b/adapters/videobyte/videobytetest/supplemental/status-code-other-error.json
@@ -0,0 +1,115 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "bcat": [
+      "IAB25",
+      "IAB7-39",
+      "IAB8-18"
+    ],
+    "user": {
+      "buyeruid": "user-101",
+      "yob": 1973
+    },
+    "device": {
+      "ua": "my-user-agent",
+      "ip": "1.2.3.4"
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "video": {
+          "w": 900,
+          "h": 250,
+          "mimes": [
+            "video/x-flv",
+            "video/mp4"
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "pubId": "examplePublisherId",
+            "placementId": "examplePlacementId",
+            "nid": "exampleNetworkId"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "http://example.com/page-1",
+      "ref": "http://referer.com/page-2"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "method": "GET",
+        "headers": {
+          "Referer": [
+            "http://referer.com/page-2"
+          ],
+          "Origin": [
+            "example.com"
+          ],
+          "Accept": [
+           "application/json"
+          ],
+          "Content-Type": [
+           "application/json;charset=utf-8"
+          ]
+        },
+        "uri": "https://mock.videobyte.com?nid=exampleNetworkId&pid=examplePublisherId&placementId=examplePlacementId&source=pbs",
+        "body": {
+          "id": "test-request-id",
+          "bcat": [
+            "IAB25",
+            "IAB7-39",
+            "IAB8-18"
+          ],
+          "user": {
+            "buyeruid": "user-101",
+            "yob": 1973
+          },
+          "device": {
+            "ua": "my-user-agent",
+            "ip": "1.2.3.4"
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "video": {
+                "w": 900,
+                "h": 250,
+                "mimes": [
+                  "video/x-flv",
+                  "video/mp4"
+                ]
+              },
+              "ext": {
+                "bidder": {
+                  "pubId": "examplePublisherId",
+                  "placementId": "examplePlacementId",
+                  "nid": "exampleNetworkId"
+                }
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "http://example.com/page-1",
+            "ref": "http://referer.com/page-2"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 505
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 505. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/config/bidderinfo_test.go b/config/bidderinfo_test.go
index 19abdc43..9cf5fe09 100644
--- a/config/bidderinfo_test.go
+++ b/config/bidderinfo_test.go
@@ -226,6 +226,12 @@ func TestSyncerOverride(t *testing.T) {
 			givenOverride: &Syncer{Redirect: &SyncerEndpoint{URL: "override"}},
 			expected:      &Syncer{Redirect: &SyncerEndpoint{URL: "override"}},
 		},
+		{
+			description:   "Override ExternalURL",
+			givenOriginal: &Syncer{ExternalURL: "original"},
+			givenOverride: &Syncer{ExternalURL: "override"},
+			expected:      &Syncer{ExternalURL: "override"},
+		},
 		{
 			description:   "Override SupportCORS",
 			givenOriginal: &Syncer{SupportCORS: &trueValue},
diff --git a/currency/validation_test.go b/currency/validation_test.go
new file mode 100644
index 00000000..d49b9824
--- /dev/null
+++ b/currency/validation_test.go
@@ -0,0 +1,116 @@
+package currency
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidateCustomRates(t *testing.T) {
+	boolTrue := true
+	boolFalse := false
+
+	testCases := []struct {
+		desc               string
+		inBidReqCurrencies *openrtb_ext.ExtRequestCurrency
+		outCurrencyError   error
+	}{
+		{
+			desc:               "nil input, no errors expected",
+			inBidReqCurrencies: nil,
+			outCurrencyError:   nil,
+		},
+		{
+			desc: "empty custom currency rates but UsePBSRates is set to false, we don't return error nor warning",
+			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
+				ConversionRates: map[string]map[string]float64{},
+				UsePBSRates:     &boolFalse,
+			},
+			outCurrencyError: nil,
+		},
+		{
+			desc: "empty custom currency rates but UsePBSRates is set to true, no need to return error because we can use PBS rates",
+			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
+				ConversionRates: map[string]map[string]float64{},
+				UsePBSRates:     &boolTrue,
+			},
+			outCurrencyError: nil,
+		},
+		{
+			desc: "UsePBSRates is nil and defaults to true, bidExt fromCurrency is invalid, expect bad input error",
+			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
+				ConversionRates: map[string]map[string]float64{
+					"FOO": {
+						"GBP": 1.2,
+						"MXN": 0.05,
+						"JPY": 0.95,
+					},
+				},
+			},
+			outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"},
+		},
+		{
+			desc: "UsePBSRates set to false, bidExt fromCurrency is invalid, expect bad input error",
+			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
+				ConversionRates: map[string]map[string]float64{
+					"FOO": {
+						"GBP": 1.2,
+						"MXN": 0.05,
+						"JPY": 0.95,
+					},
+				},
+				UsePBSRates: &boolFalse,
+			},
+			outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"},
+		},
+		{
+			desc: "UsePBSRates set to false, some of the bidExt 'to' Currencies are invalid, expect bad input error when parsing the first invalid currency code",
+			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
+				ConversionRates: map[string]map[string]float64{
+					"USD": {
+						"FOO": 10.0,
+						"MXN": 0.05,
+					},
+				},
+				UsePBSRates: &boolFalse,
+			},
+			outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"},
+		},
+		{
+			desc: "UsePBSRates set to false, some of the bidExt 'from' and 'to' currencies are invalid, expect bad input error when parsing the first invalid currency code",
+			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
+				ConversionRates: map[string]map[string]float64{
+					"FOO": {
+						"MXN": 0.05,
+						"CAD": 0.95,
+					},
+				},
+				UsePBSRates: &boolFalse,
+			},
+			outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"},
+		},
+		{
+			desc: "All 3-digit currency codes exist, expect no error",
+			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
+				ConversionRates: map[string]map[string]float64{
+					"USD": {
+						"MXN": 0.05,
+					},
+					"MXN": {
+						"JPY": 10.0,
+						"EUR": 10.95,
+					},
+				},
+				UsePBSRates: &boolFalse,
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		actualErr := ValidateCustomRates(tc.inBidReqCurrencies)
+
+		assert.Equal(t, tc.outCurrencyError, actualErr, tc.desc)
+	}
+}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 8439cc65..a2f03289 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -1423,113 +1423,6 @@ func TestContentType(t *testing.T) {
 	}
 }
 
-func TestValidateCustomRates(t *testing.T) {
-	boolTrue := true
-	boolFalse := false
-
-	testCases := []struct {
-		desc               string
-		inBidReqCurrencies *openrtb_ext.ExtRequestCurrency
-		outCurrencyError   error
-	}{
-		{
-			desc:               "nil input, no errors expected",
-			inBidReqCurrencies: nil,
-			outCurrencyError:   nil,
-		},
-		{
-			desc: "empty custom currency rates but UsePBSRates is set to false, we don't return error nor warning",
-			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
-				ConversionRates: map[string]map[string]float64{},
-				UsePBSRates:     &boolFalse,
-			},
-			outCurrencyError: nil,
-		},
-		{
-			desc: "empty custom currency rates but UsePBSRates is set to true, no need to return error because we can use PBS rates",
-			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
-				ConversionRates: map[string]map[string]float64{},
-				UsePBSRates:     &boolTrue,
-			},
-			outCurrencyError: nil,
-		},
-		{
-			desc: "UsePBSRates is nil and defaults to true, bidExt fromCurrency is invalid, expect bad input error",
-			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
-				ConversionRates: map[string]map[string]float64{
-					"FOO": {
-						"GBP": 1.2,
-						"MXN": 0.05,
-						"JPY": 0.95,
-					},
-				},
-			},
-			outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"},
-		},
-		{
-			desc: "UsePBSRates set to false, bidExt fromCurrency is invalid, expect bad input error",
-			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
-				ConversionRates: map[string]map[string]float64{
-					"FOO": {
-						"GBP": 1.2,
-						"MXN": 0.05,
-						"JPY": 0.95,
-					},
-				},
-				UsePBSRates: &boolFalse,
-			},
-			outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"},
-		},
-		{
-			desc: "UsePBSRates set to false, some of the bidExt 'to' Currencies are invalid, expect bad input error when parsing the first invalid currency code",
-			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
-				ConversionRates: map[string]map[string]float64{
-					"USD": {
-						"FOO": 10.0,
-						"MXN": 0.05,
-					},
-				},
-				UsePBSRates: &boolFalse,
-			},
-			outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"},
-		},
-		{
-			desc: "UsePBSRates set to false, some of the bidExt 'from' and 'to' currencies are invalid, expect bad input error when parsing the first invalid currency code",
-			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
-				ConversionRates: map[string]map[string]float64{
-					"FOO": {
-						"MXN": 0.05,
-						"CAD": 0.95,
-					},
-				},
-				UsePBSRates: &boolFalse,
-			},
-			outCurrencyError: &errortypes.BadInput{Message: "currency code FOO is not recognized or malformed"},
-		},
-		{
-			desc: "All 3-digit currency codes exist, expect no error",
-			inBidReqCurrencies: &openrtb_ext.ExtRequestCurrency{
-				ConversionRates: map[string]map[string]float64{
-					"USD": {
-						"MXN": 0.05,
-					},
-					"MXN": {
-						"JPY": 10.0,
-						"EUR": 10.95,
-					},
-				},
-				UsePBSRates: &boolFalse,
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		actualErr := validateCustomRates(tc.inBidReqCurrencies)
-
-		assert.Equal(t, tc.outCurrencyError, actualErr, tc.desc)
-	}
-}
-
 func TestValidateImpExt(t *testing.T) {
 	type testCase struct {
 		description    string
@@ -3915,3 +3808,117 @@ func TestValidateBanner(t *testing.T) {
 		assert.Equal(t, test.expectedError, result, test.description)
 	}
 }
+
+func TestParseRequestMergeBidderParams(t *testing.T) {
+	type args struct {
+		reqBody string
+	}
+	tests := []struct {
+		name           string
+		args           args
+		expectedImpExt json.RawMessage
+		expectedReqExt json.RawMessage
+		errL           int
+	}{
+		{
+			name: "add missing bidder-params from req.ext.prebid.bidderparams to imp[].ext.prebid.bidder",
+			args: args{
+				reqBody: validRequest(t, "req-ext-bidder-params.json"),
+			},
+			expectedImpExt: getObject(t, "req-ext-bidder-params.json", "expectedImpExt"),
+			expectedReqExt: getObject(t, "req-ext-bidder-params.json", "expectedReqExt"),
+			errL:           0,
+		},
+		{
+			name: "add missing bidder-params from req.ext.prebid.bidderparams to imp[].ext.prebid.bidder with preference for imp[].ext.prebid.bidder params",
+			args: args{
+				reqBody: validRequest(t, "req-ext-bidder-params-merge.json"),
+			},
+			expectedImpExt: getObject(t, "req-ext-bidder-params-merge.json", "expectedImpExt"),
+			expectedReqExt: getObject(t, "req-ext-bidder-params-merge.json", "expectedReqExt"),
+			errL:           0,
+		},
+		{
+			name: "add missing bidder-params from req.ext.prebid.bidderparams to imp[].ext for backward compatibility",
+			args: args{
+				reqBody: validRequest(t, "req-ext-bidder-params-backward-compatible-merge.json"),
+			},
+			expectedImpExt: getObject(t, "req-ext-bidder-params-backward-compatible-merge.json", "expectedImpExt"),
+			expectedReqExt: getObject(t, "req-ext-bidder-params-backward-compatible-merge.json", "expectedReqExt"),
+			errL:           0,
+		},
+		{
+			name: "add missing bidder-params from req.ext.prebid.bidderparams to imp[].ext.prebid.bidder for reserved keyword",
+			args: args{
+				reqBody: validRequest(t, "req-ext-bidder-params-merge-reserved-keyword.json"),
+			},
+			expectedImpExt: getObject(t, "req-ext-bidder-params-merge-reserved-keyword.json", "expectedImpExt"),
+			expectedReqExt: getObject(t, "req-ext-bidder-params-merge-reserved-keyword.json", "expectedReqExt"),
+			errL:           0,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			deps := &endpointDeps{
+				fakeUUIDGenerator{},
+				&warningsCheckExchange{},
+				newParamsValidator(t),
+				&mockStoredReqFetcher{},
+				empty_fetcher.EmptyFetcher{},
+				empty_fetcher.EmptyFetcher{},
+				&config.Configuration{MaxRequestSize: int64(len(tt.args.reqBody))},
+				&metricsConfig.NilMetricsEngine{},
+				analyticsConf.NewPBSAnalytics(&config.Analytics{}),
+				map[string]string{},
+				false,
+				[]byte{},
+				openrtb_ext.BuildBidderMap(),
+				nil,
+				nil,
+				hardcodedResponseIPValidator{response: true},
+			}
+
+			req := httptest.NewRequest("POST", "/openrtb2/auction", strings.NewReader(tt.args.reqBody))
+
+			resReq, _, errL := deps.parseRequest(req)
+
+			var expIExt, iExt map[string]interface{}
+			err := json.Unmarshal(tt.expectedImpExt, &expIExt)
+			assert.Nil(t, err, "unmarshal() should return nil error")
+
+			assert.NotNil(t, resReq.BidRequest.Imp[0].Ext, "imp[0].Ext should not be nil")
+			err = json.Unmarshal(resReq.BidRequest.Imp[0].Ext, &iExt)
+			assert.Nil(t, err, "unmarshal() should return nil error")
+
+			assert.Equal(t, expIExt, iExt, "bidderparams in imp[].Ext should match")
+
+			var eReqE, reqE map[string]interface{}
+			err = json.Unmarshal(tt.expectedReqExt, &eReqE)
+			assert.Nil(t, err, "unmarshal() should return nil error")
+
+			err = json.Unmarshal(resReq.BidRequest.Ext, &reqE)
+			assert.Nil(t, err, "unmarshal() should return nil error")
+
+			assert.Equal(t, eReqE, reqE, "req.Ext should match")
+
+			assert.Len(t, errL, tt.errL, "error length should match")
+		})
+	}
+}
+
+func getObject(t *testing.T, filename, key string) json.RawMessage {
+	requestData, err := ioutil.ReadFile("sample-requests/valid-whole/supplementary/" + filename)
+	if err != nil {
+		t.Fatalf("Failed to fetch a valid request: %v", err)
+	}
+	testBidRequest, _, _, err := jsonparser.Get(requestData, key)
+	assert.NoError(t, err, "Error jsonparsing root.mockBidRequest from file %s. Desc: %v.", filename, err)
+
+	var obj json.RawMessage
+	err = json.Unmarshal(testBidRequest, &obj)
+	if err != nil {
+		t.Fatalf("Failed to fetch object with key '%s' ... got error: %v", key, err)
+	}
+	return obj
+}
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index ef7d0bdb..d0b4006d 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -5,12 +5,12 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/prebid/prebid-server/firstpartydata"
 	"testing"
 
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/firstpartydata"
 	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -933,6 +933,90 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) {
 	}
 }
 
+func TestCleanOpenRTBRequestsBidderParams(t *testing.T) {
+	testCases := []struct {
+		description string
+		inExt       json.RawMessage
+		expectedExt map[string]json.RawMessage
+		hasError    bool
+	}{
+		{
+			description: "Nil Bidder params",
+			inExt:       nil,
+			expectedExt: getExpectedReqExt(true, false, false),
+			hasError:    false,
+		},
+		{
+			description: "Bidder params for single partner",
+			inExt:       json.RawMessage(`{"prebid":{"bidderparams": {"pubmatic": {"profile":1234,"version":2}}}}`),
+			expectedExt: getExpectedReqExt(false, true, false),
+			hasError:    false,
+		},
+		{
+			description: "Bidder params for two partners",
+			inExt:       json.RawMessage(`{"prebid":{"bidderparams": {"pubmatic": {"profile":1234,"version":2}, "appnexus": {"key1": 123, "key2": {"innerKey1":"innerValue1"}} }}}`),
+			expectedExt: getExpectedReqExt(false, true, true),
+			hasError:    false,
+		},
+	}
+
+	for _, test := range testCases {
+		req := newBidRequestWithBidderParams(t)
+		var extRequest *openrtb_ext.ExtRequest
+		if test.inExt != nil {
+			req.Ext = test.inExt
+			unmarshaledExt, err := extractBidRequestExt(req)
+			assert.NoErrorf(t, err, test.description+":Error unmarshaling inExt")
+			extRequest = unmarshaledExt
+		}
+
+		auctionReq := AuctionRequest{
+			BidRequest: req,
+			UserSyncs:  &emptyUsersync{},
+		}
+
+		bidderToSyncerKey := map[string]string{}
+		permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
+		metrics := metrics.MetricsEngineMock{}
+		bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, bidderToSyncerKey, &permissions, &metrics, gdpr.SignalNo, config.Privacy{}, nil)
+		if test.hasError == true {
+			assert.NotNil(t, errs)
+			assert.Len(t, bidderRequests, 0)
+		} else {
+			assert.Nil(t, errs)
+			for _, r := range bidderRequests {
+				expected := test.expectedExt[r.BidderName.String()]
+				actual := r.BidRequest.Ext
+				assert.Equal(t, expected, actual, test.description+" Req:Ext.Prebid.BidderParams")
+			}
+		}
+	}
+}
+
+func getExpectedReqExt(nilExt, includePubmaticParams, includeAppnexusParams bool) map[string]json.RawMessage {
+	bidderParamsMap := make(map[string]json.RawMessage)
+
+	if nilExt {
+		bidderParamsMap["pubmatic"] = json.RawMessage(``)
+		bidderParamsMap["appnexus"] = json.RawMessage(``)
+		return bidderParamsMap
+	}
+
+	if includePubmaticParams {
+		bidderParamsMap["pubmatic"] = json.RawMessage(`{"prebid":{"bidderparams":{"profile":1234,"version":2}}}`)
+	} else {
+		bidderParamsMap["pubmatic"] = json.RawMessage(`{"prebid":{}}`)
+	}
+
+	if includeAppnexusParams {
+		bidderParamsMap["appnexus"] = json.RawMessage(`{"prebid":{"bidderparams":{"key1":123,"key2":{"innerKey1":"innerValue1"}}}}`)
+	} else {
+		bidderParamsMap["appnexus"] = json.RawMessage(`{"prebid":{}}`)
+	}
+
+	return bidderParamsMap
+}
+
 func TestExtractBidRequestExt(t *testing.T) {
 	var boolFalse, boolTrue *bool = new(bool), new(bool)
 	*boolFalse = false
@@ -2000,6 +2084,47 @@ func newBidRequest(t *testing.T) *openrtb2.BidRequest {
 	}
 }
 
+func newBidRequestWithBidderParams(t *testing.T) *openrtb2.BidRequest {
+	return &openrtb2.BidRequest{
+		Site: &openrtb2.Site{
+			Page:   "www.some.domain.com",
+			Domain: "domain.com",
+			Publisher: &openrtb2.Publisher{
+				ID: "some-publisher-id",
+			},
+		},
+		Device: &openrtb2.Device{
+			DIDMD5:   "some device ID hash",
+			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",
+			Language: "EN",
+		},
+		Source: &openrtb2.Source{
+			TID: "61018dc9-fa61-4c41-b7dc-f90b9ae80e87",
+		},
+		User: &openrtb2.User{
+			ID:       "our-id",
+			BuyerUID: "their-id",
+			Yob:      1982,
+			Ext:      json.RawMessage(`{}`),
+		},
+		Imp: []openrtb2.Imp{{
+			ID: "some-imp-id",
+			Banner: &openrtb2.Banner{
+				Format: []openrtb2.Format{{
+					W: 300,
+					H: 250,
+				}, {
+					W: 300,
+					H: 600,
+				}},
+			},
+			Ext: json.RawMessage(`{"appnexus": {"placementId": 1}, "pubmatic":{"publisherId": "1234"}}`),
+		}},
+	}
+}
+
 func TestRandomizeList(t *testing.T) {
 	var (
 		bidder1 = openrtb_ext.BidderName("bidder1")
@@ -2609,8 +2734,87 @@ func TestBuildXPrebidHeader(t *testing.T) {
 	}
 }
 
-func TestApplyFPD(t *testing.T) {
+func TestSourceExtSChainCopied(t *testing.T) {
+	bidRequest := newBidRequest(t)
 
+	bidderSchains := map[string]*openrtb_ext.ExtRequestPrebidSChainSChain{
+		"bidder1": {
+			Ver:      "1.0",
+			Complete: 1,
+			Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{
+				{
+					ASI: "bidder1.com",
+					SID: "0001",
+					HP:  1,
+				},
+			},
+		},
+		"bidder2": {
+			Ver:      "1.0",
+			Complete: 1,
+			Nodes: []*openrtb_ext.ExtRequestPrebidSChainSChainNode{
+				{
+					ASI: "bidder2.com",
+					SID: "0002",
+					HP:  1,
+				},
+			},
+		},
+	}
+
+	copy1 := *bidRequest
+	originalTid := copy1.Source.TID
+	prepareSource(&copy1, "bidder1", bidderSchains)
+	copy2 := *bidRequest
+	copy2.Source.TID = "new TID"
+	prepareSource(&copy2, "bidder2", bidderSchains)
+
+	assert.Equal(t, json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"bidder1.com","sid":"0001","hp":1}],"ver":"1.0"}}`), copy1.Source.Ext, "First schain was overwritten or not set")
+	assert.Equal(t, originalTid, copy1.Source.TID, "Original TID was overwritten")
+	assert.Equal(t, json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"bidder2.com","sid":"0002","hp":1}],"ver":"1.0"}}`), copy2.Source.Ext, "Second schain was overwritten or not set")
+	assert.Equal(t, "new TID", copy2.Source.TID, "New TID was not set")
+}
+
+func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) {
+	req := &openrtb2.BidRequest{
+		Site: &openrtb2.Site{},
+		Source: &openrtb2.Source{
+			TID: "61018dc9-fa61-4c41-b7dc-f90b9ae80e87",
+		},
+		Imp: []openrtb2.Imp{{
+			Ext: json.RawMessage(`{"appnexus": {"placementId": 1}, "axonix": { "supplyId": "123"}}`),
+		}},
+		Ext: json.RawMessage(`{"prebid":{"schains":[{ "bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["axonix"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`),
+	}
+
+	unmarshaledExt, err := extractBidRequestExt(req)
+	assert.NoErrorf(t, err, "Error unmarshaling inExt")
+	extRequest := unmarshaledExt
+
+	auctionReq := AuctionRequest{
+		BidRequest: req,
+		UserSyncs:  &emptyUsersync{},
+	}
+	bidderToSyncerKey := map[string]string{}
+	permissions := permissionsMock{allowAllBidders: true, passGeo: true, passID: true}
+	metrics := metrics.MetricsEngineMock{}
+	bidderRequests, _, errs := cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, bidderToSyncerKey, &permissions, &metrics, gdpr.SignalNo, config.Privacy{}, nil)
+
+	assert.Nil(t, errs)
+	assert.Len(t, bidderRequests, 2, "Bid request count is not 2")
+
+	bidRequestSourceExts := map[openrtb_ext.BidderName]json.RawMessage{}
+	for _, bidderRequest := range bidderRequests {
+		bidRequestSourceExts[bidderRequest.BidderName] = bidderRequest.BidRequest.Source.Ext
+	}
+
+	appnexusPrebidSchainsSchain := json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}`)
+	axonixPrebidSchainsSchain := json.RawMessage(`{"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}`)
+	assert.Equal(t, appnexusPrebidSchainsSchain, bidRequestSourceExts["appnexus"], "Incorrect appnexus bid request schain in source.ext")
+	assert.Equal(t, axonixPrebidSchainsSchain, bidRequestSourceExts["axonix"], "Incorrect axonix bid request schain in source.ext")
+}
+
+func TestApplyFPD(t *testing.T) {
 	fpdBidderTest := map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData{}
 	bidderTest := openrtb_ext.BidderName("test")
 	fpdBidderTest[bidderTest] = &firstpartydata.ResolvedFirstPartyData{Site: nil, App: nil, User: nil}
diff --git a/openrtb_ext/bidders_test.go b/openrtb_ext/bidders_test.go
index 26ebf7dc..8bd25d9e 100644
--- a/openrtb_ext/bidders_test.go
+++ b/openrtb_ext/bidders_test.go
@@ -110,6 +110,9 @@ func TestIsBidderNameReserved(t *testing.T) {
 		{"prebid", true},
 		{"PREbid", true},
 		{"PREBID", true},
+		{"bidder", true},
+		{"BIDDER", true},
+		{"BidDer", true},
 		{"notreserved", false},
 	}
 
diff --git a/usersync/syncer_test.go b/usersync/syncer_test.go
index 2f4e3203..6ff23009 100644
--- a/usersync/syncer_test.go
+++ b/usersync/syncer_test.go
@@ -29,6 +29,7 @@ func TestNewSyncer(t *testing.T) {
 		givenDefault        string
 		givenIFrameConfig   *config.SyncerEndpoint
 		givenRedirectConfig *config.SyncerEndpoint
+		givenExternalURL    string
 		expectedError       string
 		expectedDefault     SyncType
 		expectedIFrame      string
@@ -100,6 +101,17 @@ func TestNewSyncer(t *testing.T) {
 			givenRedirectConfig: errInvalidConfig,
 			expectedError:       "redirect composed url: \"notAURL:http%3A%2F%2Fhost.com%2Fhost\" is invalid",
 		},
+		{
+			description:         "Syncer Level External URL",
+			givenKey:            "a",
+			givenDefault:        "iframe",
+			givenExternalURL:    "http://syncer.com",
+			givenIFrameConfig:   iframeConfig,
+			givenRedirectConfig: redirectConfig,
+			expectedDefault:     SyncTypeIFrame,
+			expectedIFrame:      "https://bidder.com/iframe?redirect=http%3A%2F%2Fsyncer.com%2Fhost",
+			expectedRedirect:    "https://bidder.com/redirect?redirect=http%3A%2F%2Fsyncer.com%2Fhost",
+		},
 	}
 
 	for _, test := range testCases {
@@ -109,6 +121,7 @@ func TestNewSyncer(t *testing.T) {
 			Default:     test.givenDefault,
 			IFrame:      test.givenIFrameConfig,
 			Redirect:    test.givenRedirectConfig,
+			ExternalURL: test.givenExternalURL,
 		}
 
 		result, err := NewSyncer(hostConfig, syncerConfig)
@@ -232,11 +245,12 @@ func TestBuildTemplate(t *testing.T) {
 	)
 
 	testCases := []struct {
-		description         string
-		givenHostConfig     config.UserSync
-		givenSyncerEndpoint config.SyncerEndpoint
-		expectedError       string
-		expectedRendered    string
+		description            string
+		givenHostConfig        config.UserSync
+		givenSyncerExternalURL string
+		givenSyncerEndpoint    config.SyncerEndpoint
+		expectedError          string
+		expectedRendered       string
 	}{
 		{
 			description: "No Composed Macros",
@@ -278,6 +292,23 @@ func TestBuildTemplate(t *testing.T) {
 			},
 			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncer.com%2Fhost",
 		},
+		{
+			description:            "External URL From Syncer Config",
+			givenSyncerExternalURL: "http://syncershared.com",
+			givenSyncerEndpoint: config.SyncerEndpoint{
+				URL: "https://bidder.com/sync?redirect={{.RedirectURL}}",
+			},
+			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncershared.com%2Fhost",
+		},
+		{
+			description:            "External URL From Syncer Config (Most Specific Wins)",
+			givenSyncerExternalURL: "http://syncershared.com",
+			givenSyncerEndpoint: config.SyncerEndpoint{
+				URL:         "https://bidder.com/sync?redirect={{.RedirectURL}}",
+				ExternalURL: "http://syncer.com",
+			},
+			expectedRendered: "https://bidder.com/sync?redirect=http%3A%2F%2Fsyncer.com%2Fhost",
+		},
 		{
 			description: "Template Parse Error",
 			givenSyncerEndpoint: config.SyncerEndpoint{
@@ -339,7 +370,7 @@ func TestBuildTemplate(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		result, err := buildTemplate(key, syncTypeValue, hostConfig, test.givenSyncerEndpoint)
+		result, err := buildTemplate(key, syncTypeValue, hostConfig, test.givenSyncerExternalURL, test.givenSyncerEndpoint)
 
 		if test.expectedError == "" {
 			assert.NoError(t, err, test.description+":err")
@@ -353,6 +384,50 @@ func TestBuildTemplate(t *testing.T) {
 	}
 }
 
+func TestChooseExternalURL(t *testing.T) {
+	testCases := []struct {
+		description            string
+		givenSyncerEndpointURL string
+		givenSyncerURL         string
+		givenHostConfigURL     string
+		expected               string
+	}{
+		{
+			description:            "Syncer Endpoint Chosen",
+			givenSyncerEndpointURL: "a",
+			givenSyncerURL:         "b",
+			givenHostConfigURL:     "c",
+			expected:               "a",
+		},
+		{
+			description:            "Syncer Chosen",
+			givenSyncerEndpointURL: "",
+			givenSyncerURL:         "b",
+			givenHostConfigURL:     "c",
+			expected:               "b",
+		},
+		{
+			description:            "Host Config Chosen",
+			givenSyncerEndpointURL: "",
+			givenSyncerURL:         "",
+			givenHostConfigURL:     "c",
+			expected:               "c",
+		},
+		{
+			description:            "All Empty",
+			givenSyncerEndpointURL: "",
+			givenSyncerURL:         "",
+			givenHostConfigURL:     "",
+			expected:               "",
+		},
+	}
+
+	for _, test := range testCases {
+		result := chooseExternalURL(test.givenSyncerEndpointURL, test.givenSyncerURL, test.givenHostConfigURL)
+		assert.Equal(t, test.expected, result, test.description)
+	}
+}
+
 func TestEscapeTemplate(t *testing.T) {
 	testCases := []struct {
 		description string