diff --git a/adapters/revantage/params_test.go b/adapters/revantage/params_test.go
new file mode 100644
index 00000000000..8a470823f5e
--- /dev/null
+++ b/adapters/revantage/params_test.go
@@ -0,0 +1,55 @@
+package revantage
+
+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-schemas: %v", err)
+ }
+
+ for _, validParam := range validParams {
+ if err := validator.Validate(openrtb_ext.BidderRevantage, json.RawMessage(validParam)); err != nil {
+ t.Errorf("Schema rejected revantage params that should be valid: %s\nError: %s", validParam, err)
+ }
+ }
+}
+
+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.BidderRevantage, json.RawMessage(invalidParam)); err == nil {
+ t.Errorf("Schema accepted revantage params that should be invalid: %s", invalidParam)
+ }
+ }
+}
+
+var validParams = []string{
+ `{"feedId":"feed-abc"}`,
+ `{"feedId":"feed-abc","placementId":"plc-1"}`,
+ `{"feedId":"feed-abc","placementId":"plc-1","publisherId":"pub-1"}`,
+ `{"feedId":"x"}`,
+}
+
+var invalidParams = []string{
+ ``,
+ `null`,
+ `true`,
+ `5`,
+ `[]`,
+ `{}`,
+ `{"placementId":"plc-1"}`,
+ `{"feedId":""}`,
+ `{"feedId":123}`,
+ `{"feedId":null}`,
+ `{"feedId":"feed-abc","placementId":42}`,
+}
diff --git a/adapters/revantage/revantage.go b/adapters/revantage/revantage.go
new file mode 100644
index 00000000000..4596032e9f5
--- /dev/null
+++ b/adapters/revantage/revantage.go
@@ -0,0 +1,251 @@
+package revantage
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "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"
+)
+
+type adapter struct {
+ endpoint string
+}
+
+// Builder builds a new instance of the Revantage adapter for the given bidder with the given config.
+func Builder(_ openrtb_ext.BidderName, cfg config.Adapter, _ config.Server) (adapters.Bidder, error) {
+ return &adapter{endpoint: cfg.Endpoint}, nil
+}
+
+// rewrittenImpExt is the shape the Revantage endpoint expects on imp.ext.
+// It mirrors the public Prebid.js client adapter (revantageBidAdapter.js) so the
+// upstream endpoint can be a single code path for both client- and server-side
+// integrations.
+type rewrittenImpExt struct {
+ FeedID string `json:"feedId"`
+ Bidder rewrittenImpBidder `json:"bidder"`
+}
+
+type rewrittenImpBidder struct {
+ PlacementID string `json:"placementId,omitempty"`
+ PublisherID string `json:"publisherId,omitempty"`
+}
+
+// MakeRequests converts an OpenRTB bid request into one or more HTTP calls to the Revantage endpoint.
+//
+// Impressions are grouped by feedId. Each group becomes a separate HTTP call so that a single
+// auction can serve multiple feeds without conflict — the endpoint's `?feed=` query param must
+// match the feedId used in every imp it carries.
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, _ *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ if request == nil || len(request.Imp) == 0 {
+ return nil, []error{&errortypes.BadInput{Message: "no impressions in bid request"}}
+ }
+
+ // Preserve insertion order of feed groups for deterministic test output.
+ type group struct {
+ imps []openrtb2.Imp
+ }
+ groups := make(map[string]*group)
+ feedOrder := make([]string, 0)
+ var errs []error
+
+ for i := range request.Imp {
+ imp := request.Imp[i]
+ feedID, err := rewriteImpExt(&imp)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ g, ok := groups[feedID]
+ if !ok {
+ g = &group{}
+ groups[feedID] = g
+ feedOrder = append(feedOrder, feedID)
+ }
+ g.imps = append(g.imps, imp)
+ }
+
+ if len(groups) == 0 {
+ return nil, errs
+ }
+
+ requests := make([]*adapters.RequestData, 0, len(groups))
+ for _, feedID := range feedOrder {
+ imps := groups[feedID].imps
+
+ // Shallow copy the request and replace imps with this feed's slice.
+ reqCopy := *request
+ reqCopy.Imp = imps
+
+ body, err := json.Marshal(reqCopy)
+ if err != nil {
+ errs = append(errs, fmt.Errorf("failed to marshal request for feed %s: %w", feedID, err))
+ continue
+ }
+
+ headers := http.Header{}
+ headers.Add("Content-Type", "application/json;charset=utf-8")
+ headers.Add("Accept", "application/json")
+
+ requests = append(requests, &adapters.RequestData{
+ Method: http.MethodPost,
+ Uri: a.endpoint + "?feed=" + url.QueryEscape(feedID),
+ Body: body,
+ Headers: headers,
+ ImpIDs: collectImpIDs(imps),
+ })
+ }
+
+ return requests, errs
+}
+
+// rewriteImpExt validates the bidder params on a single imp and rewrites imp.ext into the
+// shape the Revantage endpoint expects. Returns the resolved feedId.
+func rewriteImpExt(imp *openrtb2.Imp) (string, error) {
+ var bidderExt adapters.ExtImpBidder
+ if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
+ return "", &errortypes.BadInput{
+ Message: fmt.Sprintf("imp %s: invalid imp.ext: %s", imp.ID, err.Error()),
+ }
+ }
+
+ var revantageExt openrtb_ext.ImpExtRevantage
+ if err := json.Unmarshal(bidderExt.Bidder, &revantageExt); err != nil {
+ return "", &errortypes.BadInput{
+ Message: fmt.Sprintf("imp %s: invalid imp.ext.bidder: %s", imp.ID, err.Error()),
+ }
+ }
+
+ feedID := strings.TrimSpace(revantageExt.FeedID)
+ if feedID == "" {
+ return "", &errortypes.BadInput{
+ Message: fmt.Sprintf("imp %s: missing required param feedId", imp.ID),
+ }
+ }
+
+ rewritten := rewrittenImpExt{
+ FeedID: feedID,
+ Bidder: rewrittenImpBidder{
+ PlacementID: revantageExt.PlacementID,
+ PublisherID: revantageExt.PublisherID,
+ },
+ }
+ extBytes, err := json.Marshal(rewritten)
+ if err != nil {
+ return "", fmt.Errorf("imp %s: failed to marshal rewritten ext: %w", imp.ID, err)
+ }
+ imp.Ext = extBytes
+
+ return feedID, nil
+}
+
+func collectImpIDs(imps []openrtb2.Imp) []string {
+ ids := make([]string, len(imps))
+ for i, imp := range imps {
+ ids[i] = imp.ID
+ }
+ return ids
+}
+
+// MakeBids parses the upstream Revantage response into typed Prebid bids.
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, _ *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 bidResp openrtb2.BidResponse
+ if err := json.Unmarshal(responseData.Body, &bidResp); err != nil {
+ return nil, []error{&errortypes.BadServerResponse{
+ Message: "invalid bid response: " + err.Error(),
+ }}
+ }
+
+ if len(bidResp.SeatBid) == 0 {
+ return nil, nil
+ }
+
+ response := adapters.NewBidderResponse()
+ if bidResp.Cur != "" {
+ response.Currency = bidResp.Cur
+ } else {
+ response.Currency = "USD"
+ }
+
+ var errs []error
+ for _, seat := range bidResp.SeatBid {
+ for i := range seat.Bid {
+ bid := &seat.Bid[i]
+ mt, err := resolveMediaType(bid, request.Imp)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ response.Bids = append(response.Bids, &adapters.TypedBid{
+ Bid: bid,
+ BidType: mt,
+ Seat: openrtb_ext.BidderName(seat.Seat),
+ })
+ }
+ }
+ return response, errs
+}
+
+// resolveMediaType determines the bid's media type. Resolution order:
+// 1. bid.mtype (oRTB 2.6) — REQUIRED on bids for multi-format impressions.
+// 2. bid.ext.mediaType — legacy signal, accepted as a fallback.
+// 3. The single, unambiguous media type on the originating imp (only when the
+// imp declares exactly one of banner/video).
+//
+// Bids on multi-format imps that arrive without bid.mtype or bid.ext.mediaType
+// are rejected with a BadServerResponse error. Per Prebid Server guidance, the
+// adapter server MUST set MType on every bid; we do not guess.
+func resolveMediaType(bid *openrtb2.Bid, imps []openrtb2.Imp) (openrtb_ext.BidType, error) {
+ switch bid.MType {
+ case openrtb2.MarkupBanner:
+ return openrtb_ext.BidTypeBanner, nil
+ case openrtb2.MarkupVideo:
+ return openrtb_ext.BidTypeVideo, nil
+ }
+
+ if len(bid.Ext) > 0 {
+ var ext struct {
+ MediaType string `json:"mediaType"`
+ }
+ if err := json.Unmarshal(bid.Ext, &ext); err == nil {
+ switch strings.ToLower(ext.MediaType) {
+ case "banner":
+ return openrtb_ext.BidTypeBanner, nil
+ case "video":
+ return openrtb_ext.BidTypeVideo, nil
+ }
+ }
+ }
+
+ for _, imp := range imps {
+ if imp.ID != bid.ImpID {
+ continue
+ }
+ hasBanner := imp.Banner != nil
+ hasVideo := imp.Video != nil
+ switch {
+ case hasVideo && !hasBanner:
+ return openrtb_ext.BidTypeVideo, nil
+ case hasBanner && !hasVideo:
+ return openrtb_ext.BidTypeBanner, nil
+ }
+ break
+ }
+
+ return "", &errortypes.BadServerResponse{
+ Message: fmt.Sprintf("could not determine media type for bid %s on imp %s: response missing bid.mtype and bid.ext.mediaType", bid.ID, bid.ImpID),
+ }
+}
diff --git a/adapters/revantage/revantage_test.go b/adapters/revantage/revantage_test.go
new file mode 100644
index 00000000000..99be3d388ab
--- /dev/null
+++ b/adapters/revantage/revantage_test.go
@@ -0,0 +1,25 @@
+package revantage
+
+import (
+ "testing"
+
+ "github.com/prebid/prebid-server/v4/adapters/adapterstest"
+ "github.com/prebid/prebid-server/v4/config"
+ "github.com/prebid/prebid-server/v4/openrtb_ext"
+)
+
+func TestJsonSamples(t *testing.T) {
+ bidder, buildErr := Builder(openrtb_ext.BidderRevantage, config.Adapter{
+ Endpoint: "https://bid.revantage.io/bid",
+ }, config.Server{
+ ExternalUrl: "http://hosturl.com",
+ GvlID: 1,
+ DataCenter: "2",
+ })
+
+ if buildErr != nil {
+ t.Fatalf("Builder returned unexpected error: %v", buildErr)
+ }
+
+ adapterstest.RunJSONBidderTest(t, "revantagetest", bidder)
+}
diff --git a/adapters/revantage/revantagetest/exemplary/multi-feed-split.json b/adapters/revantage/revantagetest/exemplary/multi-feed-split.json
new file mode 100644
index 00000000000..778098dc589
--- /dev/null
+++ b/adapters/revantage/revantagetest/exemplary/multi-feed-split.json
@@ -0,0 +1,112 @@
+{
+ "mockBidRequest": {
+ "id": "multi-feed-request",
+ "imp": [
+ {
+ "id": "imp-a",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "ext": {"bidder": {"feedId": "feed-one"}}
+ },
+ {
+ "id": "imp-b",
+ "banner": {"w": 728, "h": 90, "format": [{"w": 728, "h": 90}]},
+ "ext": {"bidder": {"feedId": "feed-two"}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://bid.revantage.io/bid?feed=feed-one",
+ "body": {
+ "id": "multi-feed-request",
+ "imp": [
+ {
+ "id": "imp-a",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "ext": {"feedId": "feed-one", "bidder": {}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "impIDs": ["imp-a"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "multi-feed-request",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "seat": "dsp-1",
+ "bid": [
+ {"id": "b1", "impid": "imp-a", "price": 1.0, "adm": "
1
", "crid": "c1", "w": 300, "h": 250, "mtype": 1}
+ ]
+ }
+ ]
+ }
+ }
+ },
+ {
+ "expectedRequest": {
+ "uri": "https://bid.revantage.io/bid?feed=feed-two",
+ "body": {
+ "id": "multi-feed-request",
+ "imp": [
+ {
+ "id": "imp-b",
+ "banner": {"w": 728, "h": 90, "format": [{"w": 728, "h": 90}]},
+ "ext": {"feedId": "feed-two", "bidder": {}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "impIDs": ["imp-b"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "multi-feed-request",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "seat": "dsp-2",
+ "bid": [
+ {"id": "b2", "impid": "imp-b", "price": 2.0, "adm": "2
", "crid": "c2", "w": 728, "h": 90, "mtype": 1}
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {"id": "b1", "impid": "imp-a", "price": 1.0, "adm": "1
", "crid": "c1", "w": 300, "h": 250, "mtype": 1},
+ "type": "banner",
+ "seat": "dsp-1"
+ }
+ ]
+ },
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {"id": "b2", "impid": "imp-b", "price": 2.0, "adm": "2
", "crid": "c2", "w": 728, "h": 90, "mtype": 1},
+ "type": "banner",
+ "seat": "dsp-2"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/revantage/revantagetest/exemplary/multi-imp-same-feed.json b/adapters/revantage/revantagetest/exemplary/multi-imp-same-feed.json
new file mode 100644
index 00000000000..ad7db608a82
--- /dev/null
+++ b/adapters/revantage/revantagetest/exemplary/multi-imp-same-feed.json
@@ -0,0 +1,95 @@
+{
+ "mockBidRequest": {
+ "id": "multi-imp-request",
+ "imp": [
+ {
+ "id": "imp-1",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "ext": {"bidder": {"feedId": "feed-shared"}}
+ },
+ {
+ "id": "imp-2",
+ "banner": {"w": 728, "h": 90, "format": [{"w": 728, "h": 90}]},
+ "ext": {"bidder": {"feedId": "feed-shared"}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://bid.revantage.io/bid?feed=feed-shared",
+ "body": {
+ "id": "multi-imp-request",
+ "imp": [
+ {
+ "id": "imp-1",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "ext": {"feedId": "feed-shared", "bidder": {}}
+ },
+ {
+ "id": "imp-2",
+ "banner": {"w": 728, "h": 90, "format": [{"w": 728, "h": 90}]},
+ "ext": {"feedId": "feed-shared", "bidder": {}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "impIDs": ["imp-1", "imp-2"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "multi-imp-request",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "seat": "dsp-a",
+ "bid": [
+ {
+ "id": "b1", "impid": "imp-1", "price": 1.10,
+ "adm": "A
", "crid": "c1", "w": 300, "h": 250,
+ "adomain": ["a.com"], "mtype": 1
+ },
+ {
+ "id": "b2", "impid": "imp-2", "price": 0.80,
+ "adm": "B
", "crid": "c2", "w": 728, "h": 90,
+ "adomain": ["b.com"], "mtype": 1
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "b1", "impid": "imp-1", "price": 1.10,
+ "adm": "A
", "crid": "c1", "w": 300, "h": 250,
+ "adomain": ["a.com"], "mtype": 1
+ },
+ "type": "banner",
+ "seat": "dsp-a"
+ },
+ {
+ "bid": {
+ "id": "b2", "impid": "imp-2", "price": 0.80,
+ "adm": "B
", "crid": "c2", "w": 728, "h": 90,
+ "adomain": ["b.com"], "mtype": 1
+ },
+ "type": "banner",
+ "seat": "dsp-a"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/revantage/revantagetest/exemplary/simple-banner.json b/adapters/revantage/revantagetest/exemplary/simple-banner.json
new file mode 100644
index 00000000000..4afc3a4891d
--- /dev/null
+++ b/adapters/revantage/revantagetest/exemplary/simple-banner.json
@@ -0,0 +1,149 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "imp-1",
+ "tagid": "ad-unit-1",
+ "banner": {
+ "w": 300,
+ "h": 250,
+ "format": [
+ {"w": 300, "h": 250},
+ {"w": 300, "h": 600}
+ ]
+ },
+ "bidfloor": 0.5,
+ "bidfloorcur": "USD",
+ "ext": {
+ "bidder": {
+ "feedId": "feed-abc",
+ "placementId": "plc-1",
+ "publisherId": "pub-1"
+ }
+ }
+ }
+ ],
+ "site": {
+ "domain": "example.com",
+ "page": "https://example.com/article"
+ },
+ "device": {
+ "ua": "Mozilla/5.0",
+ "language": "en"
+ },
+ "user": {
+ "ext": {
+ "consent": "BOJ/P2HOJ/P2HABABMAAAAAZ+A=="
+ }
+ },
+ "regs": {
+ "ext": {
+ "gdpr": 1
+ }
+ },
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://bid.revantage.io/bid?feed=feed-abc",
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "imp-1",
+ "tagid": "ad-unit-1",
+ "banner": {
+ "w": 300,
+ "h": 250,
+ "format": [
+ {"w": 300, "h": 250},
+ {"w": 300, "h": 600}
+ ]
+ },
+ "bidfloor": 0.5,
+ "bidfloorcur": "USD",
+ "ext": {
+ "feedId": "feed-abc",
+ "bidder": {
+ "placementId": "plc-1",
+ "publisherId": "pub-1"
+ }
+ }
+ }
+ ],
+ "site": {
+ "domain": "example.com",
+ "page": "https://example.com/article"
+ },
+ "device": {
+ "ua": "Mozilla/5.0",
+ "language": "en"
+ },
+ "user": {
+ "ext": {
+ "consent": "BOJ/P2HOJ/P2HABABMAAAAAZ+A=="
+ }
+ },
+ "regs": {
+ "ext": {
+ "gdpr": 1
+ }
+ },
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "impIDs": ["imp-1"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "seat": "test-dsp",
+ "bid": [
+ {
+ "id": "bid-1",
+ "impid": "imp-1",
+ "price": 1.25,
+ "adm": "Banner Ad
",
+ "crid": "creative-123",
+ "w": 300,
+ "h": 250,
+ "adomain": ["advertiser.com"],
+ "mtype": 1
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "bid-1",
+ "impid": "imp-1",
+ "price": 1.25,
+ "adm": "Banner Ad
",
+ "crid": "creative-123",
+ "w": 300,
+ "h": 250,
+ "adomain": ["advertiser.com"],
+ "mtype": 1
+ },
+ "type": "banner",
+ "seat": "test-dsp"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/revantage/revantagetest/exemplary/simple-video.json b/adapters/revantage/revantagetest/exemplary/simple-video.json
new file mode 100644
index 00000000000..c209fa11075
--- /dev/null
+++ b/adapters/revantage/revantagetest/exemplary/simple-video.json
@@ -0,0 +1,118 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-video",
+ "imp": [
+ {
+ "id": "video-imp-1",
+ "tagid": "video-unit",
+ "video": {
+ "mimes": ["video/mp4", "video/webm"],
+ "w": 640,
+ "h": 480,
+ "minduration": 5,
+ "maxduration": 30,
+ "protocols": [2, 3, 5, 6],
+ "api": [1, 2],
+ "placement": 1,
+ "linearity": 1
+ },
+ "ext": {
+ "bidder": {
+ "feedId": "feed-video"
+ }
+ }
+ }
+ ],
+ "site": {
+ "domain": "example.com",
+ "page": "https://example.com/watch"
+ },
+ "tmax": 1500,
+ "cur": ["USD"]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://bid.revantage.io/bid?feed=feed-video",
+ "body": {
+ "id": "test-request-video",
+ "imp": [
+ {
+ "id": "video-imp-1",
+ "tagid": "video-unit",
+ "video": {
+ "mimes": ["video/mp4", "video/webm"],
+ "w": 640,
+ "h": 480,
+ "minduration": 5,
+ "maxduration": 30,
+ "protocols": [2, 3, 5, 6],
+ "api": [1, 2],
+ "placement": 1,
+ "linearity": 1
+ },
+ "ext": {
+ "feedId": "feed-video",
+ "bidder": {}
+ }
+ }
+ ],
+ "site": {
+ "domain": "example.com",
+ "page": "https://example.com/watch"
+ },
+ "tmax": 1500,
+ "cur": ["USD"]
+ },
+ "impIDs": ["video-imp-1"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-video",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "seat": "video-dsp",
+ "bid": [
+ {
+ "id": "video-bid-1",
+ "impid": "video-imp-1",
+ "price": 4.50,
+ "adm": "",
+ "crid": "video-creative-1",
+ "w": 640,
+ "h": 480,
+ "adomain": ["video-advertiser.com"],
+ "mtype": 2
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "video-bid-1",
+ "impid": "video-imp-1",
+ "price": 4.50,
+ "adm": "",
+ "crid": "video-creative-1",
+ "w": 640,
+ "h": 480,
+ "adomain": ["video-advertiser.com"],
+ "mtype": 2
+ },
+ "type": "video",
+ "seat": "video-dsp"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/revantage/revantagetest/supplemental/204-response.json b/adapters/revantage/revantagetest/supplemental/204-response.json
new file mode 100644
index 00000000000..a603a4ca484
--- /dev/null
+++ b/adapters/revantage/revantagetest/supplemental/204-response.json
@@ -0,0 +1,41 @@
+{
+ "mockBidRequest": {
+ "id": "no-content-request",
+ "imp": [
+ {
+ "id": "imp-1",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "ext": {"bidder": {"feedId": "feed-abc"}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://bid.revantage.io/bid?feed=feed-abc",
+ "body": {
+ "id": "no-content-request",
+ "imp": [
+ {
+ "id": "imp-1",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "ext": {"feedId": "feed-abc", "bidder": {}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "impIDs": ["imp-1"]
+ },
+ "mockResponse": {
+ "status": 204,
+ "body": {}
+ }
+ }
+ ],
+ "expectedBidResponses": []
+}
diff --git a/adapters/revantage/revantagetest/supplemental/400-response.json b/adapters/revantage/revantagetest/supplemental/400-response.json
new file mode 100644
index 00000000000..17846b71972
--- /dev/null
+++ b/adapters/revantage/revantagetest/supplemental/400-response.json
@@ -0,0 +1,46 @@
+{
+ "mockBidRequest": {
+ "id": "bad-request",
+ "imp": [
+ {
+ "id": "imp-1",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "ext": {"bidder": {"feedId": "feed-abc"}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://bid.revantage.io/bid?feed=feed-abc",
+ "body": {
+ "id": "bad-request",
+ "imp": [
+ {
+ "id": "imp-1",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "ext": {"feedId": "feed-abc", "bidder": {}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "impIDs": ["imp-1"]
+ },
+ "mockResponse": {
+ "status": 400,
+ "body": {}
+ }
+ }
+ ],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "Unexpected status code: 400. Run with request.debug = 1 for more info",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/adapters/revantage/revantagetest/supplemental/missing-feedid.json b/adapters/revantage/revantagetest/supplemental/missing-feedid.json
new file mode 100644
index 00000000000..9a4693c899b
--- /dev/null
+++ b/adapters/revantage/revantagetest/supplemental/missing-feedid.json
@@ -0,0 +1,22 @@
+{
+ "mockBidRequest": {
+ "id": "missing-feed",
+ "imp": [
+ {
+ "id": "imp-x",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "ext": {"bidder": {}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "httpCalls": [],
+ "expectedMakeRequestsErrors": [
+ {
+ "value": "imp imp-x: missing required param feedId",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/adapters/revantage/revantagetest/supplemental/multi-format-missing-mtype.json b/adapters/revantage/revantagetest/supplemental/multi-format-missing-mtype.json
new file mode 100644
index 00000000000..2f3e699fcf3
--- /dev/null
+++ b/adapters/revantage/revantagetest/supplemental/multi-format-missing-mtype.json
@@ -0,0 +1,73 @@
+{
+ "mockBidRequest": {
+ "id": "multi-format-request",
+ "imp": [
+ {
+ "id": "imp-mf",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "video": {"mimes": ["video/mp4"], "w": 640, "h": 480},
+ "ext": {"bidder": {"feedId": "feed-abc"}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://bid.revantage.io/bid?feed=feed-abc",
+ "body": {
+ "id": "multi-format-request",
+ "imp": [
+ {
+ "id": "imp-mf",
+ "banner": {"w": 300, "h": 250, "format": [{"w": 300, "h": 250}]},
+ "video": {"mimes": ["video/mp4"], "w": 640, "h": 480},
+ "ext": {"feedId": "feed-abc", "bidder": {}}
+ }
+ ],
+ "site": {"domain": "example.com"},
+ "tmax": 1000,
+ "cur": ["USD"]
+ },
+ "impIDs": ["imp-mf"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "multi-format-request",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "seat": "dsp",
+ "bid": [
+ {
+ "id": "b1",
+ "impid": "imp-mf",
+ "price": 1.0,
+ "adm": "ambiguous
",
+ "crid": "c1",
+ "w": 300,
+ "h": 250
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": []
+ }
+ ],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "could not determine media type for bid b1 on imp imp-mf: response missing bid.mtype and bid.ext.mediaType",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/analytics/build/testFiles/test-20260428 b/analytics/build/testFiles/test-20260428
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 630fbe348b9..7fd4add7731 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -194,6 +194,7 @@ import (
"github.com/prebid/prebid-server/v4/adapters/rediads"
"github.com/prebid/prebid-server/v4/adapters/relevantdigital"
"github.com/prebid/prebid-server/v4/adapters/resetdigital"
+ "github.com/prebid/prebid-server/v4/adapters/revantage"
"github.com/prebid/prebid-server/v4/adapters/revcontent"
"github.com/prebid/prebid-server/v4/adapters/richaudience"
"github.com/prebid/prebid-server/v4/adapters/rise"
@@ -464,6 +465,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
openrtb_ext.BidderRediads: rediads.Builder,
openrtb_ext.BidderRelevantDigital: relevantdigital.Builder,
openrtb_ext.BidderResetDigital: resetdigital.Builder,
+ openrtb_ext.BidderRevantage: revantage.Builder,
openrtb_ext.BidderRevcontent: revcontent.Builder,
openrtb_ext.BidderRichaudience: richaudience.Builder,
openrtb_ext.BidderRise: rise.Builder,
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 1b61ff724e2..abbdccc4846 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -212,6 +212,7 @@ var coreBidderNames []BidderName = []BidderName{
BidderRediads,
BidderRelevantDigital,
BidderResetDigital,
+ BidderRevantage,
BidderRevcontent,
BidderRichaudience,
BidderRise,
@@ -586,6 +587,7 @@ const (
BidderRediads BidderName = "rediads"
BidderRelevantDigital BidderName = "relevantdigital"
BidderResetDigital BidderName = "resetdigital"
+ BidderRevantage BidderName = "revantage"
BidderRevcontent BidderName = "revcontent"
BidderRichaudience BidderName = "richaudience"
BidderRise BidderName = "rise"
diff --git a/openrtb_ext/imp_revantage.go b/openrtb_ext/imp_revantage.go
new file mode 100644
index 00000000000..dd4fee2ef4f
--- /dev/null
+++ b/openrtb_ext/imp_revantage.go
@@ -0,0 +1,12 @@
+package openrtb_ext
+
+// ImpExtRevantage defines the contract for the bidder-specific portion of
+// imp.ext when targeting the Revantage adapter.
+//
+// feedId is required and identifies the publisher feed the impression
+// belongs to. placementId and publisherId are optional pass-through values.
+type ImpExtRevantage struct {
+ FeedID string `json:"feedId"`
+ PlacementID string `json:"placementId,omitempty"`
+ PublisherID string `json:"publisherId,omitempty"`
+}
diff --git a/static/bidder-info/revantage.yaml b/static/bidder-info/revantage.yaml
new file mode 100644
index 00000000000..f10fcb323e1
--- /dev/null
+++ b/static/bidder-info/revantage.yaml
@@ -0,0 +1,17 @@
+endpoint: "https://bid.revantage.io/bid"
+maintainer:
+ email: "prebid@revantage.io"
+gvlVendorID: 0
+capabilities:
+ app:
+ mediaTypes:
+ - banner
+ - video
+ site:
+ mediaTypes:
+ - banner
+ - video
+userSync:
+ redirect:
+ url: "https://sync.revantage.io/pbs/usersync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&r={{.RedirectURL}}"
+ userMacro: "$UID"
\ No newline at end of file
diff --git a/static/bidder-params/revantage.json b/static/bidder-params/revantage.json
new file mode 100644
index 00000000000..d693afa5714
--- /dev/null
+++ b/static/bidder-params/revantage.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Revantage Adapter Params",
+ "description": "A schema which validates params accepted by the Revantage adapter",
+ "type": "object",
+ "properties": {
+ "feedId": {
+ "type": "string",
+ "description": "Revantage feed identifier (required)",
+ "minLength": 1
+ },
+ "placementId": {
+ "type": "string",
+ "description": "Optional placement identifier"
+ },
+ "publisherId": {
+ "type": "string",
+ "description": "Optional publisher identifier"
+ }
+ },
+ "required": ["feedId"]
+}