diff --git a/modules/programmaticaBidAdapter.js b/modules/programmaticaBidAdapter.js
new file mode 100644
index 00000000000..7d52e305189
--- /dev/null
+++ b/modules/programmaticaBidAdapter.js
@@ -0,0 +1,153 @@
+import { registerBidder } from '../src/adapters/bidderFactory.js';
+import { BANNER, VIDEO } from '../src/mediaTypes.js';
+import { hasPurpose1Consent } from '../src/utils/gpdr.js';
+import { deepAccess, parseSizesInput, isArray } from '../src/utils.js';
+
+const BIDDER_CODE = 'programmatica';
+const DEFAULT_ENDPOINT = 'asr.programmatica.com';
+const SYNC_ENDPOINT = 'sync.programmatica.com';
+const ADOMAIN = 'programmatica.com';
+const TIME_TO_LIVE = 360;
+
+export const spec = {
+ code: BIDDER_CODE,
+
+ isBidRequestValid: function(bid) {
+ let valid = bid.params.siteId && bid.params.placementId;
+
+ return !!valid;
+ },
+
+ buildRequests: function(validBidRequests, bidderRequest) {
+ let requests = [];
+ for (const bid of validBidRequests) {
+ let endpoint = bid.params.endpoint || DEFAULT_ENDPOINT;
+
+ requests.push({
+ method: 'GET',
+ url: `https://${endpoint}/get`,
+ data: {
+ site_id: bid.params.siteId,
+ placement_id: bid.params.placementId,
+ prebid: true,
+ },
+ bidRequest: bid,
+ });
+ }
+
+ return requests;
+ },
+
+ interpretResponse: function(serverResponse, request) {
+ if (!serverResponse?.body?.content?.data) {
+ return [];
+ }
+
+ const bidResponses = [];
+ const body = serverResponse.body;
+
+ let mediaType = BANNER;
+ let ad, vastXml;
+ let width;
+ let height;
+
+ let sizes = getSize(body.size);
+ if (isArray(sizes)) {
+ [width, height] = sizes;
+ }
+
+ if (body.type.format != '') {
+ // banner
+ ad = body.content.data;
+ if (body.content.imps?.length) {
+ for (const imp of body.content.imps) {
+ ad += ``;
+ }
+ }
+ } else {
+ // video
+ vastXml = body.content.data;
+ mediaType = VIDEO;
+
+ if (!width || !height) {
+ const pSize = deepAccess(request.bidRequest, 'mediaTypes.video.playerSize');
+ const reqSize = getSize(pSize);
+ if (isArray(reqSize)) {
+ [width, height] = reqSize;
+ }
+ }
+ }
+
+ const bidResponse = {
+ requestId: request.bidRequest.bidId,
+ cpm: body.cpm,
+ currency: body.currency || 'USD',
+ width: parseInt(width),
+ height: parseInt(height),
+ creativeId: body.id,
+ netRevenue: true,
+ ttl: TIME_TO_LIVE,
+ ad: ad,
+ mediaType: mediaType,
+ vastXml: vastXml,
+ meta: {
+ advertiserDomains: [ADOMAIN],
+ }
+ };
+
+ if ((mediaType === VIDEO && request.bidRequest.mediaTypes?.video) || (mediaType === BANNER && request.bidRequest.mediaTypes?.banner)) {
+ bidResponses.push(bidResponse);
+ }
+
+ return bidResponses;
+ },
+
+ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) {
+ const syncs = []
+
+ if (!hasPurpose1Consent(gdprConsent)) {
+ return syncs;
+ }
+
+ let params = `usp=${uspConsent ?? ''}&consent=${gdprConsent?.consentString ?? ''}`;
+ if (typeof gdprConsent?.gdprApplies === 'boolean') {
+ params += `&gdpr=${Number(gdprConsent.gdprApplies)}`;
+ }
+
+ if (syncOptions.iframeEnabled) {
+ syncs.push({
+ type: 'iframe',
+ url: `//${SYNC_ENDPOINT}/match/sp.ifr?${params}`
+ });
+ }
+
+ if (syncOptions.pixelEnabled) {
+ syncs.push({
+ type: 'image',
+ url: `//${SYNC_ENDPOINT}/match/sp?${params}`
+ });
+ }
+
+ return syncs;
+ },
+
+ onTimeout: function(timeoutData) {},
+ onBidWon: function(bid) {},
+ onSetTargeting: function(bid) {},
+ onBidderError: function() {},
+ supportedMediaTypes: [ BANNER, VIDEO ]
+}
+
+registerBidder(spec);
+
+function getSize(paramSizes) {
+ const parsedSizes = parseSizesInput(paramSizes);
+ const sizes = parsedSizes.map(size => {
+ const [width, height] = size.split('x');
+ const w = parseInt(width, 10);
+ const h = parseInt(height, 10);
+ return [w, h];
+ });
+
+ return sizes[0] || null;
+}
diff --git a/modules/programmaticaBidAdapter.md b/modules/programmaticaBidAdapter.md
new file mode 100644
index 00000000000..5982edf143e
--- /dev/null
+++ b/modules/programmaticaBidAdapter.md
@@ -0,0 +1,46 @@
+# Overview
+
+```
+Module Name: Programmatica Bid Adapter
+Module Type: Bidder Adapter
+Maintainer: tech@programmatica.com
+```
+
+# Description
+Connects to Programmatica server for bids.
+Module supports banner and video mediaType.
+
+# Test Parameters
+
+```
+ var adUnits = [{
+ code: '/test/div',
+ mediaTypes: {
+ banner: {
+ sizes: [[300, 250]]
+ }
+ },
+ bids: [{
+ bidder: 'programmatica',
+ params: {
+ siteId: 'cga9l34ipgja79esubrg',
+ placementId: 'cgim20sipgj0vj1cb510'
+ }
+ }]
+ },
+ {
+ code: '/test/div',
+ mediaTypes: {
+ video: {
+ playerSize: [[640, 360]]
+ }
+ },
+ bids: [{
+ bidder: 'programmatica',
+ params: {
+ siteId: 'cga9l34ipgja79esubrg',
+ placementId: 'cioghpcipgj8r721e9ag'
+ }
+ }]
+ },];
+```
diff --git a/test/spec/modules/programmaticaBidAdapter_spec.js b/test/spec/modules/programmaticaBidAdapter_spec.js
new file mode 100644
index 00000000000..247d20752c3
--- /dev/null
+++ b/test/spec/modules/programmaticaBidAdapter_spec.js
@@ -0,0 +1,263 @@
+import { expect } from 'chai';
+import { spec } from 'modules/programmaticaBidAdapter.js';
+import { deepClone } from 'src/utils.js';
+
+describe('programmaticaBidAdapterTests', function () {
+ let bidRequestData = {
+ bids: [
+ {
+ bidId: 'testbid',
+ bidder: 'programmatica',
+ params: {
+ siteId: 'testsite',
+ placementId: 'testplacement',
+ },
+ sizes: [[300, 250]]
+ }
+ ]
+ };
+ let request = [];
+
+ it('validate_pub_params', function () {
+ expect(
+ spec.isBidRequestValid({
+ bidder: 'programmatica',
+ params: {
+ siteId: 'testsite',
+ placementId: 'testplacement',
+ }
+ })
+ ).to.equal(true);
+ });
+
+ it('validate_generated_url', function () {
+ const request = spec.buildRequests(deepClone(bidRequestData.bids), { timeout: 1234 });
+ let req_url = request[0].url;
+
+ expect(req_url).to.equal('https://asr.programmatica.com/get');
+ });
+
+ it('validate_response_params', function () {
+ let serverResponse = {
+ body: {
+ 'id': 'crid',
+ 'type': {
+ 'format': 'Image',
+ 'source': 'passback',
+ 'dspId': '',
+ 'dspCreativeId': ''
+ },
+ 'content': {
+ 'data': 'test ad',
+ 'imps': null,
+ 'click': {
+ 'url': '',
+ 'track': null
+ }
+ },
+ 'size': '300x250',
+ 'matching': '',
+ 'cpm': 10,
+ 'currency': 'USD'
+ }
+ };
+
+ const bidRequest = deepClone(bidRequestData.bids)
+ bidRequest[0].mediaTypes = {
+ banner: {}
+ }
+
+ const request = spec.buildRequests(bidRequest);
+ let bids = spec.interpretResponse(serverResponse, request[0]);
+ expect(bids).to.have.lengthOf(1);
+
+ let bid = bids[0];
+ expect(bid.ad).to.equal('test ad');
+ expect(bid.cpm).to.equal(10);
+ expect(bid.currency).to.equal('USD');
+ expect(bid.width).to.equal(300);
+ expect(bid.height).to.equal(250);
+ expect(bid.creativeId).to.equal('crid');
+ expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']);
+ });
+
+ it('validate_response_params_imps', function () {
+ let serverResponse = {
+ body: {
+ 'id': 'crid',
+ 'type': {
+ 'format': 'Image',
+ 'source': 'passback',
+ 'dspId': '',
+ 'dspCreativeId': ''
+ },
+ 'content': {
+ 'data': 'test ad',
+ 'imps': [
+ 'testImp'
+ ],
+ 'click': {
+ 'url': '',
+ 'track': null
+ }
+ },
+ 'size': '300x250',
+ 'matching': '',
+ 'cpm': 10,
+ 'currency': 'USD'
+ }
+ };
+
+ const bidRequest = deepClone(bidRequestData.bids)
+ bidRequest[0].mediaTypes = {
+ banner: {}
+ }
+
+ const request = spec.buildRequests(bidRequest);
+ let bids = spec.interpretResponse(serverResponse, request[0]);
+ expect(bids).to.have.lengthOf(1);
+
+ let bid = bids[0];
+ expect(bid.ad).to.equal('test ad');
+ expect(bid.cpm).to.equal(10);
+ expect(bid.currency).to.equal('USD');
+ expect(bid.width).to.equal(300);
+ expect(bid.height).to.equal(250);
+ expect(bid.creativeId).to.equal('crid');
+ expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']);
+ })
+
+ it('validate_invalid_response', function () {
+ let serverResponse = {
+ body: {}
+ };
+
+ const bidRequest = deepClone(bidRequestData.bids)
+ bidRequest[0].mediaTypes = {
+ banner: {}
+ }
+
+ const request = spec.buildRequests(bidRequest);
+ let bids = spec.interpretResponse(serverResponse, request[0]);
+ expect(bids).to.have.lengthOf(0);
+ })
+
+ it('video_bid', function () {
+ const bidRequest = deepClone(bidRequestData.bids);
+ bidRequest[0].mediaTypes = {
+ video: {
+ playerSize: [234, 765]
+ }
+ };
+
+ const request = spec.buildRequests(bidRequest, { timeout: 1234 });
+ const vastXml = '';
+ let serverResponse = {
+ body: {
+ 'id': 'cki2n3n6snkuulqutpf0',
+ 'type': {
+ 'format': '',
+ 'source': 'rtb',
+ 'dspId': '1'
+ },
+ 'content': {
+ 'data': vastXml,
+ 'imps': [
+ 'https://asr.dev.programmatica.com/track/imp'
+ ],
+ 'click': {
+ 'url': '',
+ 'track': null
+ }
+ },
+ 'size': '',
+ 'matching': '',
+ 'cpm': 70,
+ 'currency': 'RUB'
+ }
+ };
+
+ let bids = spec.interpretResponse(serverResponse, request[0]);
+ expect(bids).to.have.lengthOf(1);
+
+ let bid = bids[0];
+ expect(bid.mediaType).to.equal('video');
+ expect(bid.vastXml).to.equal(vastXml);
+ expect(bid.width).to.equal(234);
+ expect(bid.height).to.equal(765);
+ });
+});
+
+describe('getUserSyncs', function() {
+ it('returns empty sync array', function() {
+ const syncOptions = {};
+
+ expect(spec.getUserSyncs(syncOptions)).to.deep.equal([]);
+ });
+
+ it('Should return array of objects with proper sync config , include CCPA', function() {
+ const syncData = spec.getUserSyncs({
+ pixelEnabled: true,
+ }, {}, {}, '1---');
+ expect(syncData).to.be.an('array').which.is.not.empty;
+ expect(syncData[0]).to.be.an('object')
+ expect(syncData[0].type).to.be.a('string')
+ expect(syncData[0].type).to.equal('image')
+ expect(syncData[0].url).to.be.a('string')
+ expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp?usp=1---&consent=')
+ });
+
+ it('Should return array of objects with proper sync config , include GDPR', function() {
+ const syncData = spec.getUserSyncs({
+ iframeEnabled: true,
+ }, {}, {
+ gdprApplies: true,
+ consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw',
+ vendorData: {
+ purpose: {
+ consents: {
+ 1: true
+ },
+ },
+ }
+ }, '');
+ expect(syncData).to.be.an('array').which.is.not.empty;
+ expect(syncData[0]).to.be.an('object')
+ expect(syncData[0].type).to.be.a('string')
+ expect(syncData[0].type).to.equal('iframe')
+ expect(syncData[0].url).to.be.a('string')
+ expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=1')
+ });
+
+ it('Should return array of objects with proper sync config , include GDPR, no purpose', function() {
+ const syncData = spec.getUserSyncs({
+ iframeEnabled: true,
+ }, {}, {
+ gdprApplies: true,
+ consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw',
+ vendorData: {
+ purpose: {
+ consents: {
+ 1: false
+ },
+ },
+ }
+ }, '');
+ expect(syncData).is.empty;
+ });
+
+ it('Should return array of objects with proper sync config , GDPR not applies', function() {
+ const syncData = spec.getUserSyncs({
+ iframeEnabled: true,
+ }, {}, {
+ gdprApplies: false,
+ consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw',
+ }, '');
+ expect(syncData).to.be.an('array').which.is.not.empty;
+ expect(syncData[0]).to.be.an('object')
+ expect(syncData[0].type).to.be.a('string')
+ expect(syncData[0].type).to.equal('iframe')
+ expect(syncData[0].url).to.be.a('string')
+ expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=0')
+ });
+})