diff --git a/adapters/hypelab/hypelab.go b/adapters/hypelab/hypelab.go
new file mode 100644
index 00000000000..c32c0d73fa9
--- /dev/null
+++ b/adapters/hypelab/hypelab.go
@@ -0,0 +1,303 @@
+package hypelab
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v4/adapters"
+ "github.com/prebid/prebid-server/v4/config"
+ "github.com/prebid/prebid-server/v4/errortypes"
+ "github.com/prebid/prebid-server/v4/openrtb_ext"
+ "github.com/prebid/prebid-server/v4/util/jsonutil"
+ "github.com/prebid/prebid-server/v4/version"
+)
+
+const (
+ displayManager = "HypeLab Prebid Server"
+ source = "prebid-server"
+)
+
+type adapter struct {
+ endpoint string
+}
+
+type bidExt struct {
+ HypeLab *hypeLabBidExt `json:"hypelab,omitempty"`
+}
+
+type hypeLabBidExt struct {
+ CreativeType string `json:"creative_type,omitempty"`
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+ return &adapter{endpoint: config.Endpoint}, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ outgoingRequest, errs := makeOutgoingRequest(request)
+ if len(outgoingRequest.Imp) == 0 {
+ return nil, errs
+ }
+
+ body, err := jsonutil.Marshal(outgoingRequest)
+ if err != nil {
+ return nil, append(errs, err)
+ }
+
+ headers := http.Header{}
+ headers.Add("Accept", "application/json")
+ headers.Add("Content-Type", "application/json;charset=utf-8")
+ headers.Add("X-OpenRTB-Version", "2.6")
+
+ return []*adapters.RequestData{{
+ Method: http.MethodPost,
+ Uri: a.endpoint,
+ Body: body,
+ Headers: headers,
+ ImpIDs: openrtb_ext.GetImpIDs(outgoingRequest.Imp),
+ }}, errs
+}
+
+func makeOutgoingRequest(request *openrtb2.BidRequest) (openrtb2.BidRequest, []error) {
+ requestCopy := *request
+ requestCopy.Imp = make([]openrtb2.Imp, 0, len(request.Imp))
+
+ var errs []error
+ for _, imp := range request.Imp {
+ updatedImp, err := makeOutgoingImp(imp)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ requestCopy.Imp = append(requestCopy.Imp, updatedImp)
+ }
+
+ if err := setRequestExt(&requestCopy); err != nil {
+ errs = append(errs, err)
+ }
+
+ return requestCopy, errs
+}
+
+func makeOutgoingImp(imp openrtb2.Imp) (openrtb2.Imp, error) {
+ params, err := getImpParams(imp)
+ if err != nil {
+ return imp, err
+ }
+
+ imp.TagID = params.PlacementSlug
+ imp.DisplayManager = displayManager
+ imp.DisplayManagerVer = prebidServerVersion()
+
+ imp.Ext, err = jsonutil.Marshal(map[string]openrtb_ext.ExtImpHypeLab{
+ "bidder": params,
+ })
+ if err != nil {
+ return imp, err
+ }
+
+ return imp, nil
+}
+
+func getImpParams(imp openrtb2.Imp) (openrtb_ext.ExtImpHypeLab, error) {
+ var ext adapters.ExtImpBidder
+ if err := jsonutil.Unmarshal(imp.Ext, &ext); err != nil {
+ return openrtb_ext.ExtImpHypeLab{}, &errortypes.BadInput{
+ Message: fmt.Sprintf("imp %s: unable to unmarshal ext", imp.ID),
+ }
+ }
+
+ var params openrtb_ext.ExtImpHypeLab
+ if err := jsonutil.Unmarshal(ext.Bidder, ¶ms); err != nil {
+ return openrtb_ext.ExtImpHypeLab{}, &errortypes.BadInput{
+ Message: fmt.Sprintf("imp %s: unable to unmarshal ext.bidder", imp.ID),
+ }
+ }
+
+ if params.PropertySlug == "" || params.PlacementSlug == "" {
+ return openrtb_ext.ExtImpHypeLab{}, &errortypes.BadInput{
+ Message: fmt.Sprintf("imp %s: property_slug and placement_slug are required", imp.ID),
+ }
+ }
+
+ return params, nil
+}
+
+func setRequestExt(request *openrtb2.BidRequest) error {
+ var ext map[string]json.RawMessage
+ if len(request.Ext) > 0 {
+ if err := jsonutil.Unmarshal(request.Ext, &ext); err != nil {
+ return err
+ }
+ }
+ if ext == nil {
+ ext = map[string]json.RawMessage{}
+ }
+
+ sourceJSON, err := jsonutil.Marshal(source)
+ if err != nil {
+ return err
+ }
+ providerVersionJSON, err := jsonutil.Marshal("prebid-server@" + prebidServerVersion())
+ if err != nil {
+ return err
+ }
+
+ ext["source"] = sourceJSON
+ ext["provider_version"] = providerVersionJSON
+
+ request.Ext, err = jsonutil.Marshal(ext)
+ return err
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+ if adapters.IsResponseStatusCodeNoContent(responseData) {
+ return nil, nil
+ }
+
+ if err := adapters.CheckResponseStatusCodeForErrors(responseData); err != nil {
+ return nil, []error{err}
+ }
+
+ var response openrtb2.BidResponse
+ if err := jsonutil.Unmarshal(responseData.Body, &response); err != nil {
+ return nil, []error{err}
+ }
+
+ impLookup := makeImpLookup(request.Imp)
+ bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+ bidResponse.Currency = response.Cur
+
+ var errs []error
+ for _, seatBid := range response.SeatBid {
+ for i := range seatBid.Bid {
+ bidType, err := getBidMediaType(&seatBid.Bid[i], impLookup)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+
+ typedBid := &adapters.TypedBid{
+ Bid: &seatBid.Bid[i],
+ BidType: bidType,
+ }
+ if seatBid.Seat != "" {
+ typedBid.Seat = openrtb_ext.BidderName(seatBid.Seat)
+ }
+ bidResponse.Bids = append(bidResponse.Bids, typedBid)
+ }
+ }
+
+ return bidResponse, errs
+}
+
+func makeImpLookup(imps []openrtb2.Imp) map[string]openrtb2.Imp {
+ lookup := make(map[string]openrtb2.Imp, len(imps))
+ for _, imp := range imps {
+ lookup[imp.ID] = imp
+ }
+ return lookup
+}
+
+func getBidMediaType(bid *openrtb2.Bid, impLookup map[string]openrtb2.Imp) (openrtb_ext.BidType, error) {
+ imp, ok := impLookup[bid.ImpID]
+ if !ok {
+ return "", &errortypes.BadServerResponse{
+ Message: fmt.Sprintf("bid %s references unknown imp %s", bid.ID, bid.ImpID),
+ }
+ }
+
+ switch bid.MType {
+ case openrtb2.MarkupBanner:
+ return openrtb_ext.BidTypeBanner, nil
+ case openrtb2.MarkupVideo:
+ return openrtb_ext.BidTypeVideo, nil
+ case openrtb2.MarkupNative:
+ return openrtb_ext.BidTypeNative, nil
+ }
+ if bid.MType != 0 {
+ return "", &errortypes.BadServerResponse{
+ Message: fmt.Sprintf("bid %s uses unsupported mtype %d", bid.ID, bid.MType),
+ }
+ }
+
+ bidType, found, err := getBidMediaTypeFromExt(bid)
+ if err != nil {
+ return "", err
+ }
+ if found {
+ return bidType, nil
+ }
+
+ if strings.HasPrefix(strings.TrimSpace(bid.AdM), ""},
+ expected: openrtb_ext.BidTypeVideo,
+ },
+ {
+ name: "single media type fallback",
+ bid: openrtb2.Bid{ID: "bid", ImpID: "banner"},
+ expected: openrtb_ext.BidTypeBanner,
+ },
+ {
+ name: "ext without hypelab falls back to imp",
+ bid: openrtb2.Bid{ID: "bid", ImpID: "banner", Ext: json.RawMessage(`{"other":{}}`)},
+ expected: openrtb_ext.BidTypeBanner,
+ },
+ {
+ name: "ambiguous multiformat fallback",
+ bid: openrtb2.Bid{ID: "bid", ImpID: "multi"},
+ expectedError: "unable to determine media type for bid bid on imp multi",
+ },
+ {
+ name: "unknown imp",
+ bid: openrtb2.Bid{ID: "bid", ImpID: "unknown", MType: openrtb2.MarkupBanner},
+ expectedError: "bid bid references unknown imp unknown",
+ },
+ {
+ name: "no media type fallback",
+ bid: openrtb2.Bid{ID: "bid", ImpID: "empty"},
+ expectedError: "unable to determine media type for bid bid on imp empty",
+ },
+ }
+
+ for _, test := range testCases {
+ t.Run(test.name, func(t *testing.T) {
+ result, err := getBidMediaType(&test.bid, impLookup)
+
+ if test.expectedError != "" {
+ require.Error(t, err)
+ assert.EqualError(t, err, test.expectedError)
+ return
+ }
+
+ require.NoError(t, err)
+ assert.Equal(t, test.expected, result)
+ })
+ }
+}
+
+func TestPrebidServerVersion(t *testing.T) {
+ originalVersion := version.Ver
+ t.Cleanup(func() {
+ version.Ver = originalVersion
+ })
+
+ version.Ver = "test-version"
+ assert.Equal(t, "test-version", prebidServerVersion())
+
+ version.Ver = ""
+ assert.Equal(t, version.VerUnknown, prebidServerVersion())
+}
diff --git a/adapters/hypelab/hypelabtest/exemplary/fullscreen-video.json b/adapters/hypelab/hypelabtest/exemplary/fullscreen-video.json
new file mode 100644
index 00000000000..35aa524bace
--- /dev/null
+++ b/adapters/hypelab/hypelabtest/exemplary/fullscreen-video.json
@@ -0,0 +1,121 @@
+{
+ "mockBidRequest": {
+ "id": "test-video-request-id",
+ "imp": [
+ {
+ "id": "video-imp-id",
+ "instl": 1,
+ "video": {
+ "mimes": ["video/mp4"],
+ "w": 1080,
+ "h": 1920
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "interstitial-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/video"
+ }
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://api.hypelab.com/v1/rtb_requests",
+ "headers": {
+ "Accept": ["application/json"],
+ "Content-Type": ["application/json;charset=utf-8"],
+ "X-Openrtb-Version": ["2.6"]
+ },
+ "body": {
+ "id": "test-video-request-id",
+ "imp": [
+ {
+ "id": "video-imp-id",
+ "tagid": "interstitial-placement",
+ "displaymanager": "HypeLab Prebid Server",
+ "displaymanagerver": "unknown",
+ "instl": 1,
+ "video": {
+ "mimes": ["video/mp4"],
+ "w": 1080,
+ "h": 1920
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "interstitial-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/video"
+ },
+ "ext": {
+ "source": "prebid-server",
+ "provider_version": "prebid-server@unknown"
+ }
+ },
+ "impIDs": ["video-imp-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-video-request-id",
+ "seatbid": [
+ {
+ "seat": "hypelab",
+ "bid": [
+ {
+ "id": "video-bid-id",
+ "impid": "video-imp-id",
+ "price": 1.25,
+ "adm": "",
+ "adomain": ["advertiser.example"],
+ "mtype": 2,
+ "ext": {
+ "hypelab": {
+ "creative_type": "video",
+ "native_render": false
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "seat": "hypelab",
+ "type": "video",
+ "bid": {
+ "id": "video-bid-id",
+ "impid": "video-imp-id",
+ "price": 1.25,
+ "adm": "",
+ "adomain": ["advertiser.example"],
+ "mtype": 2,
+ "ext": {
+ "hypelab": {
+ "creative_type": "video",
+ "native_render": false
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/hypelab/hypelabtest/exemplary/native.json b/adapters/hypelab/hypelabtest/exemplary/native.json
new file mode 100644
index 00000000000..9469c48c92d
--- /dev/null
+++ b/adapters/hypelab/hypelabtest/exemplary/native.json
@@ -0,0 +1,103 @@
+{
+ "mockBidRequest": {
+ "id": "test-native-request-id",
+ "imp": [
+ {
+ "id": "native-imp-id",
+ "native": {
+ "request": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"title\":{\"len\":90}}]}}"
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "native-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/native"
+ }
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://api.hypelab.com/v1/rtb_requests",
+ "headers": {
+ "Accept": ["application/json"],
+ "Content-Type": ["application/json;charset=utf-8"],
+ "X-Openrtb-Version": ["2.6"]
+ },
+ "body": {
+ "id": "test-native-request-id",
+ "imp": [
+ {
+ "id": "native-imp-id",
+ "tagid": "native-placement",
+ "displaymanager": "HypeLab Prebid Server",
+ "displaymanagerver": "unknown",
+ "native": {
+ "request": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"title\":{\"len\":90}}]}}"
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "native-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/native"
+ },
+ "ext": {
+ "source": "prebid-server",
+ "provider_version": "prebid-server@unknown"
+ }
+ },
+ "impIDs": ["native-imp-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-native-request-id",
+ "seatbid": [
+ {
+ "seat": "hypelab",
+ "bid": [
+ {
+ "id": "native-bid-id",
+ "impid": "native-imp-id",
+ "price": 0.75,
+ "adm": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"title\":{\"text\":\"Test Native Ad\"}}]}}",
+ "adomain": ["advertiser.example"],
+ "mtype": 4
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "seat": "hypelab",
+ "type": "native",
+ "bid": {
+ "id": "native-bid-id",
+ "impid": "native-imp-id",
+ "price": 0.75,
+ "adm": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"title\":{\"text\":\"Test Native Ad\"}}]}}",
+ "adomain": ["advertiser.example"],
+ "mtype": 4
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/hypelab/hypelabtest/exemplary/simple-banner.json b/adapters/hypelab/hypelabtest/exemplary/simple-banner.json
new file mode 100644
index 00000000000..daceae08845
--- /dev/null
+++ b/adapters/hypelab/hypelabtest/exemplary/simple-banner.json
@@ -0,0 +1,141 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [{ "w": 300, "h": 250 }]
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "test-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/article",
+ "ref": "https://referrer.example/"
+ },
+ "device": {
+ "ua": "Mozilla/5.0",
+ "ip": "192.0.2.1",
+ "language": "en"
+ },
+ "user": {
+ "buyeruid": "test-user-id"
+ },
+ "regs": {
+ "gdpr": 0
+ }
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://api.hypelab.com/v1/rtb_requests",
+ "headers": {
+ "Accept": ["application/json"],
+ "Content-Type": ["application/json;charset=utf-8"],
+ "X-Openrtb-Version": ["2.6"]
+ },
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "test-placement",
+ "displaymanager": "HypeLab Prebid Server",
+ "displaymanagerver": "unknown",
+ "banner": {
+ "format": [{ "w": 300, "h": 250 }]
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "test-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/article",
+ "ref": "https://referrer.example/"
+ },
+ "device": {
+ "ua": "Mozilla/5.0",
+ "ip": "192.0.2.1",
+ "language": "en"
+ },
+ "user": {
+ "buyeruid": "test-user-id"
+ },
+ "regs": {
+ "gdpr": 0
+ },
+ "ext": {
+ "source": "prebid-server",
+ "provider_version": "prebid-server@unknown"
+ }
+ },
+ "impIDs": ["test-imp-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "seatbid": [
+ {
+ "seat": "hypelab",
+ "bid": [
+ {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "ad",
+ "adomain": ["advertiser.example"],
+ "cid": "test-campaign",
+ "crid": "test-creative",
+ "w": 300,
+ "h": 250,
+ "mtype": 1,
+ "ext": {
+ "curl": "https://api.hypelab.com/click"
+ }
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "seat": "hypelab",
+ "type": "banner",
+ "bid": {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "ad",
+ "adomain": ["advertiser.example"],
+ "cid": "test-campaign",
+ "crid": "test-creative",
+ "w": 300,
+ "h": 250,
+ "mtype": 1,
+ "ext": {
+ "curl": "https://api.hypelab.com/click"
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/hypelab/hypelabtest/supplemental/204-response.json b/adapters/hypelab/hypelabtest/supplemental/204-response.json
new file mode 100644
index 00000000000..b888642f3f9
--- /dev/null
+++ b/adapters/hypelab/hypelabtest/supplemental/204-response.json
@@ -0,0 +1,65 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [{ "w": 300, "h": 250 }]
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "test-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/article"
+ }
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://api.hypelab.com/v1/rtb_requests",
+ "headers": {
+ "Accept": ["application/json"],
+ "Content-Type": ["application/json;charset=utf-8"],
+ "X-Openrtb-Version": ["2.6"]
+ },
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "test-placement",
+ "displaymanager": "HypeLab Prebid Server",
+ "displaymanagerver": "unknown",
+ "banner": {
+ "format": [{ "w": 300, "h": 250 }]
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "test-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/article"
+ },
+ "ext": {
+ "source": "prebid-server",
+ "provider_version": "prebid-server@unknown"
+ }
+ },
+ "impIDs": ["test-imp-id"]
+ },
+ "mockResponse": {
+ "status": 204
+ }
+ }
+ ]
+}
diff --git a/adapters/hypelab/hypelabtest/supplemental/500-response.json b/adapters/hypelab/hypelabtest/supplemental/500-response.json
new file mode 100644
index 00000000000..b8f05c35b9c
--- /dev/null
+++ b/adapters/hypelab/hypelabtest/supplemental/500-response.json
@@ -0,0 +1,70 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [{ "w": 300, "h": 250 }]
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "test-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/article"
+ }
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://api.hypelab.com/v1/rtb_requests",
+ "headers": {
+ "Accept": ["application/json"],
+ "Content-Type": ["application/json;charset=utf-8"],
+ "X-Openrtb-Version": ["2.6"]
+ },
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "test-placement",
+ "displaymanager": "HypeLab Prebid Server",
+ "displaymanagerver": "unknown",
+ "banner": {
+ "format": [{ "w": 300, "h": 250 }]
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "test-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/article"
+ },
+ "ext": {
+ "source": "prebid-server",
+ "provider_version": "prebid-server@unknown"
+ }
+ },
+ "impIDs": ["test-imp-id"]
+ },
+ "mockResponse": {
+ "status": 500
+ }
+ }
+ ],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "Unexpected status code: 500. Run with request.debug = 1 for more info"
+ }
+ ]
+}
diff --git a/adapters/hypelab/hypelabtest/supplemental/fullscreen-display-without-mtype.json b/adapters/hypelab/hypelabtest/supplemental/fullscreen-display-without-mtype.json
new file mode 100644
index 00000000000..3875911e9cb
--- /dev/null
+++ b/adapters/hypelab/hypelabtest/supplemental/fullscreen-display-without-mtype.json
@@ -0,0 +1,119 @@
+{
+ "mockBidRequest": {
+ "id": "test-display-request-id",
+ "imp": [
+ {
+ "id": "display-imp-id",
+ "instl": 1,
+ "banner": {
+ "format": [{ "w": 1080, "h": 1920 }]
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "interstitial-display-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/display"
+ }
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://api.hypelab.com/v1/rtb_requests",
+ "headers": {
+ "Accept": ["application/json"],
+ "Content-Type": ["application/json;charset=utf-8"],
+ "X-Openrtb-Version": ["2.6"]
+ },
+ "body": {
+ "id": "test-display-request-id",
+ "imp": [
+ {
+ "id": "display-imp-id",
+ "tagid": "interstitial-display-placement",
+ "displaymanager": "HypeLab Prebid Server",
+ "displaymanagerver": "unknown",
+ "instl": 1,
+ "banner": {
+ "format": [{ "w": 1080, "h": 1920 }]
+ },
+ "ext": {
+ "bidder": {
+ "property_slug": "test-property",
+ "placement_slug": "interstitial-display-placement"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "https://publisher.example/display"
+ },
+ "ext": {
+ "source": "prebid-server",
+ "provider_version": "prebid-server@unknown"
+ }
+ },
+ "impIDs": ["display-imp-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-display-request-id",
+ "seatbid": [
+ {
+ "seat": "hypelab",
+ "bid": [
+ {
+ "id": "display-bid-id",
+ "impid": "display-imp-id",
+ "price": 1.10,
+ "adomain": ["advertiser.example"],
+ "w": 1080,
+ "h": 1920,
+ "ext": {
+ "hypelab": {
+ "creative_type": "display",
+ "native_render": true,
+ "image_url": "https://cdn.example/display.png"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "cur": "USD"
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "seat": "hypelab",
+ "type": "banner",
+ "bid": {
+ "id": "display-bid-id",
+ "impid": "display-imp-id",
+ "price": 1.10,
+ "adomain": ["advertiser.example"],
+ "w": 1080,
+ "h": 1920,
+ "ext": {
+ "hypelab": {
+ "creative_type": "display",
+ "native_render": true,
+ "image_url": "https://cdn.example/display.png"
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/hypelab/params_test.go b/adapters/hypelab/params_test.go
new file mode 100644
index 00000000000..2783caf569c
--- /dev/null
+++ b/adapters/hypelab/params_test.go
@@ -0,0 +1,53 @@
+package hypelab
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/prebid/prebid-server/v4/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.BidderHypeLab, 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.BidderHypeLab, json.RawMessage(p)); err == nil {
+ t.Errorf("Schema allowed invalid params: %s", p)
+ }
+ }
+}
+
+var validParams = []string{
+ `{"property_slug":"test-property","placement_slug":"test-placement"}`,
+}
+
+var invalidParams = []string{
+ ``,
+ `null`,
+ `true`,
+ `5`,
+ `[]`,
+ `{}`,
+ `{"property_slug":"","placement_slug":"test-placement"}`,
+ `{"property_slug":"test-property","placement_slug":""}`,
+ `{"property_slug":"test-property"}`,
+ `{"placement_slug":"test-placement"}`,
+ `{"property_slug":123,"placement_slug":"test-placement"}`,
+ `{"property_slug":"test-property","placement_slug":123}`,
+}
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 630fbe348b9..ee7a8d514fb 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -118,6 +118,7 @@ import (
"github.com/prebid/prebid-server/v4/adapters/grid"
"github.com/prebid/prebid-server/v4/adapters/gumgum"
"github.com/prebid/prebid-server/v4/adapters/huaweiads"
+ "github.com/prebid/prebid-server/v4/adapters/hypelab"
"github.com/prebid/prebid-server/v4/adapters/imds"
"github.com/prebid/prebid-server/v4/adapters/impactify"
"github.com/prebid/prebid-server/v4/adapters/improvedigital"
@@ -386,6 +387,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
openrtb_ext.BidderGrid: grid.Builder,
openrtb_ext.BidderGumGum: gumgum.Builder,
openrtb_ext.BidderHuaweiAds: huaweiads.Builder,
+ openrtb_ext.BidderHypeLab: hypelab.Builder,
openrtb_ext.BidderImds: imds.Builder,
openrtb_ext.BidderImpactify: impactify.Builder,
openrtb_ext.BidderImprovedigital: improvedigital.Builder,
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 1b61ff724e2..4c9500c2f06 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -135,6 +135,7 @@ var coreBidderNames []BidderName = []BidderName{
BidderGrid,
BidderGumGum,
BidderHuaweiAds,
+ BidderHypeLab,
BidderImds,
BidderImpactify,
BidderImprovedigital,
@@ -509,6 +510,7 @@ const (
BidderGrid BidderName = "grid"
BidderGumGum BidderName = "gumgum"
BidderHuaweiAds BidderName = "huaweiads"
+ BidderHypeLab BidderName = "hypelab"
BidderImds BidderName = "imds"
BidderImpactify BidderName = "impactify"
BidderImprovedigital BidderName = "improvedigital"
diff --git a/openrtb_ext/imp_hypelab.go b/openrtb_ext/imp_hypelab.go
new file mode 100644
index 00000000000..614f7302c68
--- /dev/null
+++ b/openrtb_ext/imp_hypelab.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ExtImpHypeLab struct {
+ PropertySlug string `json:"property_slug"`
+ PlacementSlug string `json:"placement_slug"`
+}
diff --git a/static/bidder-info/hype.yaml b/static/bidder-info/hype.yaml
new file mode 100644
index 00000000000..f3879d07f6a
--- /dev/null
+++ b/static/bidder-info/hype.yaml
@@ -0,0 +1 @@
+aliasOf: hypelab
diff --git a/static/bidder-info/hypelab.yaml b/static/bidder-info/hypelab.yaml
new file mode 100644
index 00000000000..c76d6053faf
--- /dev/null
+++ b/static/bidder-info/hypelab.yaml
@@ -0,0 +1,15 @@
+endpoint: "https://api.hypelab.com/v1/rtb_requests"
+maintainer:
+ email: "sdk@hypelab.com"
+openrtb:
+ version: 2.6
+capabilities:
+ site:
+ mediaTypes:
+ - banner
+ - native
+ - video
+userSync:
+ redirect:
+ url: "https://api.hypelab.com/v1/i?redirect_url={{.RedirectURL}}&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}"
+ userMacro: "$UID"
diff --git a/static/bidder-params/hypelab.json b/static/bidder-params/hypelab.json
new file mode 100644
index 00000000000..fd90f96adfc
--- /dev/null
+++ b/static/bidder-params/hypelab.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "HypeLab Adapter Params",
+ "description": "A schema which validates params accepted by the HypeLab adapter",
+ "type": "object",
+ "properties": {
+ "property_slug": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HypeLab property slug"
+ },
+ "placement_slug": {
+ "type": "string",
+ "minLength": 1,
+ "description": "HypeLab placement slug"
+ }
+ },
+ "required": ["property_slug", "placement_slug"]
+}