diff --git a/adapters/scalibur/params_test.go b/adapters/scalibur/params_test.go
new file mode 100644
index 00000000000..e03d549f366
--- /dev/null
+++ b/adapters/scalibur/params_test.go
@@ -0,0 +1,51 @@
+package scalibur
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/prebid/prebid-server/v3/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.BidderScalibur, json.RawMessage(validParam)); err != nil {
+ t.Errorf("Schema rejected scalibur 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.BidderScalibur, json.RawMessage(invalidParam)); err == nil {
+ t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+ }
+ }
+}
+
+var validParams = []string{
+ `{"placementId":"p123"}`,
+ `{"placementId":"p123", "bidfloor": 1.5}`,
+ `{"placementId":"p123", "bidfloor": 1.5, "bidfloorcur": "USD"}`,
+}
+
+var invalidParams = []string{
+ ``,
+ `null`,
+ `true`,
+ `5`,
+ `[]`,
+ `{}`,
+ `{"placementId": 123}`,
+ `{"bidfloor": 1.5}`,
+}
diff --git a/adapters/scalibur/scalibur.go b/adapters/scalibur/scalibur.go
new file mode 100644
index 00000000000..291807462ee
--- /dev/null
+++ b/adapters/scalibur/scalibur.go
@@ -0,0 +1,330 @@
+package scalibur
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "text/template"
+
+ "github.com/prebid/openrtb/v20/adcom1"
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v3/adapters"
+ "github.com/prebid/prebid-server/v3/config"
+ "github.com/prebid/prebid-server/v3/errortypes"
+ "github.com/prebid/prebid-server/v3/openrtb_ext"
+ "github.com/prebid/prebid-server/v3/util/jsonutil"
+)
+
+type adapter struct {
+ endpoint *template.Template
+}
+
+// Builder builds a new instance of the Scalibur adapter for the given bidder with the given config.
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+ temp, err := template.New("endpointTemplate").Parse(config.Endpoint)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse endpoint url template: %v", err)
+ }
+
+ return &adapter{
+ endpoint: temp,
+ }, nil
+}
+
+// MakeRequests creates the HTTP requests which should be made to fetch bids from Scalibur.
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+ var errs []error
+ var validImps []openrtb2.Imp
+
+ // Process each impression
+ for _, imp := range request.Imp {
+ scaliburExt, err := parseScaliburExt(imp.Ext)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+
+ impCopy := imp
+
+ // Apply bid floor and currency
+ if scaliburExt.BidFloor != nil && *scaliburExt.BidFloor > 0 {
+ impCopy.BidFloor = *scaliburExt.BidFloor
+ if scaliburExt.BidFloorCur != "" {
+ impCopy.BidFloorCur = scaliburExt.BidFloorCur
+ }
+ }
+
+ if impCopy.BidFloor > 0 && impCopy.BidFloorCur != "" && impCopy.BidFloorCur != "USD" {
+ convertedValue, err := reqInfo.ConvertCurrency(impCopy.BidFloor, impCopy.BidFloorCur, "USD")
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ impCopy.BidFloor = convertedValue
+ impCopy.BidFloorCur = "USD"
+ }
+
+ if impCopy.BidFloorCur == "" {
+ impCopy.BidFloorCur = "USD"
+ }
+
+ // Prepare imp.ext with placementId and params
+ impExtData := make(map[string]interface{})
+ impExtData["placementId"] = scaliburExt.PlacementID
+
+ if impCopy.BidFloor > 0 {
+ impExtData["bidfloor"] = impCopy.BidFloor
+ }
+ impExtData["bidfloorcur"] = impCopy.BidFloorCur
+
+ // Preserve GPID if present
+ var rawExt map[string]json.RawMessage
+ if err := jsonutil.Unmarshal(imp.Ext, &rawExt); err == nil {
+ if gpid, ok := rawExt["gpid"]; ok {
+ impExtData["gpid"] = json.RawMessage(gpid)
+ }
+ }
+
+ impExt, err := jsonutil.Marshal(impExtData)
+ if err != nil {
+ errs = append(errs, err)
+ continue
+ }
+ impCopy.Ext = impExt
+
+ // Apply video defaults (matching JS defaults)
+ if impCopy.Video != nil {
+ videoCopy := *impCopy.Video
+
+ // Note: In openrtb v20, field names are capitalized (MIMEs not Mimes)
+ if len(videoCopy.MIMEs) == 0 {
+ videoCopy.MIMEs = []string{"video/mp4"}
+ }
+ if videoCopy.MinDuration == 0 {
+ videoCopy.MinDuration = 1
+ }
+ if videoCopy.MaxDuration == 0 {
+ videoCopy.MaxDuration = 180
+ }
+ if videoCopy.MaxBitRate == 0 {
+ videoCopy.MaxBitRate = 30000
+ }
+ if len(videoCopy.Protocols) == 0 {
+ // Use adcom1.MediaCreativeSubtype for protocols in v20
+ videoCopy.Protocols = []adcom1.MediaCreativeSubtype{2, 3, 5, 6}
+ }
+ // Note: In openrtb v20, W and H are pointers
+ if videoCopy.W == nil || *videoCopy.W == 0 {
+ w := int64(640)
+ videoCopy.W = &w
+ }
+ if videoCopy.H == nil || *videoCopy.H == 0 {
+ h := int64(480)
+ videoCopy.H = &h
+ }
+ if videoCopy.Placement == 0 {
+ videoCopy.Placement = 1
+ }
+ if videoCopy.Linearity == 0 {
+ videoCopy.Linearity = 1
+ }
+
+ impCopy.Video = &videoCopy
+ }
+
+ validImps = append(validImps, impCopy)
+ }
+
+ // If no valid impressions, return errors
+ if len(validImps) == 0 {
+ return nil, errs
+ }
+
+ // Create the outgoing request
+ requestCopy := *request
+ requestCopy.Imp = validImps
+ requestCopy.Cur = nil
+
+ isDebug := request.Test == 1
+ if !isDebug && len(request.Ext) > 0 {
+ var extRequest openrtb_ext.ExtRequest
+ if err := jsonutil.Unmarshal(request.Ext, &extRequest); err == nil {
+ isDebug = extRequest.Prebid.Debug
+ }
+ }
+
+ if isDebug {
+ reqExt := openrtb_ext.ExtRequestScalibur{IsDebug: 1}
+ if reqExtJSON, err := jsonutil.Marshal(reqExt); err == nil {
+ requestCopy.Ext = reqExtJSON
+ }
+ } else {
+ requestCopy.Ext = nil
+ }
+
+ reqJSON, err := jsonutil.Marshal(requestCopy)
+ if err != nil {
+ return nil, append(errs, err)
+ }
+
+ var endpointBuffer bytes.Buffer
+ if err := a.endpoint.Execute(&endpointBuffer, nil); err != nil {
+ return nil, []error{err}
+ }
+
+ headers := http.Header{}
+ headers.Add("Content-Type", "application/json;charset=utf-8")
+ headers.Add("Accept", "application/json")
+
+ requestData := &adapters.RequestData{
+ Method: "POST",
+ Uri: endpointBuffer.String(),
+ Body: reqJSON,
+ Headers: headers,
+ ImpIDs: openrtb_ext.GetImpIDs(requestCopy.Imp),
+ }
+
+ return []*adapters.RequestData{requestData}, errs
+}
+
+// MakeBids unpacks the server's response into bids.
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+ if response.StatusCode == http.StatusNoContent {
+ return nil, nil
+ }
+
+ if response.StatusCode != http.StatusOK {
+ return nil, []error{&errortypes.BadServerResponse{
+ Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
+ }}
+ }
+
+ var bidResp openrtb2.BidResponse
+ if err := jsonutil.Unmarshal(response.Body, &bidResp); err != nil {
+ return nil, []error{err}
+ }
+
+ // Parse the external request to get impression details
+ var bidReq openrtb2.BidRequest
+ if err := jsonutil.Unmarshal(externalRequest.Body, &bidReq); err != nil {
+ return nil, []error{err}
+ }
+
+ // Build impression map for lookup
+ impMap := make(map[string]*openrtb2.Imp, len(bidReq.Imp))
+ for i := range bidReq.Imp {
+ impMap[bidReq.Imp[i].ID] = &bidReq.Imp[i]
+ }
+
+ bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(bidReq.Imp))
+
+ // Set currency
+ if bidResp.Cur != "" {
+ bidResponse.Currency = bidResp.Cur
+ } else {
+ bidResponse.Currency = "USD"
+ }
+
+ // Process each seat bid
+ for _, seatBid := range bidResp.SeatBid {
+ for _, bid := range seatBid.Bid {
+ // Find the corresponding imp
+ imp, found := impMap[bid.ImpID]
+ if !found {
+ return nil, []error{&errortypes.BadServerResponse{
+ Message: fmt.Sprintf("Invalid bid imp ID %s", bid.ImpID),
+ }}
+ }
+
+ // Determine bid type based on imp
+ bidType, err := getBidMediaType(bid, imp)
+ if err != nil {
+ return nil, []error{&errortypes.BadServerResponse{
+ Message: err.Error(),
+ }}
+ }
+
+ bidCopy := bid
+
+ // Handle video VAST
+ if bidType == openrtb_ext.BidTypeVideo {
+ // Try to extract custom fields vastXml and vastUrl from bid.ext
+ var bidExtData struct {
+ VastXML string `json:"vastXml"`
+ VastURL string `json:"vastUrl"`
+ }
+ if bid.Ext != nil {
+ if err := jsonutil.Unmarshal(bid.Ext, &bidExtData); err == nil {
+ if bidExtData.VastXML != "" {
+ bidCopy.AdM = bidExtData.VastXML
+ } else if bidExtData.VastURL != "" && bidCopy.AdM == "" {
+ // Wrap VAST URL in VAST wrapper
+ bidCopy.AdM = fmt.Sprintf(``, bidExtData.VastURL)
+ }
+ }
+ }
+ }
+
+ bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+ Bid: &bidCopy,
+ BidType: bidType,
+ })
+ }
+ }
+
+ if len(bidResponse.Bids) == 0 {
+ return nil, nil
+ }
+
+ return bidResponse, nil
+}
+
+// parseScaliburExt extracts Scalibur-specific parameters from the impression extension.
+func parseScaliburExt(impExt json.RawMessage) (*openrtb_ext.ExtImpScalibur, error) {
+ var bidderExt adapters.ExtImpBidder
+ if err := jsonutil.Unmarshal(impExt, &bidderExt); err != nil {
+ return nil, &errortypes.BadInput{
+ Message: fmt.Sprintf("Failed to parse imp.ext: %s", err.Error()),
+ }
+ }
+
+ var scaliburExt openrtb_ext.ExtImpScalibur
+ if err := jsonutil.Unmarshal(bidderExt.Bidder, &scaliburExt); err != nil {
+ return nil, &errortypes.BadInput{
+ Message: fmt.Sprintf("Failed to parse Scalibur params: %s", err.Error()),
+ }
+ }
+
+ if scaliburExt.PlacementID == "" {
+ return nil, &errortypes.BadInput{
+ Message: "placementId is required",
+ }
+ }
+
+ return &scaliburExt, nil
+}
+
+// getBidMediaType determines the media type based on the impression
+func getBidMediaType(bid openrtb2.Bid, imp *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
+ case openrtb2.MarkupAudio:
+ return openrtb_ext.BidTypeAudio, nil
+ case openrtb2.MarkupNative:
+ return openrtb_ext.BidTypeNative, nil
+ }
+
+ // Fallback for bidders not supporting mtype (non-multi-format requests)
+ if imp.Banner != nil && imp.Video == nil {
+ return openrtb_ext.BidTypeBanner, nil
+ }
+ if imp.Video != nil && imp.Banner == nil {
+ return openrtb_ext.BidTypeVideo, nil
+ }
+
+ return "", fmt.Errorf("unsupported or ambiguous media type for bid id=%s", bid.ID)
+}
diff --git a/adapters/scalibur/scalibur_test.go b/adapters/scalibur/scalibur_test.go
new file mode 100644
index 00000000000..2979cba817c
--- /dev/null
+++ b/adapters/scalibur/scalibur_test.go
@@ -0,0 +1,339 @@
+package scalibur
+
+import (
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/prebid/openrtb/v20/openrtb2"
+ "github.com/prebid/prebid-server/v3/adapters"
+ "github.com/prebid/prebid-server/v3/adapters/adapterstest"
+ "github.com/prebid/prebid-server/v3/config"
+ "github.com/prebid/prebid-server/v3/openrtb_ext"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func newTestAdapter() adapters.Bidder {
+ adapter, _ := Builder(
+ openrtb_ext.BidderScalibur,
+ config.Adapter{Endpoint: "https://srv.scalibur.io/adserver/ortb?type=prebid-server"},
+ config.Server{},
+ )
+ return adapter
+}
+
+//
+// ------------------------------------------------------------------------------------------
+// MAKE REQUESTS TESTS
+// ------------------------------------------------------------------------------------------
+//
+
+func TestJsonSamples(t *testing.T) {
+ adapterstest.RunJSONBidderTest(t, "scaliburtest", newTestAdapter())
+}
+
+func TestMakeRequests_SuccessBanner(t *testing.T) {
+ bidder := newTestAdapter()
+
+ ext, _ := json.Marshal(adapters.ExtImpBidder{
+ Bidder: json.RawMessage(`{
+ "placementId": "p123",
+ "bidfloor": 1.25,
+ "bidfloorcur": "EUR"
+ }`),
+ })
+
+ req := &openrtb2.BidRequest{
+ ID: "req1",
+ Imp: []openrtb2.Imp{
+ {
+ ID: "imp1",
+ Ext: ext,
+ Banner: &openrtb2.Banner{
+ W: ptrInt64(300), H: ptrInt64(250),
+ },
+ },
+ },
+ Site: &openrtb2.Site{Page: "https://test.com"},
+ }
+
+ requests, errs := bidder.MakeRequests(req, &adapters.ExtraRequestInfo{
+ CurrencyConversions: &mockConversions{},
+ })
+
+ require.Len(t, errs, 0)
+ require.Len(t, requests, 1)
+
+ r := requests[0]
+ assert.Equal(t, "https://srv.scalibur.io/adserver/ortb?type=prebid-server", r.Uri)
+ assert.Equal(t, "POST", r.Method)
+ assert.Contains(t, r.Headers.Get("Content-Type"), "application/json")
+
+ // Ensure body contains rewritten ext fields
+ var out openrtb2.BidRequest
+ require.NoError(t, json.Unmarshal(r.Body, &out))
+ require.Len(t, out.Imp, 1)
+
+ imp := out.Imp[0]
+
+ assert.Equal(t, float64(1.25), imp.BidFloor)
+ assert.Equal(t, "USD", imp.BidFloorCur)
+
+ var outExt map[string]interface{}
+ require.NoError(t, json.Unmarshal(imp.Ext, &outExt))
+ assert.Equal(t, "p123", outExt["placementId"])
+}
+
+func TestMakeRequests_InvalidExt(t *testing.T) {
+ bidder := newTestAdapter()
+
+ // Missing placementId
+ badExt, _ := json.Marshal(adapters.ExtImpBidder{
+ Bidder: json.RawMessage(`{"bidfloor": 1.5}`),
+ })
+
+ req := &openrtb2.BidRequest{
+ ID: "req2",
+ Imp: []openrtb2.Imp{
+ {
+ ID: "imp1",
+ Ext: badExt,
+ Banner: &openrtb2.Banner{
+ W: ptrInt64(300), H: ptrInt64(250),
+ },
+ },
+ },
+ }
+
+ requests, errs := bidder.MakeRequests(req, &adapters.ExtraRequestInfo{})
+
+ require.Len(t, requests, 0)
+ require.Len(t, errs, 1)
+ assert.Contains(t, errs[0].Error(), "placementId is required")
+}
+
+func TestMakeRequests_VideoDefaultsApplied(t *testing.T) {
+ bidder := newTestAdapter()
+
+ ext, _ := json.Marshal(adapters.ExtImpBidder{
+ Bidder: json.RawMessage(`{"placementId": "p123"}`),
+ })
+
+ req := &openrtb2.BidRequest{
+ ID: "req-video",
+ Imp: []openrtb2.Imp{
+ {
+ ID: "v1",
+ Ext: ext,
+ Video: &openrtb2.Video{
+ // Intentionally empty → should fill defaults
+ },
+ },
+ },
+ }
+
+ requests, errs := bidder.MakeRequests(req, &adapters.ExtraRequestInfo{
+ CurrencyConversions: &mockConversions{},
+ })
+
+ require.Len(t, errs, 0)
+ require.Len(t, requests, 1)
+
+ var out openrtb2.BidRequest
+ require.NoError(t, json.Unmarshal(requests[0].Body, &out))
+
+ v := out.Imp[0].Video
+ require.NotNil(t, v)
+
+ assert.NotEmpty(t, v.MIMEs)
+ assert.NotZero(t, v.MinDuration)
+ assert.NotZero(t, v.MaxDuration)
+ assert.NotZero(t, v.MaxBitRate)
+ assert.NotEmpty(t, v.Protocols)
+ assert.NotNil(t, v.W)
+ assert.NotNil(t, v.H)
+ assert.NotZero(t, v.Placement)
+ assert.NotZero(t, v.Linearity)
+}
+
+type mockConversions struct{}
+
+func (m *mockConversions) GetRate(from string, to string) (float64, error) {
+ return 1.0, nil
+}
+
+func (m *mockConversions) GetRates() *map[string]map[string]float64 {
+ return nil
+}
+
+//
+// ------------------------------------------------------------------------------------------
+// MAKE BIDS TESTS
+// ------------------------------------------------------------------------------------------
+//
+
+func TestMakeBids_SuccessBanner(t *testing.T) {
+ bidder := newTestAdapter()
+
+ mockReq := &openrtb2.BidRequest{
+ ID: "req1",
+ Imp: []openrtb2.Imp{
+ {
+ ID: "1",
+ Banner: &openrtb2.Banner{W: ptrInt64(300), H: ptrInt64(250)},
+ },
+ },
+ }
+
+ mockReqData := &adapters.RequestData{
+ Body: func() []byte {
+ b, _ := json.Marshal(mockReq)
+ return b
+ }(),
+ }
+
+ mockResp := &openrtb2.BidResponse{
+ ID: "resp1",
+ Cur: "USD",
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "b1",
+ ImpID: "1",
+ Price: 2.5,
+ AdM: "
ad markup
",
+ W: 300,
+ H: 250,
+ },
+ },
+ },
+ },
+ }
+
+ respData := &adapters.ResponseData{
+ StatusCode: http.StatusOK,
+ Body: func() []byte {
+ b, _ := json.Marshal(mockResp)
+ return b
+ }(),
+ }
+
+ bidderResp, errs := bidder.MakeBids(mockReq, mockReqData, respData)
+
+ require.Len(t, errs, 0)
+ require.NotNil(t, bidderResp)
+ require.Len(t, bidderResp.Bids, 1)
+
+ b := bidderResp.Bids[0]
+ assert.Equal(t, openrtb_ext.BidTypeBanner, b.BidType)
+ assert.Equal(t, float64(2.5), b.Bid.Price)
+ assert.Equal(t, "ad markup
", b.Bid.AdM)
+}
+
+func TestMakeBids_EmptySeatBid(t *testing.T) {
+ bidder := newTestAdapter()
+
+ mockReq := &openrtb2.BidRequest{
+ ID: "req2",
+ Imp: []openrtb2.Imp{},
+ }
+
+ mockReqData := &adapters.RequestData{
+ Body: []byte(`{"id":"req2","imp":[]}`),
+ }
+
+ respData := &adapters.ResponseData{
+ StatusCode: http.StatusOK,
+ Body: []byte(`{"id":"resp","seatbid":[]}`),
+ }
+
+ bidderResp, errs := bidder.MakeBids(mockReq, mockReqData, respData)
+
+ require.Len(t, errs, 0)
+ assert.Nil(t, bidderResp)
+}
+
+func TestMakeBids_Status204(t *testing.T) {
+ bidder := newTestAdapter()
+
+ respData := &adapters.ResponseData{
+ StatusCode: http.StatusNoContent,
+ }
+
+ resp, errs := bidder.MakeBids(&openrtb2.BidRequest{}, &adapters.RequestData{}, respData)
+
+ assert.Nil(t, resp)
+ assert.Nil(t, errs)
+}
+
+func TestMakeBids_InvalidJSON(t *testing.T) {
+ bidder := newTestAdapter()
+
+ respData := &adapters.ResponseData{
+ StatusCode: http.StatusOK,
+ Body: []byte(`not-json`),
+ }
+
+ _, errs := bidder.MakeBids(&openrtb2.BidRequest{}, &adapters.RequestData{}, respData)
+ require.Len(t, errs, 1)
+}
+
+func TestMakeBids_ImpNotFound(t *testing.T) {
+ bidder := newTestAdapter()
+
+ mockReq := &openrtb2.BidRequest{
+ ID: "req1",
+ Imp: []openrtb2.Imp{
+ {ID: "1"},
+ },
+ }
+
+ mockReqData := &adapters.RequestData{
+ Body: []byte(`{"id":"req1","imp":[{"id":"1"}]}`),
+ }
+
+ mockResp := &openrtb2.BidResponse{
+ ID: "resp1",
+ SeatBid: []openrtb2.SeatBid{
+ {
+ Bid: []openrtb2.Bid{
+ {
+ ID: "b1",
+ ImpID: "non-existent",
+ },
+ },
+ },
+ },
+ }
+
+ respData := &adapters.ResponseData{
+ StatusCode: http.StatusOK,
+ Body: func() []byte {
+ b, _ := json.Marshal(mockResp)
+ return b
+ }(),
+ }
+
+ _, errs := bidder.MakeBids(mockReq, mockReqData, respData)
+ require.Len(t, errs, 1)
+ assert.Contains(t, errs[0].Error(), "Invalid bid imp ID non-existent")
+}
+
+func TestGetBidMediaType_Error(t *testing.T) {
+ bid := openrtb2.Bid{
+ ID: "bid1",
+ ImpID: "imp1",
+ }
+ imp := &openrtb2.Imp{
+ ID: "imp1",
+ // No banner or video
+ }
+
+ _, err := getBidMediaType(bid, imp)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "unsupported or ambiguous media type")
+}
+
+func ptrInt64(x int64) *int64 { return &x }
diff --git a/adapters/scalibur/scaliburtest/exemplary/debug.json b/adapters/scalibur/scaliburtest/exemplary/debug.json
new file mode 100644
index 00000000000..4422ffb07ee
--- /dev/null
+++ b/adapters/scalibur/scaliburtest/exemplary/debug.json
@@ -0,0 +1,59 @@
+{
+ "mockBidRequest": {
+ "id": "test-debug-request",
+ "test": 1,
+ "imp": [{
+ "id": "test-imp-id",
+ "banner": { "format": [{"w": 300, "h": 250}] },
+ "ext": { "bidder": { "placementId": "test-placement" } }
+ }]
+ },
+ "httpCalls": [{
+ "expectedRequest": {
+ "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server",
+ "impIDs": ["test-imp-id"],
+ "body": {
+ "id": "test-debug-request",
+ "test": 1,
+ "imp": [{
+ "id": "test-imp-id",
+ "banner": { "format": [{"w": 300, "h": 250}] },
+ "bidfloorcur": "USD",
+ "ext": {
+ "placementId": "test-placement",
+ "bidfloorcur": "USD"
+ }
+ }],
+ "ext": { "isDebug": 1 }
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-debug-request",
+ "seatbid": [{
+ "bid": [{
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "...",
+ "crid": "test-creative-id"
+ }]
+ }]
+ }
+ }
+ }],
+ "expectedBidResponses": [{
+ "currency": "USD",
+ "bids": [{
+ "bid": {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "...",
+ "crid": "test-creative-id"
+ },
+ "type": "banner"
+ }]
+ }]
+}
diff --git a/adapters/scalibur/scaliburtest/exemplary/debug_prebid.json b/adapters/scalibur/scaliburtest/exemplary/debug_prebid.json
new file mode 100644
index 00000000000..5f780c6810f
--- /dev/null
+++ b/adapters/scalibur/scaliburtest/exemplary/debug_prebid.json
@@ -0,0 +1,62 @@
+{
+ "mockBidRequest": {
+ "id": "test-debug-prebid-request",
+ "imp": [{
+ "id": "test-imp-id",
+ "banner": { "format": [{"w": 300, "h": 250}] },
+ "ext": { "bidder": { "placementId": "test-placement" } }
+ }],
+ "ext": {
+ "prebid": {
+ "debug": true
+ }
+ }
+ },
+ "httpCalls": [{
+ "expectedRequest": {
+ "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server",
+ "impIDs": ["test-imp-id"],
+ "body": {
+ "id": "test-debug-prebid-request",
+ "imp": [{
+ "id": "test-imp-id",
+ "banner": { "format": [{"w": 300, "h": 250}] },
+ "bidfloorcur": "USD",
+ "ext": {
+ "placementId": "test-placement",
+ "bidfloorcur": "USD"
+ }
+ }],
+ "ext": { "isDebug": 1 }
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-debug-prebid-request",
+ "seatbid": [{
+ "bid": [{
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "...",
+ "crid": "test-creative-id"
+ }]
+ }]
+ }
+ }
+ }],
+ "expectedBidResponses": [{
+ "currency": "USD",
+ "bids": [{
+ "bid": {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "...",
+ "crid": "test-creative-id"
+ },
+ "type": "banner"
+ }]
+ }]
+}
diff --git a/adapters/scalibur/scaliburtest/exemplary/gpid.json b/adapters/scalibur/scaliburtest/exemplary/gpid.json
new file mode 100644
index 00000000000..393c8a86ddb
--- /dev/null
+++ b/adapters/scalibur/scaliburtest/exemplary/gpid.json
@@ -0,0 +1,65 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [{
+ "id": "test-imp-id",
+ "banner": { "format": [{"w": 300, "h": 250}] },
+ "ext": {
+ "bidder": { "placementId": "test-scl-placement" },
+ "gpid": "test-gpid-value"
+ }
+ }]
+ },
+ "httpCalls": [{
+ "expectedRequest": {
+ "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server",
+ "impIDs": ["test-imp-id"],
+ "body": {
+ "id": "test-request-id",
+ "imp": [{
+ "id": "test-imp-id",
+ "banner": { "format": [{"w": 300, "h": 250}] },
+ "bidfloorcur": "USD",
+ "ext": {
+ "placementId": "test-scl-placement",
+ "bidfloorcur": "USD",
+ "gpid": "test-gpid-value"
+ }
+ }]
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "seatbid": [{
+ "bid": [{
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "...",
+ "crid": "test-creative-id",
+ "w": 300,
+ "h": 250
+ }]
+ }],
+ "cur": "USD"
+ }
+ }
+ }],
+ "expectedBidResponses": [{
+ "currency": "USD",
+ "bids": [{
+ "bid": {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "...",
+ "crid": "test-creative-id",
+ "w": 300,
+ "h": 250
+ },
+ "type": "banner"
+ }]
+ }]
+}
diff --git a/adapters/scalibur/scaliburtest/exemplary/mtype.json b/adapters/scalibur/scaliburtest/exemplary/mtype.json
new file mode 100644
index 00000000000..4256373d8f8
--- /dev/null
+++ b/adapters/scalibur/scaliburtest/exemplary/mtype.json
@@ -0,0 +1,58 @@
+{
+ "mockBidRequest": {
+ "id": "test-mtype-request",
+ "imp": [{
+ "id": "test-imp-id",
+ "banner": { "format": [{"w": 300, "h": 250}] },
+ "ext": { "bidder": { "placementId": "test-placement" } }
+ }]
+ },
+ "httpCalls": [{
+ "expectedRequest": {
+ "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server",
+ "impIDs": ["test-imp-id"],
+ "body": {
+ "id": "test-mtype-request",
+ "imp": [{
+ "id": "test-imp-id",
+ "banner": { "format": [{"w": 300, "h": 250}] },
+ "bidfloorcur": "USD",
+ "ext": {
+ "placementId": "test-placement",
+ "bidfloorcur": "USD"
+ }
+ }]
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-mtype-request",
+ "seatbid": [{
+ "bid": [{
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "...",
+ "crid": "test-creative-id",
+ "mtype": 1
+ }]
+ }]
+ }
+ }
+ }],
+ "expectedBidResponses": [{
+ "currency": "USD",
+ "bids": [{
+ "bid": {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "...",
+ "crid": "test-creative-id",
+ "mtype": 1
+ },
+ "type": "banner"
+ }]
+ }]
+}
diff --git a/adapters/scalibur/scaliburtest/exemplary/success_banner.json b/adapters/scalibur/scaliburtest/exemplary/success_banner.json
new file mode 100644
index 00000000000..aabe2beb479
--- /dev/null
+++ b/adapters/scalibur/scaliburtest/exemplary/success_banner.json
@@ -0,0 +1,61 @@
+{
+ "mockBidRequest": {
+ "id": "test-request-id",
+ "imp": [{
+ "id": "test-imp-id",
+ "banner": { "format": [{"w": 300, "h": 250}] },
+ "ext": { "bidder": { "placementId": "test-scl-placement" } }
+ }]
+ },
+ "httpCalls": [{
+ "expectedRequest": {
+ "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server",
+ "impIDs": ["test-imp-id"],
+ "body": {
+ "id": "test-request-id",
+ "imp": [{
+ "id": "test-imp-id",
+ "banner": { "format": [{"w": 300, "h": 250}] },
+ "bidfloorcur": "USD",
+ "ext": {
+ "placementId": "test-scl-placement",
+ "bidfloorcur": "USD"
+ }
+ }]
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-request-id",
+ "seatbid": [{
+ "bid": [{
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "...",
+ "crid": "test-creative-id",
+ "w": 300,
+ "h": 250
+ }]
+ }],
+ "cur": "USD"
+ }
+ }
+ }],
+ "expectedBidResponses": [{
+ "currency": "USD",
+ "bids": [{
+ "bid": {
+ "id": "test-bid-id",
+ "impid": "test-imp-id",
+ "price": 0.5,
+ "adm": "...",
+ "crid": "test-creative-id",
+ "w": 300,
+ "h": 250
+ },
+ "type": "banner"
+ }]
+ }]
+}
\ No newline at end of file
diff --git a/adapters/scalibur/scaliburtest/exemplary/video.json b/adapters/scalibur/scaliburtest/exemplary/video.json
new file mode 100644
index 00000000000..7eaea7a2895
--- /dev/null
+++ b/adapters/scalibur/scaliburtest/exemplary/video.json
@@ -0,0 +1,78 @@
+{
+ "mockBidRequest": {
+ "id": "test-video-req",
+ "imp": [{
+ "id": "video-imp-1",
+ "video": {
+ "mimes": ["video/mp4"],
+ "w": 640,
+ "h": 480
+ },
+ "ext": {
+ "bidder": {
+ "placementId": "46"
+ }
+ }
+ }]
+ },
+ "httpCalls": [{
+ "expectedRequest": {
+ "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server",
+ "impIDs": ["video-imp-1"],
+ "body": {
+ "id": "test-video-req",
+ "imp": [{
+ "id": "video-imp-1",
+ "video": {
+ "mimes": ["video/mp4"],
+ "minduration": 1,
+ "maxduration": 180,
+ "protocols": [2, 3, 5, 6],
+ "w": 640,
+ "h": 480,
+ "placement": 1,
+ "linearity": 1,
+ "maxbitrate": 30000
+ },
+ "bidfloorcur": "USD",
+ "ext": {
+ "placementId": "46",
+ "bidfloorcur": "USD"
+ }
+ }]
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-video-req",
+ "seatbid": [{
+ "bid": [{
+ "id": "bid-v1",
+ "impid": "video-imp-1",
+ "price": 1.5,
+ "crid": "cr-video",
+ "ext": {
+ "vastXml": "..."
+ }
+ }]
+ }],
+ "cur": "USD"
+ }
+ }
+ }],
+ "expectedBidResponses": [{
+ "currency": "USD",
+ "bids": [{
+ "bid": {
+ "id": "bid-v1",
+ "impid": "video-imp-1",
+ "price": 1.5,
+ "adm": "...",
+ "crid": "cr-video",
+ "ext": { "vastXml": "..." }
+ },
+ "type": "video"
+ }]
+ }]
+}
\ No newline at end of file
diff --git a/adapters/scalibur/scaliburtest/exemplary/video_vast_url.json b/adapters/scalibur/scaliburtest/exemplary/video_vast_url.json
new file mode 100644
index 00000000000..f00833b95b9
--- /dev/null
+++ b/adapters/scalibur/scaliburtest/exemplary/video_vast_url.json
@@ -0,0 +1,67 @@
+{
+ "mockBidRequest": {
+ "id": "test-video-url-req",
+ "imp": [{
+ "id": "video-imp-2",
+ "video": { "mimes": ["video/mp4"] },
+ "ext": { "bidder": { "placementId": "46" } }
+ }]
+ },
+ "httpCalls": [{
+ "expectedRequest": {
+ "uri": "https://srv.scalibur.io/adserver/ortb?type=prebid-server",
+ "impIDs": ["video-imp-2"],
+ "body": {
+ "id": "test-video-url-req",
+ "imp": [{
+ "id": "video-imp-2",
+ "video": {
+ "mimes": ["video/mp4"],
+ "minduration": 1,
+ "maxduration": 180,
+ "protocols": [2, 3, 5, 6],
+ "w": 640,
+ "h": 480,
+ "placement": 1,
+ "linearity": 1,
+ "maxbitrate": 30000
+ },
+ "bidfloorcur": "USD",
+ "ext": {
+ "placementId": "46",
+ "bidfloorcur": "USD"
+ }
+ }]
+ }
+ },
+ "mockResponse": {
+ "status": 200,
+ "body": {
+ "id": "test-video-url-req",
+ "seatbid": [{
+ "bid": [{
+ "id": "bid-v2",
+ "impid": "video-imp-2",
+ "price": 2.0,
+ "ext": {
+ "vastUrl": "https://cdn.scalibur.io/vast.xml"
+ }
+ }]
+ }]
+ }
+ }
+ }],
+ "expectedBidResponses": [{
+ "currency": "USD",
+ "bids": [{
+ "bid": {
+ "id": "bid-v2",
+ "impid": "video-imp-2",
+ "price": 2.0,
+ "adm": "",
+ "ext": { "vastUrl": "https://cdn.scalibur.io/vast.xml" }
+ },
+ "type": "video"
+ }]
+ }]
+}
\ No newline at end of file
diff --git a/docs/bidders/scalibur.md b/docs/bidders/scalibur.md
new file mode 100644
index 00000000000..95f409b997d
--- /dev/null
+++ b/docs/bidders/scalibur.md
@@ -0,0 +1,16 @@
+# Scalibur
+
+The Scalibur adapter supports Banner and Video media types via OpenRTB 2.6.
+
+## Registration
+Contact [support@scalibur.io](mailto:support@scalibur.io) to obtain a valid `placementId`.
+
+## Bid Params
+| Name | Type | Description | Notes |
+| :--- | :--- | :--- |:----------------------|
+| `placementId` | string | **Required**. Scalibur placement identifier. | Example: `"468acd11"` |
+| `bidfloor` | number | Optional. Minimum bid floor price. | |
+| `bidfloorcur` | string | Optional. Currency for the bid floor. | Default is `USD`. |
+
+## User Sync
+Supports iframe synchronization. Prebid Server handles the mapping of the sync ID to the `user.buyeruid` field in outgoing OpenRTB requests.
\ No newline at end of file
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 3cb651bcbdb..cccc1bdf288 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -201,6 +201,7 @@ import (
"github.com/prebid/prebid-server/v3/adapters/rtbhouse"
"github.com/prebid/prebid-server/v3/adapters/rubicon"
salunamedia "github.com/prebid/prebid-server/v3/adapters/sa_lunamedia"
+ "github.com/prebid/prebid-server/v3/adapters/scalibur"
"github.com/prebid/prebid-server/v3/adapters/screencore"
"github.com/prebid/prebid-server/v3/adapters/seedingAlliance"
"github.com/prebid/prebid-server/v3/adapters/seedtag"
@@ -473,6 +474,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
openrtb_ext.BidderSeedingAlliance: seedingAlliance.Builder,
openrtb_ext.BidderSeedtag: seedtag.Builder,
openrtb_ext.BidderSaLunaMedia: salunamedia.Builder,
+ openrtb_ext.BidderScalibur: scalibur.Builder,
openrtb_ext.BidderScreencore: screencore.Builder,
openrtb_ext.BidderSharethrough: sharethrough.Builder,
openrtb_ext.BidderShowheroes: showheroes.Builder,
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index e94d0fe5513..e563260ca1f 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -221,6 +221,7 @@ var coreBidderNames []BidderName = []BidderName{
BidderSeedingAlliance,
BidderSeedtag,
BidderSaLunaMedia,
+ BidderScalibur,
BidderScreencore,
BidderSharethrough,
BidderShowheroes,
@@ -595,6 +596,7 @@ const (
BidderSeedingAlliance BidderName = "seedingAlliance"
BidderSeedtag BidderName = "seedtag"
BidderSaLunaMedia BidderName = "sa_lunamedia"
+ BidderScalibur BidderName = "scalibur"
BidderScreencore BidderName = "screencore"
BidderSharethrough BidderName = "sharethrough"
BidderShowheroes BidderName = "showheroes"
diff --git a/openrtb_ext/imp_scalibur.go b/openrtb_ext/imp_scalibur.go
new file mode 100644
index 00000000000..9a0ff7902f5
--- /dev/null
+++ b/openrtb_ext/imp_scalibur.go
@@ -0,0 +1,11 @@
+package openrtb_ext
+
+type ExtImpScalibur struct {
+ PlacementID string `json:"placementId"` // required
+ BidFloor *float64 `json:"bidfloor,omitempty"` // optional, used as fallback
+ BidFloorCur string `json:"bidfloorcur,omitempty"` // optional, defaults to USD if empty
+}
+
+type ExtRequestScalibur struct {
+ IsDebug int `json:"isDebug,omitempty"`
+}
diff --git a/static/bidder-info/scalibur.yaml b/static/bidder-info/scalibur.yaml
new file mode 100644
index 00000000000..4b6a081d81a
--- /dev/null
+++ b/static/bidder-info/scalibur.yaml
@@ -0,0 +1,34 @@
+endpoint: "https://srv.scalibur.io/adserver/ortb?type=prebid-server"
+
+# Leave disabled: false → adapter enabled
+compression: gzip
+disabled: false
+
+maintainer:
+ email: "support@scalibur.io"
+supported-media-types:
+ - banner
+ - video
+
+gvlVendorID: 1471
+
+openrtb:
+ version: 2.6
+ gpp-supported: true
+
+capabilities:
+ site:
+ mediaTypes:
+ - banner
+ - video
+ app:
+ mediaTypes:
+ - banner
+ - video
+
+userSync:
+ iframe:
+ url: "https://srv.scalibur.io/adserver/sync?type=iframe&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect={{.RedirectURL}}"
+ userMacro: "[PBS_UID]"
+ supports:
+ - iframe
\ No newline at end of file
diff --git a/static/bidder-params/scalibur.json b/static/bidder-params/scalibur.json
new file mode 100644
index 00000000000..05a12e99f24
--- /dev/null
+++ b/static/bidder-params/scalibur.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Scalibur Adapter Params",
+ "description": "A valid BidderParams object for the Scalibur adapter",
+ "type": "object",
+ "properties": {
+ "placementId": {
+ "type": "string",
+ "minLength": 1
+ },
+ "bidfloor": {
+ "type": "number",
+ "minimum": 0
+ },
+ "bidfloorcur": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "placementId"
+ ]
+}