diff --git a/modules/.submodules.json b/modules/.submodules.json
index f08db109bb6..e4de1819a16 100644
--- a/modules/.submodules.json
+++ b/modules/.submodules.json
@@ -32,7 +32,8 @@
     "flocIdSystem",
     "amxIdSystem",
     "naveggId",
-    "imuIdSystem"
+    "imuIdSystem",
+    "publinkIdSystem"
   ],
   "adpod": [
     "freeWheelAdserverVideo",
diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js
new file mode 100644
index 00000000000..5e549732097
--- /dev/null
+++ b/modules/publinkIdSystem.js
@@ -0,0 +1,123 @@
+/**
+ * This module adds the PublinkId to the User ID module
+ * The {@link module:modules/userId} module is required
+ * @module modules/publinkIdSystem
+ * @requires module:modules/userId
+ */
+
+import {submodule} from '../src/hook.js';
+import {getStorageManager} from '../src/storageManager.js';
+import {ajax} from '../src/ajax.js';
+import * as utils from '../src/utils.js';
+import {uspDataHandler} from '../src/adapterManager.js';
+
+const MODULE_NAME = 'publinkId';
+const GVLID = 24;
+const PUBLINK_COOKIE = '_publink';
+const PUBLINK_S2S_COOKIE = '_publink_srv';
+
+export const storage = getStorageManager(GVLID);
+
+function publinkIdUrl(params, consentData) {
+  let url = utils.parseUrl('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink');
+  url.search = {
+    deh: params.e,
+    mpn: 'Prebid.js',
+    mpv: '$prebid.version$',
+  };
+  if (consentData) {
+    url.search.gdpr = (consentData.gdprApplies) ? 1 : 0;
+    url.search.gdpr_consent = consentData.consentString;
+  }
+
+  const usPrivacyString = uspDataHandler.getConsentData();
+  if (usPrivacyString && typeof usPrivacyString === 'string') {
+    url.search.us_privacy = usPrivacyString;
+  }
+
+  return utils.buildUrl(url);
+}
+
+function makeCallback(config = {}, consentData) {
+  return function(prebidCallback) {
+    const options = {method: 'GET', withCredentials: true};
+    let handleResponse = function(responseText, xhr) {
+      if (xhr.status === 200) {
+        let response = JSON.parse(responseText);
+        if (response) {
+          prebidCallback(response.publink);
+        }
+      }
+    };
+    if (config.params && config.params.e) {
+      ajax(publinkIdUrl(config.params, consentData), handleResponse, undefined, options);
+    }
+  };
+}
+
+function getlocalValue() {
+  let result;
+  function getData(key) {
+    let value;
+    if (storage.hasLocalStorage()) {
+      value = storage.getDataFromLocalStorage(key);
+    }
+    if (!value) {
+      value = storage.getCookie(key);
+    }
+
+    if (typeof value === 'string') {
+      try {
+        const obj = JSON.parse(value);
+        if (obj && obj.exp && obj.exp > Date.now()) {
+          return obj.publink;
+        }
+      } catch (e) {
+        utils.logError(e);
+      }
+    }
+  }
+  result = getData(PUBLINK_S2S_COOKIE);
+  if (!result) {
+    result = getData(PUBLINK_COOKIE);
+  }
+  return result;
+}
+
+/** @type {Submodule} */
+export const publinkIdSubmodule = {
+  /**
+   * used to link submodule with config
+   * @type {string}
+   */
+  name: MODULE_NAME,
+  gvlid: GVLID,
+
+  /**
+   * decode the stored id value for passing to bid requests
+   * @function
+   * @param {string} id encrypted userid
+   * @returns {{publinkId: string} | undefined}
+   */
+  decode(publinkId) {
+    return {publink: publinkId};
+  },
+
+  /**
+   * performs action to obtain id
+   * Use a publink cookie first if it is present, otherwise use prebids copy, if neither are available callout to get a new id
+   * @function
+   * @param {SubmoduleConfig} [config] Config object with params and storage properties
+   * @returns {IdResponse}
+   */
+  getId: function(config, consentData, storedId) {
+    const localValue = getlocalValue();
+    if (localValue) {
+      return {id: localValue};
+    }
+    if (!storedId) {
+      return {callback: makeCallback(config, consentData)};
+    }
+  }
+};
+submodule('userId', publinkIdSubmodule);
diff --git a/modules/publinkIdSystem.md b/modules/publinkIdSystem.md
new file mode 100644
index 00000000000..669828322a5
--- /dev/null
+++ b/modules/publinkIdSystem.md
@@ -0,0 +1,28 @@
+## Publink User ID Submodule
+
+Publink user id module
+
+## Configuration Descriptions for the `userId` Configuration Section
+
+| Param Name | Required | Type | Description | Example |
+| --- | --- | --- | --- | --- |
+| name | Yes | String | module identifier | `"publinkId"` |
+| params.e | Yes | String | hashed email address | `"e80b5017098950fc58aad83c8c14978e"` |
+
+### Example configuration for Publink
+```
+pbjs.setConfig({
+       userSync: {
+           userIds: [{
+               name: "publinkId",
+               storage: {
+                   name: "pbjs_publink",
+                   type: "html5"
+               },
+               params: {
+                   e: "e80b5017098950fc58aad83c8c14978e", // example hashed email (md5)
+               }
+           }],
+       }
+   });
+```
diff --git a/modules/userId/eids.js b/modules/userId/eids.js
index f6707f211d2..0a8e94883b4 100644
--- a/modules/userId/eids.js
+++ b/modules/userId/eids.js
@@ -228,6 +228,10 @@ const USER_IDS_CONFIG = {
     source: 'amxrtb.com',
     atype: 1,
   },
+  'publinkId': {
+    source: 'epsilon.com',
+    atype: 3
+  },
   'kpuid': {
     source: 'kpuid.com',
     atype: 3
diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js
new file mode 100644
index 00000000000..326285709f3
--- /dev/null
+++ b/test/spec/modules/publinkIdSystem_spec.js
@@ -0,0 +1,136 @@
+import {publinkIdSubmodule} from 'modules/publinkIdSystem.js';
+import {getStorageManager} from '../../../src/storageManager';
+import {server} from 'test/mocks/xhr.js';
+import sinon from 'sinon';
+import {uspDataHandler} from '../../../src/adapterManager';
+
+export const storage = getStorageManager(24);
+const TEST_COOKIE_VALUE = 'cookievalue';
+describe('PublinkIdSystem', () => {
+  describe('decode', () => {
+    it('decode', () => {
+      const result = publinkIdSubmodule.decode(TEST_COOKIE_VALUE);
+      expect(result).deep.equals({publink: TEST_COOKIE_VALUE});
+    });
+  });
+
+  describe('Fetch Local Cookies', () => {
+    const PUBLINK_COOKIE = '_publink';
+    const PUBLINK_SRV_COOKIE = '_publink_srv';
+    const EXP = Date.now() + 60 * 60 * 24 * 7 * 1000;
+    const COOKIE_VALUE = {publink: 'publinkCookieValue', exp: EXP};
+    const LOCAL_VALUE = {publink: 'publinkLocalStorageValue', exp: EXP};
+    const COOKIE_EXPIRATION = (new Date(Date.now() + 60 * 60 * 24 * 1000)).toUTCString();
+    const DELETE_COOKIE = 'Thu, 01 Jan 1970 00:00:01 GMT';
+    it('publink srv cookie', () => {
+      storage.setCookie(PUBLINK_SRV_COOKIE, JSON.stringify(COOKIE_VALUE), COOKIE_EXPIRATION);
+      const result = publinkIdSubmodule.getId();
+      expect(result.id).to.equal(COOKIE_VALUE.publink);
+      storage.setCookie(PUBLINK_SRV_COOKIE, '', DELETE_COOKIE);
+    });
+    it('publink srv local storage', () => {
+      storage.setDataInLocalStorage(PUBLINK_SRV_COOKIE, JSON.stringify(LOCAL_VALUE));
+      const result = publinkIdSubmodule.getId();
+      expect(result.id).to.equal(LOCAL_VALUE.publink);
+      storage.removeDataFromLocalStorage(PUBLINK_SRV_COOKIE);
+    });
+    it('publink cookie', () => {
+      storage.setCookie(PUBLINK_COOKIE, JSON.stringify(COOKIE_VALUE), COOKIE_EXPIRATION);
+      const result = publinkIdSubmodule.getId();
+      expect(result.id).to.equal(COOKIE_VALUE.publink);
+      storage.setCookie(PUBLINK_COOKIE, '', DELETE_COOKIE);
+    });
+    it('publink local storage', () => {
+      storage.setDataInLocalStorage(PUBLINK_COOKIE, JSON.stringify(LOCAL_VALUE));
+      const result = publinkIdSubmodule.getId();
+      expect(result.id).to.equal(LOCAL_VALUE.publink);
+      storage.removeDataFromLocalStorage(PUBLINK_COOKIE);
+    });
+    it('ignore expired cookie', () => {
+      storage.setDataInLocalStorage(PUBLINK_COOKIE, JSON.stringify({publink: 'value', exp: Date.now() - 60 * 60 * 24 * 1000}));
+      const result = publinkIdSubmodule.getId();
+      expect(result.id).to.be.undefined;
+      storage.removeDataFromLocalStorage(PUBLINK_COOKIE);
+    });
+    it('priority goes to publink_srv cookie', () => {
+      storage.setCookie(PUBLINK_SRV_COOKIE, JSON.stringify(COOKIE_VALUE), COOKIE_EXPIRATION);
+      storage.setDataInLocalStorage(PUBLINK_COOKIE, JSON.stringify(LOCAL_VALUE));
+      const result = publinkIdSubmodule.getId();
+      expect(result.id).to.equal(COOKIE_VALUE.publink);
+      storage.setCookie(PUBLINK_SRV_COOKIE, '', DELETE_COOKIE);
+      storage.removeDataFromLocalStorage(PUBLINK_COOKIE);
+    });
+  });
+
+  describe('getId', () => {
+    const serverResponse = {publink: 'ec0xHT3yfAOnykP64Qf0ORSi7LjNT1wju04ZSCsoPBekOJdBwK-0Zl_lXKDNnzhauC4iszBc-PvA1Be6IMlh1QocA'};
+    it('no config', () => {
+      const result = publinkIdSubmodule.getId();
+      expect(result).to.exist;
+      expect(result.callback).to.be.a('function');
+    });
+    it('Use local copy', () => {
+      const result = publinkIdSubmodule.getId({}, undefined, TEST_COOKIE_VALUE);
+      expect(result).to.be.undefined;
+    });
+
+    describe('callout for id', () => {
+      let callbackSpy = sinon.spy();
+
+      beforeEach(() => {
+        callbackSpy.resetHistory();
+      });
+
+      it('Fetch with consent data', () => {
+        const config = {storage: {type: 'cookie'}, params: {e: 'hashedemailvalue'}};
+        const consentData = {gdprApplies: 1, consentString: 'myconsentstring'};
+        let submoduleCallback = publinkIdSubmodule.getId(config, consentData).callback;
+        submoduleCallback(callbackSpy);
+
+        let request = server.requests[0];
+        expect(request.url).to.equal('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink?deh=hashedemailvalue&mpn=Prebid.js&mpv=$prebid.version$&gdpr=1&gdpr_consent=myconsentstring');
+
+        request.respond(200, {}, JSON.stringify(serverResponse));
+        expect(callbackSpy.calledOnce).to.be.true;
+        expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink);
+      });
+
+      it('server doesnt respond', () => {
+        const config = {storage: {type: 'cookie'}, params: {e: 'hashedemailvalue'}};
+        let submoduleCallback = publinkIdSubmodule.getId(config).callback;
+        submoduleCallback(callbackSpy);
+
+        let request = server.requests[0];
+        expect(request.url).to.equal('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink?deh=hashedemailvalue&mpn=Prebid.js&mpv=$prebid.version$');
+
+        request.respond(204, {}, JSON.stringify(serverResponse));
+        expect(callbackSpy.calledOnce).to.be.false;
+      });
+    });
+
+    describe('usPrivacy', () => {
+      let callbackSpy = sinon.spy();
+      const oldPrivacy = uspDataHandler.getConsentData();
+      before(() => {
+        uspDataHandler.setConsentData('1YNN');
+      });
+      after(() => {
+        uspDataHandler.setConsentData(oldPrivacy);
+        callbackSpy.resetHistory();
+      });
+
+      it('Fetch with usprivacy data', () => {
+        const config = {storage: {type: 'cookie'}, params: {e: 'hashedemailvalue'}};
+        let submoduleCallback = publinkIdSubmodule.getId(config).callback;
+        submoduleCallback(callbackSpy);
+
+        let request = server.requests[0];
+        expect(request.url).to.equal('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink?deh=hashedemailvalue&mpn=Prebid.js&mpv=$prebid.version$&us_privacy=1YNN');
+
+        request.respond(200, {}, JSON.stringify(serverResponse));
+        expect(callbackSpy.calledOnce).to.be.true;
+        expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink);
+      });
+    });
+  });
+});
