diff --git a/adapters/ezoic/ezoic.go b/adapters/ezoic/ezoic.go
new file mode 100644
index 00000000000..fd5e51950d6
--- /dev/null
+++ b/adapters/ezoic/ezoic.go
@@ -0,0 +1,95 @@
+package ezoic
+
+import (
+ "fmt"
+ "net/http"
+
+ "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"
+)
+
+type adapter struct {
+ endpoint string
+}
+
+// Builder builds a new instance of the Ezoic adapter for the given bidder with the given config.
+func Builder(bidderName openrtb_ext.BidderName, cfg config.Adapter, server config.Server) (adapters.Bidder, error) {
+ return &adapter{endpoint: cfg.Endpoint}, nil
+}
+
+// MakeRequests forwards the OpenRTB request to the Ezoic bidder endpoint
+// unchanged. Eligibility, demand selection, and creative construction all
+// happen server-side at Ezoic; the adapter is a deliberately thin transport.
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ body, err := jsonutil.Marshal(request)
+ if err != nil {
+ return nil, []error{fmt.Errorf("unable to marshal openrtb request: %w", err)}
+ }
+
+ headers := http.Header{}
+ headers.Add("Content-Type", "application/json;charset=utf-8")
+ headers.Add("Accept", "application/json")
+
+ return []*adapters.RequestData{{
+ Method: http.MethodPost,
+ Uri: a.endpoint,
+ Body: body,
+ Headers: headers,
+ ImpIDs: openrtb_ext.GetImpIDs(request.Imp),
+ }}, nil
+}
+
+// MakeBids unpacks the Ezoic endpoint's OpenRTB BidResponse.
+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 bidResp openrtb2.BidResponse
+ if err := jsonutil.Unmarshal(responseData.Body, &bidResp); err != nil {
+ return nil, []error{err}
+ }
+
+ bidderResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+ if bidResp.Cur != "" {
+ bidderResponse.Currency = bidResp.Cur
+ }
+
+ var errs []error
+ for _, seatBid := range bidResp.SeatBid {
+ for i := range seatBid.Bid {
+ bidType, err := getMediaTypeForBid(seatBid.Bid[i])
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
+ Bid: &seatBid.Bid[i],
+ BidType: bidType,
+ })
+ }
+ }
+ return bidderResponse, errs
+}
+
+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.MarkupNative:
+ return openrtb_ext.BidTypeNative, nil
+ default:
+ return "", &errortypes.BadServerResponse{
+ Message: fmt.Sprintf("unsupported mtype %d for bid %s", bid.MType, bid.ID),
+ }
+ }
+}
diff --git a/adapters/ezoic/ezoic_test.go b/adapters/ezoic/ezoic_test.go
new file mode 100644
index 00000000000..0c9a24a953a
--- /dev/null
+++ b/adapters/ezoic/ezoic_test.go
@@ -0,0 +1,57 @@
+package ezoic
+
+import (
+ "testing"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v4/adapters"
+ "github.com/prebid/prebid-server/v4/adapters/adapterstest"
+ "github.com/prebid/prebid-server/v4/config"
+ "github.com/prebid/prebid-server/v4/openrtb_ext"
+ "github.com/stretchr/testify/assert"
+)
+
+const testsBidderEndpoint = "https://g.ezoic.net/ezoic/prebid/adapter/ortb"
+
+func TestJsonSamples(t *testing.T) {
+ bidder, buildErr := Builder(openrtb_ext.BidderEzoic, config.Adapter{
+ Endpoint: testsBidderEndpoint}, config.Server{})
+
+ if buildErr != nil {
+ t.Fatalf("Builder returned unexpected error %v", buildErr)
+ }
+
+ adapterstest.RunJSONBidderTest(t, "ezoictest", bidder)
+}
+
+func TestNoContentResponse(t *testing.T) {
+ bidder, buildErr := Builder(openrtb_ext.BidderEzoic, config.Adapter{
+ Endpoint: testsBidderEndpoint}, config.Server{})
+ if buildErr != nil {
+ t.Fatalf("Builder returned unexpected error %v", buildErr)
+ }
+
+ bidResponse, errs := bidder.MakeBids(nil, nil, &adapters.ResponseData{StatusCode: 204})
+ assert.Nil(t, bidResponse)
+ assert.Empty(t, errs)
+}
+
+func TestGetMediaTypeForBid(t *testing.T) {
+ bidType, err := getMediaTypeForBid(openrtb2.Bid{MType: openrtb2.MarkupBanner})
+ assert.NoError(t, err)
+ assert.Equal(t, openrtb_ext.BidTypeBanner, bidType)
+
+ bidType, err = getMediaTypeForBid(openrtb2.Bid{MType: openrtb2.MarkupVideo})
+ assert.NoError(t, err)
+ assert.Equal(t, openrtb_ext.BidTypeVideo, bidType)
+
+ bidType, err = getMediaTypeForBid(openrtb2.Bid{MType: openrtb2.MarkupNative})
+ assert.NoError(t, err)
+ assert.Equal(t, openrtb_ext.BidTypeNative, bidType)
+
+ _, err = getMediaTypeForBid(openrtb2.Bid{ID: "no-mtype"})
+ assert.Error(t, err)
+
+ _, err = getMediaTypeForBid(openrtb2.Bid{ID: "audio", MType: openrtb2.MarkupAudio})
+ assert.Error(t, err)
+}
diff --git a/adapters/ezoic/ezoictest/exemplary/simple-banner.json b/adapters/ezoic/ezoictest/exemplary/simple-banner.json
new file mode 100644
index 00000000000..7c3c29b291b
--- /dev/null
+++ b/adapters/ezoic/ezoictest/exemplary/simple-banner.json
@@ -0,0 +1,124 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "tmax": 750,
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "203.0.113.9"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "div-gpt-ad-1",
+ "bidfloor": 0.25,
+ "bidfloorcur": "USD",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {
+ "placementId": "ezoic-placement-1"
+ }
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://g.ezoic.net/ezoic/prebid/adapter/ortb",
+ "body": {
+ "id": "test-request-id",
+ "tmax": 750,
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "203.0.113.9"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "div-gpt-ad-1",
+ "bidfloor": 0.25,
+ "bidfloorcur": "USD",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {
+ "placementId": "ezoic-placement-1"
+ }
+ }
+ }
+ ]
+ },
+ "impIDs": [
+ "test-imp-id"
+ ]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "seat": "ezoic",
+ "bid": [
+ {
+ "id": "ezoic-bid-1",
+ "impid": "test-imp-id",
+ "price": 2.5,
+ "adm": "
ezoic-self-tracking-creative
",
+ "crid": "ezoic-creative-1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1,
+ "exp": 300
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "ezoic-bid-1",
+ "impid": "test-imp-id",
+ "price": 2.5,
+ "adm": "ezoic-self-tracking-creative
",
+ "crid": "ezoic-creative-1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1,
+ "exp": 300
+ },
+ "type": "banner"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/ezoic/ezoictest/exemplary/simple-native.json b/adapters/ezoic/ezoictest/exemplary/simple-native.json
new file mode 100644
index 00000000000..31a072cd61d
--- /dev/null
+++ b/adapters/ezoic/ezoictest/exemplary/simple-native.json
@@ -0,0 +1,112 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "tmax": 750,
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "203.0.113.9"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "div-gpt-ad-1",
+ "bidfloor": 0.25,
+ "bidfloorcur": "USD",
+ "native": {
+ "ver": "1.2",
+ "request": "{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"w\":1200,\"h\":627}}]}"
+ },
+ "ext": {
+ "bidder": {
+ "placementId": "ezoic-placement-1"
+ }
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://g.ezoic.net/ezoic/prebid/adapter/ortb",
+ "body": {
+ "id": "test-request-id",
+ "tmax": 750,
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "203.0.113.9"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "div-gpt-ad-1",
+ "bidfloor": 0.25,
+ "bidfloorcur": "USD",
+ "native": {
+ "ver": "1.2",
+ "request": "{\"ver\":\"1.2\",\"context\":1,\"plcmttype\":4,\"plcmtcnt\":1,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"w\":1200,\"h\":627}}]}"
+ },
+ "ext": {
+ "bidder": {
+ "placementId": "ezoic-placement-1"
+ }
+ }
+ }
+ ]
+ },
+ "impIDs": [
+ "test-imp-id"
+ ]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "seat": "ezoic",
+ "bid": [
+ {
+ "id": "ezoic-bid-1",
+ "impid": "test-imp-id",
+ "price": 1.75,
+ "adm": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"title\":{\"text\":\"ezoic native title\"}}],\"link\":{\"url\":\"https://example.com/click\"}}}",
+ "crid": "ezoic-creative-1",
+ "mtype": 4,
+ "exp": 300
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "ezoic-bid-1",
+ "impid": "test-imp-id",
+ "price": 1.75,
+ "adm": "{\"native\":{\"ver\":\"1.2\",\"assets\":[{\"id\":1,\"title\":{\"text\":\"ezoic native title\"}}],\"link\":{\"url\":\"https://example.com/click\"}}}",
+ "crid": "ezoic-creative-1",
+ "mtype": 4,
+ "exp": 300
+ },
+ "type": "native"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/ezoic/ezoictest/exemplary/simple-video.json b/adapters/ezoic/ezoictest/exemplary/simple-video.json
new file mode 100644
index 00000000000..6db45edc5ed
--- /dev/null
+++ b/adapters/ezoic/ezoictest/exemplary/simple-video.json
@@ -0,0 +1,138 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "tmax": 750,
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "203.0.113.9"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "div-gpt-ad-1",
+ "bidfloor": 0.25,
+ "bidfloorcur": "USD",
+ "video": {
+ "mimes": [
+ "video/mp4"
+ ],
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6
+ ],
+ "w": 640,
+ "h": 480,
+ "minduration": 5,
+ "maxduration": 30
+ },
+ "ext": {
+ "bidder": {
+ "placementId": "ezoic-placement-1"
+ }
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://g.ezoic.net/ezoic/prebid/adapter/ortb",
+ "body": {
+ "id": "test-request-id",
+ "tmax": 750,
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "device": {
+ "ua": "test-user-agent",
+ "ip": "203.0.113.9"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "tagid": "div-gpt-ad-1",
+ "bidfloor": 0.25,
+ "bidfloorcur": "USD",
+ "video": {
+ "mimes": [
+ "video/mp4"
+ ],
+ "protocols": [
+ 2,
+ 3,
+ 5,
+ 6
+ ],
+ "w": 640,
+ "h": 480,
+ "minduration": 5,
+ "maxduration": 30
+ },
+ "ext": {
+ "bidder": {
+ "placementId": "ezoic-placement-1"
+ }
+ }
+ }
+ ]
+ },
+ "impIDs": [
+ "test-imp-id"
+ ]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "seat": "ezoic",
+ "bid": [
+ {
+ "id": "ezoic-bid-1",
+ "impid": "test-imp-id",
+ "price": 4.5,
+ "adm": "",
+ "crid": "ezoic-creative-1",
+ "w": 640,
+ "h": 480,
+ "mtype": 2,
+ "exp": 300
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "ezoic-bid-1",
+ "impid": "test-imp-id",
+ "price": 4.5,
+ "adm": "",
+ "crid": "ezoic-creative-1",
+ "w": 640,
+ "h": 480,
+ "mtype": 2,
+ "exp": 300
+ },
+ "type": "video"
+ }
+ ]
+ }
+ ]
+}
diff --git a/adapters/ezoic/ezoictest/supplemental/invalid-response.json b/adapters/ezoic/ezoictest/supplemental/invalid-response.json
new file mode 100644
index 00000000000..adcb3c10012
--- /dev/null
+++ b/adapters/ezoic/ezoictest/supplemental/invalid-response.json
@@ -0,0 +1,69 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {}
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://g.ezoic.net/ezoic/prebid/adapter/ortb",
+ "body": {
+ "id": "test-request-id",
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {}
+ }
+ }
+ ]
+ },
+ "impIDs": [
+ "test-imp-id"
+ ]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": "invalid response"
+ }
+ }
+ ],
+ "expectedBidResponses": [],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "expect",
+ "comparison": "regex"
+ }
+ ]
+}
diff --git a/adapters/ezoic/ezoictest/supplemental/status-204.json b/adapters/ezoic/ezoictest/supplemental/status-204.json
new file mode 100644
index 00000000000..2c60ef485ad
--- /dev/null
+++ b/adapters/ezoic/ezoictest/supplemental/status-204.json
@@ -0,0 +1,63 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {}
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://g.ezoic.net/ezoic/prebid/adapter/ortb",
+ "body": {
+ "id": "test-request-id",
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {}
+ }
+ }
+ ]
+ },
+ "impIDs": [
+ "test-imp-id"
+ ]
+ },
+ "mockResponse": {
+ "status": 204,
+ "body": {}
+ }
+ }
+ ],
+ "expectedBidResponses": []
+}
diff --git a/adapters/ezoic/ezoictest/supplemental/status-400.json b/adapters/ezoic/ezoictest/supplemental/status-400.json
new file mode 100644
index 00000000000..8d156eb86a4
--- /dev/null
+++ b/adapters/ezoic/ezoictest/supplemental/status-400.json
@@ -0,0 +1,69 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {}
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://g.ezoic.net/ezoic/prebid/adapter/ortb",
+ "body": {
+ "id": "test-request-id",
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {}
+ }
+ }
+ ]
+ },
+ "impIDs": [
+ "test-imp-id"
+ ]
+ },
+ "mockResponse": {
+ "status": 400,
+ "body": {}
+ }
+ }
+ ],
+ "expectedBidResponses": [],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "Unexpected status code: 400. Run with request.debug = 1 for more info",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/adapters/ezoic/ezoictest/supplemental/status-500.json b/adapters/ezoic/ezoictest/supplemental/status-500.json
new file mode 100644
index 00000000000..1d0b4182900
--- /dev/null
+++ b/adapters/ezoic/ezoictest/supplemental/status-500.json
@@ -0,0 +1,69 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {}
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://g.ezoic.net/ezoic/prebid/adapter/ortb",
+ "body": {
+ "id": "test-request-id",
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {}
+ }
+ }
+ ]
+ },
+ "impIDs": [
+ "test-imp-id"
+ ]
+ },
+ "mockResponse": {
+ "status": 500,
+ "body": {}
+ }
+ }
+ ],
+ "expectedBidResponses": [],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "Unexpected status code: 500. Run with request.debug = 1 for more info",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/adapters/ezoic/ezoictest/supplemental/unsupported-mtype.json b/adapters/ezoic/ezoictest/supplemental/unsupported-mtype.json
new file mode 100644
index 00000000000..39d7506d66d
--- /dev/null
+++ b/adapters/ezoic/ezoictest/supplemental/unsupported-mtype.json
@@ -0,0 +1,117 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {}
+ }
+ }
+ ]
+ },
+ "httpCalls": [
+ {
+ "expectedRequest": {
+ "uri": "https://g.ezoic.net/ezoic/prebid/adapter/ortb",
+ "body": {
+ "id": "test-request-id",
+ "site": {
+ "page": "https://example.com/article",
+ "domain": "example.com"
+ },
+ "imp": [
+ {
+ "id": "test-imp-id",
+ "banner": {
+ "format": [
+ {
+ "w": 300,
+ "h": 250
+ }
+ ]
+ },
+ "ext": {
+ "bidder": {}
+ }
+ }
+ ]
+ },
+ "impIDs": [
+ "test-imp-id"
+ ]
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "cur": "USD",
+ "seatbid": [
+ {
+ "seat": "ezoic",
+ "bid": [
+ {
+ "id": "ezoic-bid-good",
+ "impid": "test-imp-id",
+ "price": 1.25,
+ "adm": "creative
",
+ "crid": "ezoic-creative-1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ },
+ {
+ "id": "ezoic-bid-no-mtype",
+ "impid": "test-imp-id",
+ "price": 9.99,
+ "adm": "creative
",
+ "crid": "ezoic-creative-2",
+ "w": 300,
+ "h": 250
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "expectedBidResponses": [
+ {
+ "currency": "USD",
+ "bids": [
+ {
+ "bid": {
+ "id": "ezoic-bid-good",
+ "impid": "test-imp-id",
+ "price": 1.25,
+ "adm": "creative
",
+ "crid": "ezoic-creative-1",
+ "w": 300,
+ "h": 250,
+ "mtype": 1
+ },
+ "type": "banner"
+ }
+ ]
+ }
+ ],
+ "expectedMakeBidsErrors": [
+ {
+ "value": "unsupported mtype 0 for bid ezoic-bid-no-mtype",
+ "comparison": "literal"
+ }
+ ]
+}
diff --git a/adapters/ezoic/params_test.go b/adapters/ezoic/params_test.go
new file mode 100644
index 00000000000..91994fe4cf1
--- /dev/null
+++ b/adapters/ezoic/params_test.go
@@ -0,0 +1,52 @@
+package ezoic
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/prebid/prebid-server/v4/openrtb_ext"
+)
+
+// This file intends to test static/bidder-params/ezoic.json
+//
+// These also validate the format of the external API: request.imp[i].ext.prebid.bidder.ezoic
+
+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.BidderEzoic, json.RawMessage(validParam)); err != nil {
+ t.Errorf("Schema rejected ezoic params: %s", validParam)
+ }
+ }
+}
+
+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.BidderEzoic, json.RawMessage(invalidParam)); err == nil {
+ t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+ }
+ }
+}
+
+var validParams = []string{
+ `{}`,
+ `{"placementId": "ezoic-placement-1"}`,
+}
+
+var invalidParams = []string{
+ `null`,
+ `true`,
+ `[]`,
+ `"placementId"`,
+ `{"placementId": 12345}`,
+ `{"placementId": true}`,
+}
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 75a848b7596..090e537effb 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -105,6 +105,7 @@ import (
"github.com/prebid/prebid-server/v4/adapters/epom"
"github.com/prebid/prebid-server/v4/adapters/escalax"
"github.com/prebid/prebid-server/v4/adapters/exco"
+ "github.com/prebid/prebid-server/v4/adapters/ezoic"
"github.com/prebid/prebid-server/v4/adapters/feedad"
"github.com/prebid/prebid-server/v4/adapters/flatads"
"github.com/prebid/prebid-server/v4/adapters/flipp"
@@ -378,6 +379,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
openrtb_ext.BidderEscalax: escalax.Builder,
openrtb_ext.BidderExco: exco.Builder,
openrtb_ext.BidderEVolution: evolution.Builder,
+ openrtb_ext.BidderEzoic: ezoic.Builder,
openrtb_ext.BidderFeedAd: feedad.Builder,
openrtb_ext.BidderFlatads: flatads.Builder,
openrtb_ext.BidderFlipp: flipp.Builder,
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index aa725e7d1bd..ef529315721 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -122,6 +122,7 @@ var coreBidderNames []BidderName = []BidderName{
BidderEscalax,
BidderEVolution,
BidderExco,
+ BidderEzoic,
BidderFeedAd,
BidderFlatads,
BidderFlipp,
@@ -501,6 +502,7 @@ const (
BidderEscalax BidderName = "escalax"
BidderExco BidderName = "exco"
BidderEVolution BidderName = "e_volution"
+ BidderEzoic BidderName = "ezoic"
BidderFeedAd BidderName = "feedad"
BidderFlatads BidderName = "flatads"
BidderFlipp BidderName = "flipp"
diff --git a/openrtb_ext/imp_ezoic.go b/openrtb_ext/imp_ezoic.go
new file mode 100644
index 00000000000..609650ad5e4
--- /dev/null
+++ b/openrtb_ext/imp_ezoic.go
@@ -0,0 +1,8 @@
+package openrtb_ext
+
+// ExtImpEzoic defines the contract for bidrequest.imp[i].ext.prebid.bidder.ezoic.
+// Ezoic eligibility is resolved server-side from site.domain, so no params are
+// required; placementId is an optional onboarding-assigned identifier.
+type ExtImpEzoic struct {
+ PlacementID string `json:"placementId,omitempty"`
+}
diff --git a/static/bidder-info/ezoic.yaml b/static/bidder-info/ezoic.yaml
new file mode 100644
index 00000000000..9f8be82b52d
--- /dev/null
+++ b/static/bidder-info/ezoic.yaml
@@ -0,0 +1,23 @@
+# Ezoic requires publisher domains to be registered and approved before
+# bidding; unapproved inventory receives no-bid responses. Contact
+# prebid@ezoic.com to get set up.
+endpoint: "https://g.ezoic.net/ezoic/prebid/adapter/ortb"
+maintainer:
+ email: prebid@ezoic.com
+gvlVendorID: 347
+geoscope:
+ - global
+openrtb:
+ version: 2.6
+modifyingVastXmlAllowed: false
+capabilities:
+ app:
+ mediaTypes:
+ - banner
+ - video
+ - native
+ site:
+ mediaTypes:
+ - banner
+ - video
+ - native
diff --git a/static/bidder-params/ezoic.json b/static/bidder-params/ezoic.json
new file mode 100644
index 00000000000..ecd916a4611
--- /dev/null
+++ b/static/bidder-params/ezoic.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Ezoic Adapter Params",
+ "description": "A schema which validates params accepted by the Ezoic adapter",
+ "type": "object",
+ "properties": {
+ "placementId": {
+ "type": "string",
+ "description": "Optional placement identifier assigned during Ezoic onboarding"
+ }
+ },
+ "required": []
+}