diff --git a/adapters/concert/concert.go b/adapters/concert/concert.go
new file mode 100644
index 00000000000..26bb105842a
--- /dev/null
+++ b/adapters/concert/concert.go
@@ -0,0 +1,139 @@
+package concert
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v2/adapters"
+ "github.com/prebid/prebid-server/v2/config"
+ "github.com/prebid/prebid-server/v2/errortypes"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+type adapter struct {
+ endpoint string
+}
+
+const adapterVersion = "1.0.0"
+
+// Builder builds a new instance of the Concert adapter for the given bidder with the given config.
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+ bidder := &adapter{
+ endpoint: config.Endpoint,
+ }
+ return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, requestInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ bidderImpExt, err := getBidderExt(request.Imp[0])
+ if err != nil {
+ return nil, []error{fmt.Errorf("get bidder ext: %v", err)}
+ }
+
+ requestJSON, err := json.Marshal(request)
+ if err != nil {
+ return nil, []error{err}
+ }
+
+ var requestMap map[string]interface{}
+ err = json.Unmarshal(requestJSON, &requestMap)
+ if err != nil {
+ return nil, []error{err}
+ }
+
+ if requestMap["ext"] == nil {
+ requestMap["ext"] = make(map[string]interface{})
+ }
+ requestMap["ext"].(map[string]interface{})["adapterVersion"] = adapterVersion
+ requestMap["ext"].(map[string]interface{})["partnerId"] = bidderImpExt.PartnerId
+
+ requestJSON, err = json.Marshal(requestMap)
+ if err != nil {
+ return nil, []error{err}
+ }
+
+ headers := http.Header{}
+ headers.Add("Content-Type", "application/json")
+ headers.Add("Accept", "application/json")
+ requestData := &adapters.RequestData{
+ Method: "POST",
+ Uri: a.endpoint,
+ Body: requestJSON,
+ Headers: headers,
+ ImpIDs: openrtb_ext.GetImpIDs(request.Imp),
+ }
+
+ return []*adapters.RequestData{requestData}, nil
+}
+
+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 := json.Unmarshal(responseData.Body, &response); err != nil {
+ return nil, []error{err}
+ }
+
+ bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+ bidResponse.Currency = response.Cur
+ var errors []error
+ for _, seatBid := range response.SeatBid {
+ for i, bid := range seatBid.Bid {
+ bidType, err := getMediaTypeForBid(bid)
+ if err != nil {
+ errors = append(errors, err)
+ continue
+ }
+ bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+ Bid: &seatBid.Bid[i],
+ BidType: bidType,
+ })
+ }
+ }
+
+ if len(errors) > 0 {
+ return nil, errors
+ }
+
+ if len(bidResponse.Bids) == 0 {
+ return nil, []error{fmt.Errorf("no bids returned")}
+ }
+
+ return bidResponse, nil
+}
+
+func getMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) {
+ switch bid.MType {
+ case openrtb2.MarkupBanner:
+ return openrtb_ext.BidTypeBanner, nil
+ case openrtb2.MarkupVideo:
+ return openrtb_ext.BidTypeVideo, nil
+ case openrtb2.MarkupAudio:
+ return openrtb_ext.BidTypeAudio, nil
+ case openrtb2.MarkupNative:
+ return "", fmt.Errorf("native media types are not yet supported")
+ default:
+ return "", &errortypes.BadServerResponse{
+ Message: fmt.Sprintf("Failed to parse media type for bid: \"%s\"", bid.ImpID),
+ }
+ }
+}
+
+func getBidderExt(imp openrtb2.Imp) (bidderImpExt openrtb_ext.ImpExtConcert, err error) {
+ var impExt adapters.ExtImpBidder
+ if err = json.Unmarshal(imp.Ext, &impExt); err != nil {
+ return bidderImpExt, fmt.Errorf("imp ext: %v", err)
+ }
+ if err = json.Unmarshal(impExt.Bidder, &bidderImpExt); err != nil {
+ return bidderImpExt, fmt.Errorf("bidder ext: %v", err)
+ }
+ return bidderImpExt, nil
+}
diff --git a/adapters/concert/concert_test.go b/adapters/concert/concert_test.go
new file mode 100644
index 00000000000..e8c06d06377
--- /dev/null
+++ b/adapters/concert/concert_test.go
@@ -0,0 +1,20 @@
+package concert
+
+import (
+ "testing"
+
+ "github.com/prebid/prebid-server/v2/adapters/adapterstest"
+ "github.com/prebid/prebid-server/v2/config"
+ "github.com/prebid/prebid-server/v2/openrtb_ext"
+)
+
+func TestJsonSamples(t *testing.T) {
+ bidder, buildErr := Builder(openrtb_ext.BidderConcert, config.Adapter{
+ Endpoint: "https://bids.concert.io/bids/openrtb"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+ if buildErr != nil {
+ t.Fatalf("Builder returned unexpected error %v", buildErr)
+ }
+
+ adapterstest.RunJSONBidderTest(t, "concerttest", bidder)
+}
diff --git a/adapters/concert/concerttest/exemplary/audio.json b/adapters/concert/concerttest/exemplary/audio.json
new file mode 100644
index 00000000000..66363d5a256
--- /dev/null
+++ b/adapters/concert/concerttest/exemplary/audio.json
@@ -0,0 +1,119 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "audio": {
+ "mimes": ["audio/mp3"],
+ "minduration": 5,
+ "maxduration": 30,
+ "protocols": [1, 2]
+ },
+ "ext": {
+ "bidder": {
+ "partnerId": "partner_name",
+ "placementId": 1234567,
+ "site": "site_name",
+ "slot": "slot_name"
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "http://www.example.com"
+ },
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "123.123.123.123"
+ },
+ "user": {
+ "buyeruid": "some-buyer-uid"
+ },
+ "ext": {
+ "adapterVersion": "1.0.0",
+ "partnerId": "partner_name"
+ }
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://bids.concert.io/bids/openrtb",
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "audio": {
+ "mimes": ["audio/mp3"],
+ "minduration": 5,
+ "maxduration": 30,
+ "protocols": [1, 2]
+ },
+ "ext": {
+ "bidder": {
+ "partnerId": "partner_name",
+ "placementId": 1234567,
+ "site": "site_name",
+ "slot": "slot_name"
+ }
+ }
+ }
+ ],
+ "device": {
+ "ip": "123.123.123.123",
+ "ua": "test-user-agent"
+ },
+ "site": {
+ "page": "http://www.example.com"
+ },
+ "user": {
+ "buyeruid": "some-buyer-uid"
+ },
+ "ext": {
+ "adapterVersion": "1.0.0",
+ "partnerId": "partner_name"
+ }
+ },
+ "impIDs":["test-imp-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-response-id",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 1.23,
+ "adm": "...",
+ "crid": "test-creative-id",
+ "mtype": 3
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "bids": [
+ {
+ "bid": {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 1.23,
+ "adm": "...",
+ "crid": "test-creative-id",
+ "mtype": 3
+ },
+ "type": "audio"
+ }
+ ]
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/adapters/concert/concerttest/exemplary/banner.json b/adapters/concert/concerttest/exemplary/banner.json
new file mode 100644
index 00000000000..6fc0df4e604
--- /dev/null
+++ b/adapters/concert/concerttest/exemplary/banner.json
@@ -0,0 +1,139 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 1030,
+ "h": 590
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {
+ "partnerId": "partner_name",
+ "placementId": 1234567,
+ "site": "site_name",
+ "slot": "slot_name",
+ "sizes": [
+ [
+ 1030,
+ 590
+ ]
+ ]
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "http://www.example.com"
+ },
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "123.123.123.123"
+ },
+ "user": {
+ "buyeruid": "some-buyer-uid"
+ },
+ "ext": {
+ "adapterVersion": "1.0.0",
+ "partnerId": "partner_name"
+ }
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://bids.concert.io/bids/openrtb",
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 1030,
+ "h": 590
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {
+ "partnerId": "partner_name",
+ "placementId": 1234567,
+ "site": "site_name",
+ "slot": "slot_name",
+ "sizes": [
+ [
+ 1030,
+ 590
+ ]
+ ]
+ }
+ }
+ }
+ ],
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "123.123.123.123"
+ },
+ "site": {
+ "page": "http://www.example.com"
+ },
+ "user": {
+ "buyeruid": "some-buyer-uid"
+ },
+ "ext": {
+ "adapterVersion": "1.0.0",
+ "partnerId": "partner_name"
+ }
+ },
+ "impIDs":["test-imp-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-response-id",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 1.23,
+ "adm": "
Banner Ad
This is a mock banner ad
",
+ "crid": "test-creative-id",
+ "w": 1030,
+ "h": 590,
+ "mtype": 1
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "bids": [
+ {
+ "bid": {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 1.23,
+ "adm": "Banner Ad
This is a mock banner ad
",
+ "crid": "test-creative-id",
+ "w": 1030,
+ "h": 590,
+ "mtype": 1
+ },
+ "type": "banner"
+ }
+ ]
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/adapters/concert/concerttest/exemplary/video.json b/adapters/concert/concerttest/exemplary/video.json
new file mode 100644
index 00000000000..fdb21d04d4a
--- /dev/null
+++ b/adapters/concert/concerttest/exemplary/video.json
@@ -0,0 +1,129 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "video": {
+ "mimes": ["video/mp4"],
+ "minduration": 5,
+ "maxduration": 30,
+ "protocols": [1, 2],
+ "w": 640,
+ "h": 480
+ },
+ "ext": {
+ "bidder": {
+ "partnerId": "partner_name",
+ "placementId": 1234567,
+ "site": "site_name",
+ "slot": "slot_name",
+ "sizes": [[640, 480]]
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "http://www.example.com"
+ },
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "123.123.123.123"
+ },
+ "user": {
+ "buyeruid": "some-buyer-uid"
+ },
+ "ext": {
+ "adapterVersion": "1.0.0",
+ "partnerId": "partner_name"
+ }
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://bids.concert.io/bids/openrtb",
+ "body": {
+ "id": "test-request-id",
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "video": {
+ "w": 640,
+ "h": 480,
+ "minduration": 5,
+ "maxduration": 30,
+ "protocols": [1, 2],
+ "mimes": ["video/mp4"]
+ },
+ "ext": {
+ "bidder": {
+ "partnerId": "partner_name",
+ "placementId": 1234567,
+ "site": "site_name",
+ "slot": "slot_name",
+ "sizes": [[640, 480]]
+ }
+ }
+ }
+ ],
+ "site": {
+ "page": "http://www.example.com"
+ },
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "123.123.123.123"
+ },
+ "user": {
+ "buyeruid": "some-buyer-uid"
+ },
+ "ext": {
+ "adapterVersion": "1.0.0",
+ "partnerId": "partner_name"
+ }
+ },
+ "impIDs":["test-imp-id"]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-response-id",
+ "seatbid": [
+ {
+ "bid": [
+ {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 1.23,
+ "adm": "...",
+ "crid": "test-creative-id",
+ "w": 640,
+ "h": 480,
+ "mtype": 2
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "bids": [
+ {
+ "bid": {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 1.23,
+ "adm": "...",
+ "crid": "test-creative-id",
+ "w": 640,
+ "h": 480,
+ "mtype": 2
+ },
+ "type": "video"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/adapters/concert/params_test.go b/adapters/concert/params_test.go
new file mode 100644
index 00000000000..4cf653d2b27
--- /dev/null
+++ b/adapters/concert/params_test.go
@@ -0,0 +1,51 @@
+package concert
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/prebid/prebid-server/v2/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.BidderConcert, 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.BidderConcert, json.RawMessage(p)); err == nil {
+ t.Errorf("Schema allowed invalid params: %s", p)
+ }
+ }
+}
+
+var validParams = []string{
+ `{"partnerId": "partner_name"}`,
+ `{"partnerId": "partner_name", "placementId": 1234567}`,
+ `{"partnerId": "partner_name", "placementId": 1234567, "site": "site_name"}`,
+ `{"partnerId": "partner_name", "placementId": 1234567, "site": "site_name", "slot": "slot_name"}`,
+ `{"partnerId": "partner_name", "placementId": 1234567, "site": "site_name", "slot": "slot_name", "sizes": [[1030, 590]]}`,
+}
+
+var invalidParams = []string{
+ `{"partnerId": ""}`,
+ `{"placementId": 1234567}`,
+ `{"site": "site_name"}`,
+ `{"slot": "slot_name"}`,
+ `{"sizes": [[1030, 590]]}`,
+ `{"placementId": 1234567, "site": "site_name", "slot": "slot_name", "sizes": [[1030, 590]]}`,
+}
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 9f96cdbf171..922fecfa7ef 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -66,6 +66,7 @@ import (
"github.com/prebid/prebid-server/v2/adapters/coinzilla"
"github.com/prebid/prebid-server/v2/adapters/colossus"
"github.com/prebid/prebid-server/v2/adapters/compass"
+ "github.com/prebid/prebid-server/v2/adapters/concert"
"github.com/prebid/prebid-server/v2/adapters/connectad"
"github.com/prebid/prebid-server/v2/adapters/consumable"
"github.com/prebid/prebid-server/v2/adapters/conversant"
@@ -274,6 +275,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
openrtb_ext.BidderCoinzilla: coinzilla.Builder,
openrtb_ext.BidderColossus: colossus.Builder,
openrtb_ext.BidderCompass: compass.Builder,
+ openrtb_ext.BidderConcert: concert.Builder,
openrtb_ext.BidderConnectAd: connectad.Builder,
openrtb_ext.BidderConsumable: consumable.Builder,
openrtb_ext.BidderConversant: conversant.Builder,
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 93c6a49e8e4..3b3bcb64201 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -82,6 +82,7 @@ var coreBidderNames []BidderName = []BidderName{
BidderCoinzilla,
BidderColossus,
BidderCompass,
+ BidderConcert,
BidderConnectAd,
BidderConsumable,
BidderConversant,
@@ -368,6 +369,7 @@ const (
BidderCoinzilla BidderName = "coinzilla"
BidderColossus BidderName = "colossus"
BidderCompass BidderName = "compass"
+ BidderConcert BidderName = "concert"
BidderConnectAd BidderName = "connectad"
BidderConsumable BidderName = "consumable"
BidderConversant BidderName = "conversant"
diff --git a/openrtb_ext/imp_concert.go b/openrtb_ext/imp_concert.go
new file mode 100644
index 00000000000..bd5ec245c86
--- /dev/null
+++ b/openrtb_ext/imp_concert.go
@@ -0,0 +1,9 @@
+package openrtb_ext
+
+type ImpExtConcert struct {
+ PartnerId string `json:"partnerId"`
+ PlacementId *int `json:"placementId,omitempty"`
+ Site *string `json:"site,omitempty"`
+ Slot *string `json:"slot,omitempty"`
+ Sizes *[][]int `json:"sizes,omitempty"`
+}
diff --git a/static/bidder-info/concert.yaml b/static/bidder-info/concert.yaml
new file mode 100644
index 00000000000..8251518f29a
--- /dev/null
+++ b/static/bidder-info/concert.yaml
@@ -0,0 +1,18 @@
+endpoint: "https://bids.concert.io/bids/openrtb"
+endpointCompression: gzip
+geoscope:
+ - global
+maintainer:
+ email: support@concert.io
+modifyingVastXmlAllowed: true
+capabilities:
+ app:
+ mediaTypes:
+ - banner
+ - video
+ - audio
+ site:
+ mediaTypes:
+ - banner
+ - video
+ - audio
\ No newline at end of file
diff --git a/static/bidder-params/concert.json b/static/bidder-params/concert.json
new file mode 100644
index 00000000000..5dd82b4d486
--- /dev/null
+++ b/static/bidder-params/concert.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Concert Adapter Params",
+ "description": "A schema which validates params accepted by the Concert adapter",
+ "type": "object",
+ "properties": {
+ "partnerId": {
+ "type": "string",
+ "description": "The partner id assigned by concert.",
+ "minLength": 1
+ },
+ "placementId": {
+ "type": "integer",
+ "description": "The placement id."
+ },
+ "site": {
+ "type": "string",
+ "description": "The site name."
+ },
+ "slot": {
+ "type": "string",
+ "description": "The slot name."
+ },
+ "sizes": {
+ "type": "array",
+ "description": "All sizes this ad unit accepts.",
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "minItems": 2,
+ "maxItems": 2
+ }
+ }
+ },
+ "required": ["partnerId"]
+}
\ No newline at end of file