diff --git a/common/djangoapps/terrain/stubs/catalog.py b/common/djangoapps/terrain/stubs/catalog.py
new file mode 100644
index 0000000000000000000000000000000000000000..6ba8f0ae32b4148e728555e819f5b355f0423f8f
--- /dev/null
+++ b/common/djangoapps/terrain/stubs/catalog.py
@@ -0,0 +1,43 @@
+"""
+Stub implementation of catalog service for acceptance tests
+"""
+import re
+import urlparse
+
+from .http import StubHttpRequestHandler, StubHttpService
+
+
+class StubCatalogServiceHandler(StubHttpRequestHandler):  # pylint: disable=missing-docstring
+
+    def do_GET(self):  # pylint: disable=invalid-name, missing-docstring
+        pattern_handlers = {
+            r'/api/v1/course_runs/(?P<course_id>[^/+]+(/|\+)[^/+]+(/|\+)[^/?]+)/$': self.get_course_run,
+        }
+
+        if self.match_pattern(pattern_handlers):
+            return
+
+        self.send_response(404, content="404 Not Found")
+
+    def match_pattern(self, pattern_handlers):
+        """
+        Find the correct handler method given the path info from the HTTP request.
+        """
+        path = urlparse.urlparse(self.path).path
+        for pattern in pattern_handlers:
+            match = re.match(pattern, path)
+            if match:
+                pattern_handlers[pattern](*match.groups())
+                return True
+        return None
+
+    def get_course_run(self, course_id):
+        """
+        Stubs a catalog course run endpoint.
+        """
+        course_run = self.server.config.get('course_run.{}'.format(course_id), [])
+        self.send_json_response(course_run)
+
+
+class StubCatalogService(StubHttpService):  # pylint: disable=missing-docstring
+    HANDLER_CLASS = StubCatalogServiceHandler
diff --git a/common/djangoapps/terrain/stubs/start.py b/common/djangoapps/terrain/stubs/start.py
index ad6d826eac4f34f6d8e1178ad680a69a2e2b7237..9bb35d70a3358dbfaa1a70effb3ebf4bdcb375c9 100644
--- a/common/djangoapps/terrain/stubs/start.py
+++ b/common/djangoapps/terrain/stubs/start.py
@@ -13,6 +13,7 @@ from .lti import StubLtiService
 from .video_source import VideoSourceHttpService
 from .edxnotes import StubEdxNotesService
 from .programs import StubProgramsService
+from .catalog import StubCatalogService
 
 
 USAGE = "USAGE: python -m stubs.start SERVICE_NAME PORT_NUM [CONFIG_KEY=CONFIG_VAL, ...]"
@@ -26,6 +27,7 @@ SERVICES = {
     'edxnotes': StubEdxNotesService,
     'programs': StubProgramsService,
     'ecommerce': StubEcommerceService,
+    'catalog': StubCatalogService,
 }
 
 # Log to stdout, including debug messages
diff --git a/common/test/acceptance/fixtures/__init__.py b/common/test/acceptance/fixtures/__init__.py
index bbe146b43087f5970c541d3fef43fb323cffc543..e05975185612dcfe64655bcb56ad360273e0624e 100644
--- a/common/test/acceptance/fixtures/__init__.py
+++ b/common/test/acceptance/fixtures/__init__.py
@@ -1,5 +1,6 @@
 import os
 
+
 # Get the URL of the Studio instance under test
 STUDIO_BASE_URL = os.environ.get('studio_url', 'http://localhost:8031')
 
@@ -20,3 +21,6 @@ EDXNOTES_STUB_URL = os.environ.get('edxnotes_url', 'http://localhost:8042')
 
 # Get the URL of the Programs service stub used in the test
 PROGRAMS_STUB_URL = os.environ.get('programs_url', 'http://localhost:8090')
+
+# Get the URL of the Catalog service stub used in the test
+CATALOG_STUB_URL = os.environ.get('catalog_url', 'http://localhost:8091')
diff --git a/common/test/acceptance/fixtures/catalog.py b/common/test/acceptance/fixtures/catalog.py
new file mode 100644
index 0000000000000000000000000000000000000000..a60af67a4ccd0513f98caaff9b02e359f5b95ee5
--- /dev/null
+++ b/common/test/acceptance/fixtures/catalog.py
@@ -0,0 +1,34 @@
+"""
+Tools to create catalog-related data for use in bok choy tests.
+"""
+import json
+
+import requests
+
+from common.test.acceptance.fixtures import CATALOG_STUB_URL
+from common.test.acceptance.fixtures.config import ConfigModelFixture
+
+
+class CatalogFixture(object):
+    """
+    Interface to set up mock responses from the Catalog stub server.
+    """
+    def install_course_run(self, course_run):
+        """Set response data for the catalog's course run API."""
+        key = 'catalog.{}'.format(course_run['key'])
+
+        requests.put(
+            '{}/set_config'.format(CATALOG_STUB_URL),
+            data={key: json.dumps(course_run)},
+        )
+
+
+class CatalogConfigMixin(object):
+    """Mixin providing a method used to configure the catalog integration."""
+    def set_catalog_configuration(self, is_enabled=False, service_url=CATALOG_STUB_URL):
+        """Dynamically adjusts the catalog config model during tests."""
+        ConfigModelFixture('/config/catalog', {
+            'enabled': is_enabled,
+            'internal_api_url': '{}/api/v1/'.format(service_url),
+            'cache_ttl': 0,
+        }).install()
diff --git a/common/test/acceptance/tests/lms/test_programs.py b/common/test/acceptance/tests/lms/test_programs.py
index 48154a176cc9e5d07e7f9f570d5a9bd2623abb63..b4f610d44933a4868a3607933677423414f64d26 100644
--- a/common/test/acceptance/tests/lms/test_programs.py
+++ b/common/test/acceptance/tests/lms/test_programs.py
@@ -1,38 +1,44 @@
 """Acceptance tests for LMS-hosted Programs pages"""
 from nose.plugins.attrib import attr
 
+from ...fixtures.catalog import CatalogFixture, CatalogConfigMixin
 from ...fixtures.programs import ProgramsFixture, ProgramsConfigMixin
 from ...fixtures.course import CourseFixture
 from ..helpers import UniqueCourseTest
 from ...pages.lms.auto_auth import AutoAuthPage
 from ...pages.lms.programs import ProgramListingPage, ProgramDetailsPage
-from openedx.core.djangoapps.programs.tests import factories
+from openedx.core.djangoapps.catalog.tests import factories as catalog_factories
+from openedx.core.djangoapps.programs.tests import factories as program_factories
 
 
-class ProgramPageBase(ProgramsConfigMixin, UniqueCourseTest):
+class ProgramPageBase(ProgramsConfigMixin, CatalogConfigMixin, UniqueCourseTest):
     """Base class used for program listing page tests."""
     def setUp(self):
         super(ProgramPageBase, self).setUp()
 
         self.set_programs_api_configuration(is_enabled=True)
+        self.set_catalog_configuration(is_enabled=True)
+
+        self.course_run = catalog_factories.CourseRun(key=self.course_id)
+        self.stub_catalog_api()
 
     def create_program(self, program_id=None, course_id=None):
         """DRY helper for creating test program data."""
         course_id = course_id if course_id else self.course_id
 
-        run_mode = factories.RunMode(course_key=course_id)
-        course_code = factories.CourseCode(run_modes=[run_mode])
-        org = factories.Organization(key=self.course_info['org'])
+        run_mode = program_factories.RunMode(course_key=course_id)
+        course_code = program_factories.CourseCode(run_modes=[run_mode])
+        org = program_factories.Organization(key=self.course_info['org'])
 
         if program_id:
-            program = factories.Program(
+            program = program_factories.Program(
                 id=program_id,
                 status='active',
                 organizations=[org],
                 course_codes=[course_code]
             )
         else:
-            program = factories.Program(
+            program = program_factories.Program(
                 status='active',
                 organizations=[org],
                 course_codes=[course_code]
@@ -40,10 +46,14 @@ class ProgramPageBase(ProgramsConfigMixin, UniqueCourseTest):
 
         return program
 
-    def stub_api(self, programs, is_list=True):
+    def stub_programs_api(self, programs, is_list=True):
         """Stub out the programs API with fake data."""
         ProgramsFixture().install_programs(programs, is_list=is_list)
 
+    def stub_catalog_api(self):
+        """Stub out the catalog API's course run endpoint."""
+        CatalogFixture().install_course_run(self.course_run)
+
     def auth(self, enroll=True):
         """Authenticate, enrolling the user in the configured course if requested."""
         CourseFixture(**self.course_info).install()
@@ -62,7 +72,7 @@ class ProgramListingPageTest(ProgramPageBase):
     def test_no_enrollments(self):
         """Verify that no cards appear when the user has no enrollments."""
         program = self.create_program()
-        self.stub_api([program])
+        self.stub_programs_api([program])
         self.auth(enroll=False)
 
         self.listing_page.visit()
@@ -81,7 +91,7 @@ class ProgramListingPageTest(ProgramPageBase):
         )
 
         program = self.create_program(course_id=course_id)
-        self.stub_api([program])
+        self.stub_programs_api([program])
         self.auth()
 
         self.listing_page.visit()
@@ -95,7 +105,7 @@ class ProgramListingPageTest(ProgramPageBase):
         which are included in at least one active program.
         """
         program = self.create_program()
-        self.stub_api([program])
+        self.stub_programs_api([program])
         self.auth()
 
         self.listing_page.visit()
@@ -113,7 +123,7 @@ class ProgramListingPageA11yTest(ProgramPageBase):
         self.listing_page = ProgramListingPage(self.browser)
 
         program = self.create_program()
-        self.stub_api([program])
+        self.stub_programs_api([program])
 
     def test_empty_a11y(self):
         """Test a11y of the page's empty state."""
@@ -143,7 +153,7 @@ class ProgramDetailsPageA11yTest(ProgramPageBase):
         self.details_page = ProgramDetailsPage(self.browser)
 
         program = self.create_program(program_id=self.details_page.program_id)
-        self.stub_api([program], is_list=False)
+        self.stub_programs_api([program], is_list=False)
 
     def test_a11y(self):
         """Test the page's a11y compliance."""
diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py
index 835cf2f45e105a5eb56e63b26b124249c2e748d6..5c96412937cf7fbc953d0a4ad0934e0b29d30be8 100644
--- a/lms/djangoapps/learner_dashboard/tests/test_programs.py
+++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py
@@ -14,6 +14,7 @@ from django.test import override_settings
 from django.utils.text import slugify
 from edx_oauth2_provider.tests.factories import ClientFactory
 import httpretty
+import mock
 from provider.constants import CONFIDENTIAL
 
 from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
@@ -28,6 +29,10 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
 from xmodule.modulestore.tests.factories import CourseFactory
 
 
+UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
+MARKETING_URL = 'https://www.example.com/marketing/path'
+
+
 @httpretty.activate
 @override_settings(MKTG_URLS={'ROOT': 'https://www.example.com'})
 @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@@ -290,6 +295,7 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
 @httpretty.activate
 @override_settings(MKTG_URLS={'ROOT': 'https://www.example.com'})
 @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+@mock.patch(UTILS_MODULE + '.get_run_marketing_url', mock.Mock(return_value=MARKETING_URL))
 class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
     """Unit tests for the program details page."""
     program_id = 123
diff --git a/lms/envs/bok_choy.env.json b/lms/envs/bok_choy.env.json
index 1f04c40e9930b9a21a9f6672b2755d1b19019436..1454089e1b5262281a197d73a35eaa465daf9a85 100644
--- a/lms/envs/bok_choy.env.json
+++ b/lms/envs/bok_choy.env.json
@@ -92,6 +92,9 @@
     },
     "FEEDBACK_SUBMISSION_EMAIL": "",
     "GITHUB_REPO_ROOT": "** OVERRIDDEN **",
+    "JWT_AUTH": {
+        "JWT_SECRET_KEY": "super-secret-key"
+    },
     "LMS_BASE": "localhost:8003",
     "LOCAL_LOGLEVEL": "INFO",
     "LOGGING_ENV": "sandbox",
diff --git a/lms/static/js/learner_dashboard/models/course_card_model.js b/lms/static/js/learner_dashboard/models/course_card_model.js
index 51bc9b5c75c0354d959c8bb3652810d3b6190f47..2a0d6c36b3726c39b3be40eceba464963ace1ef8 100644
--- a/lms/static/js/learner_dashboard/models/course_card_model.js
+++ b/lms/static/js/learner_dashboard/models/course_card_model.js
@@ -72,7 +72,7 @@
                         is_enrolled: runMode.is_enrolled,
                         is_enrollment_open: runMode.is_enrollment_open,
                         key: this.context.key,
-                        marketing_url: runMode.marketing_url || '',
+                        marketing_url: runMode.marketing_url,
                         mode_slug: runMode.mode_slug,
                         run_key: runMode.run_key,
                         start_date: runMode.start_date,
diff --git a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js
index ef1ced7787edd5cce5c2215098cdae7ab2d3a7a9..15791e3653bbcd7ed13532e54aada6d158b0fb6b 100644
--- a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js
+++ b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js
@@ -23,13 +23,13 @@ define([
                         course_image_url: 'http://test.com/image1',
                         course_key: 'course-v1:ANUx+ANU-ASTRO1x+3T2015',
                         course_started: true,
-                        course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info',
+                        course_url: 'https://courses.example.com/courses/course-v1:edX+DemoX+Demo_Course',
                         end_date: 'Jun 13, 2019',
                         enrollment_open_date: 'Mar 03, 2016',
                         is_course_ended: false,
                         is_enrolled: true,
                         is_enrollment_open: true,
-                        marketing_url: 'https://www.edx.org/course/astrophysics-exploring',
+                        marketing_url: 'https://www.example.com/marketing/site',
                         mode_slug: 'verified',
                         run_key: '2T2016',
                         start_date: 'Apr 25, 2016',
@@ -53,7 +53,8 @@ define([
                 expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url);
                 expect(view.$('.course-details .course-title-link').text().trim()).toEqual(context.display_name);
                 expect(view.$('.course-details .course-title-link').attr('href')).toEqual(
-                    context.run_modes[0].course_url);
+                    context.run_modes[0].marketing_url
+                );
                 expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key);
                 expect(view.$('.course-details .course-text .run-period').html())
                     .toEqual(context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date);
@@ -140,7 +141,6 @@ define([
                 setupView(data, false);
                 expect(view.$('.header-img').attr('src')).toEqual(data.run_modes[0].course_image_url);
                 expect(view.$('.course-details .course-title').text().trim()).toEqual(data.display_name);
-                expect(view.$('.course-details .course-title-link').length).toBe(0);
                 expect(view.$('.course-details .course-text .course-key').html()).toEqual(data.key);
                 expect(view.$('.course-details .course-text .run-period').length).toBe(0);
                 expect(view.$('.no-action-message').text().trim()).toBe('Coming Soon');
@@ -156,6 +156,35 @@ define([
                 setupView(data, false);
                 validateCourseInfoDisplay();
             });
+
+            it('should link to the marketing site when a URL is available', function(){
+                $.each([ '.course-image-link', '.course-title-link' ], function( index, selector ) {
+                    expect(view.$(selector).attr('href')).toEqual(context.run_modes[0].marketing_url);
+                });
+            });
+
+            it('should link to the course home when no marketing URL is available', function(){
+                var data = $.extend({}, context);
+
+                data.run_modes[0].marketing_url = null;
+                setupView(data, false);
+
+                $.each([ '.course-image-link', '.course-title-link' ], function( index, selector ) {
+                    expect(view.$(selector).attr('href')).toEqual(context.run_modes[0].course_url);
+                });
+            });
+
+            it('should not link to the marketing site or the course home if neither URL is available', function(){
+                var data = $.extend({}, context);
+
+                data.run_modes[0].marketing_url = null;
+                data.run_modes[0].course_url = null;
+                setupView(data, false);
+
+                $.each([ '.course-image-link', '.course-title-link' ], function( index, selector ) {
+                    expect(view.$(selector).length).toEqual(0);
+                });
+            });
         });
     }
 );
diff --git a/lms/templates/learner_dashboard/course_card.underscore b/lms/templates/learner_dashboard/course_card.underscore
index e50ec8e8c5daf9b4f17ca55665d9a98bf134821c..fe035c459c7a1811e32877b7d4d6276f355799ac 100644
--- a/lms/templates/learner_dashboard/course_card.underscore
+++ b/lms/templates/learner_dashboard/course_card.underscore
@@ -1,33 +1,31 @@
 <div class="section">
     <div class="course-meta-container col-12 md-col-8 sm-col-12">
         <div class="course-image-container">
-            <% if (course_url){ %>
-                <a href="<%- course_url %>" class="course-image-link">
+            <% if ( marketing_url || course_url ) { %>
+                <a href="<%- marketing_url || course_url %>" class="course-image-link">
                     <img
                         class="header-img"
                         src="<%- course_image_url %>"
+                        <% // safe-lint: disable=underscore-not-escaped %>
                         alt="<%= interpolate(gettext('%(courseName)s Home Page.'), {courseName: display_name}, true) %>"/>
                 </a>
             <% } else { %>
-                <img
-                    class="header-img"
-                    src="<%- course_image_url %>"
-                    alt="" />
+                <img class="header-img" src="<%- course_image_url %>" alt=""/>
             <% } %>
 
         </div>
         <div class="course-details">
             <h3 class="course-title">
-                <% if (course_url){ %>
-                    <a href="<%- course_url %>" class="course-title-link">
+                <% if ( marketing_url || course_url ) { %>
+                    <a href="<%- marketing_url || course_url %>" class="course-title-link">
                         <%- display_name %>
                     </a>
-                <% }else{ %>
+                <% } else { %>
                     <%- display_name %>
                 <% } %>
             </h3>
             <div class="course-text">
-                <% if (start_date && end_date){ %>
+                <% if (start_date && end_date) { %>
                     <span class="run-period"><%- start_date %> - <%- end_date %></span>
                     -
                 <% } %>
diff --git a/lms/urls.py b/lms/urls.py
index 3a100daba523f780a4243aa1c07b7c535e6e0dc6..d197b64d7dc9283dabda5dc5a7bdfa45133619cb 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -13,6 +13,7 @@ import auth_exchange.views
 from courseware.views.views import EnrollStaffView
 from config_models.views import ConfigurationModelCurrentAPIView
 from courseware.views.index import CoursewareIndex
+from openedx.core.djangoapps.catalog.models import CatalogIntegration
 from openedx.core.djangoapps.programs.models import ProgramsApiConfig
 from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
 from student.views import LogoutView
@@ -969,6 +970,7 @@ if settings.FEATURES.get("ENABLE_LTI_PROVIDER"):
 urlpatterns += (
     url(r'config/self_paced', ConfigurationModelCurrentAPIView.as_view(model=SelfPacedConfiguration)),
     url(r'config/programs', ConfigurationModelCurrentAPIView.as_view(model=ProgramsApiConfig)),
+    url(r'config/catalog', ConfigurationModelCurrentAPIView.as_view(model=CatalogIntegration)),
 )
 
 urlpatterns = patterns(*urlpatterns)
diff --git a/openedx/core/djangoapps/catalog/tests/factories.py b/openedx/core/djangoapps/catalog/tests/factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..4136f5d3a4e73f14f32a55c55ca1987ba418696e
--- /dev/null
+++ b/openedx/core/djangoapps/catalog/tests/factories.py
@@ -0,0 +1,14 @@
+"""Factories for generating fake catalog data."""
+import factory
+from factory.fuzzy import FuzzyText
+
+
+class CourseRun(factory.Factory):
+    """
+    Factory for stubbing CourseRun resources from the catalog API.
+    """
+    class Meta(object):
+        model = dict
+
+    key = FuzzyText(prefix='org/', suffix='/run')
+    marketing_url = FuzzyText(prefix='https://www.example.com/marketing/')
diff --git a/openedx/core/djangoapps/catalog/tests/test_utils.py b/openedx/core/djangoapps/catalog/tests/test_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..690c238427f36de2042197b9c2d4f7d7137bdb34
--- /dev/null
+++ b/openedx/core/djangoapps/catalog/tests/test_utils.py
@@ -0,0 +1,120 @@
+"""Tests covering utilities for integrating with the catalog service."""
+import ddt
+from django.test import TestCase
+import mock
+from opaque_keys.edx.keys import CourseKey
+
+from openedx.core.djangoapps.catalog import utils
+from openedx.core.djangoapps.catalog.tests import factories, mixins
+from student.tests.factories import UserFactory
+
+
+UTILS_MODULE = 'openedx.core.djangoapps.catalog.utils'
+
+
+@mock.patch(UTILS_MODULE + '.get_edx_api_data')
+class TestGetCourseRun(mixins.CatalogIntegrationMixin, TestCase):
+    """Tests covering retrieval of course runs from the catalog service."""
+    def setUp(self):
+        super(TestGetCourseRun, self).setUp()
+
+        self.user = UserFactory()
+        self.course_key = CourseKey.from_string('foo/bar/baz')
+        self.catalog_integration = self.create_catalog_integration()
+
+    def assert_contract(self, call_args):
+        """Verify that API data retrieval utility is used correctly."""
+        args, kwargs = call_args
+
+        for arg in (self.catalog_integration, self.user, 'course_runs'):
+            self.assertIn(arg, args)
+
+        self.assertEqual(kwargs['resource_id'], unicode(self.course_key))
+        self.assertEqual(kwargs['api']._store['base_url'], self.catalog_integration.internal_api_url)  # pylint: disable=protected-access
+
+        return args, kwargs
+
+    def test_get_course_run(self, mock_get_catalog_data):
+        course_run = factories.CourseRun()
+        mock_get_catalog_data.return_value = course_run
+
+        data = utils.get_course_run(self.course_key, self.user)
+
+        self.assert_contract(mock_get_catalog_data.call_args)
+        self.assertEqual(data, course_run)
+
+    def test_course_run_unavailable(self, mock_get_catalog_data):
+        mock_get_catalog_data.return_value = []
+
+        data = utils.get_course_run(self.course_key, self.user)
+
+        self.assert_contract(mock_get_catalog_data.call_args)
+        self.assertEqual(data, {})
+
+    def test_cache_disabled(self, mock_get_catalog_data):
+        utils.get_course_run(self.course_key, self.user)
+
+        _, kwargs = self.assert_contract(mock_get_catalog_data.call_args)
+
+        self.assertIsNone(kwargs['cache_key'])
+
+    def test_cache_enabled(self, mock_get_catalog_data):
+        catalog_integration = self.create_catalog_integration(cache_ttl=1)
+
+        utils.get_course_run(self.course_key, self.user)
+
+        _, kwargs = mock_get_catalog_data.call_args
+
+        self.assertEqual(kwargs['cache_key'], catalog_integration.CACHE_KEY)
+
+
+@mock.patch(UTILS_MODULE + '.get_course_run')
+@mock.patch(UTILS_MODULE + '.strip_querystring')
+class TestGetRunMarketingUrl(TestCase):
+    """Tests covering retrieval of course run marketing URLs."""
+    def setUp(self):
+        super(TestGetRunMarketingUrl, self).setUp()
+
+        self.course_key = CourseKey.from_string('foo/bar/baz')
+        self.user = UserFactory()
+
+    def test_get_run_marketing_url(self, mock_strip, mock_get_course_run):
+        course_run = factories.CourseRun()
+        mock_get_course_run.return_value = course_run
+        mock_strip.return_value = course_run['marketing_url']
+
+        url = utils.get_run_marketing_url(self.course_key, self.user)
+
+        self.assertTrue(mock_strip.called)
+        self.assertEqual(url, course_run['marketing_url'])
+
+    def test_marketing_url_empty(self, mock_strip, mock_get_course_run):
+        course_run = factories.CourseRun()
+        course_run['marketing_url'] = ''
+        mock_get_course_run.return_value = course_run
+
+        url = utils.get_run_marketing_url(self.course_key, self.user)
+
+        self.assertFalse(mock_strip.called)
+        self.assertEqual(url, None)
+
+    def test_marketing_url_missing(self, mock_strip, mock_get_course_run):
+        mock_get_course_run.return_value = {}
+
+        url = utils.get_run_marketing_url(self.course_key, self.user)
+
+        self.assertFalse(mock_strip.called)
+        self.assertEqual(url, None)
+
+
+@ddt.ddt
+class TestStripQuerystring(TestCase):
+    """Tests covering querystring stripping."""
+    bare_url = 'https://www.example.com/path'
+
+    @ddt.data(
+        bare_url,
+        bare_url + '?foo=bar&baz=qux',
+    )
+    def test_strip_querystring(self, url):
+        self.assertEqual(utils.strip_querystring(url), self.bare_url)
diff --git a/openedx/core/djangoapps/catalog/utils.py b/openedx/core/djangoapps/catalog/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c762f3faec5bc10f26c047496a05273596a6ae9
--- /dev/null
+++ b/openedx/core/djangoapps/catalog/utils.py
@@ -0,0 +1,69 @@
+"""Helper functions for working with the catalog service."""
+from urlparse import urlparse
+
+from django.conf import settings
+from edx_rest_api_client.client import EdxRestApiClient
+
+from openedx.core.djangoapps.catalog.models import CatalogIntegration
+from openedx.core.lib.edx_api_utils import get_edx_api_data
+from openedx.core.lib.token_utils import JwtBuilder
+
+
+def get_course_run(course_key, user):
+    """Get a course run's data from the course catalog service.
+
+    Arguments:
+        course_key (CourseKey): Course key object identifying the run whose data we want.
+        user (User): The user to authenticate as when making requests to the catalog service.
+
+    Returns:
+        dict, empty if no data could be retrieved.
+    """
+    catalog_integration = CatalogIntegration.current()
+
+    scopes = ['email', 'profile']
+    expires_in = settings.OAUTH_ID_TOKEN_EXPIRATION
+    jwt = JwtBuilder(user).build_token(scopes, expires_in)
+    api = EdxRestApiClient(catalog_integration.internal_api_url, jwt=jwt)
+
+    data = get_edx_api_data(
+        catalog_integration,
+        user,
+        'course_runs',
+        resource_id=unicode(course_key),
+        cache_key=catalog_integration.CACHE_KEY if catalog_integration.is_cache_enabled else None,
+        api=api,
+    )
+
+    return data if data else {}
+
+
+def get_run_marketing_url(course_key, user):
+    """Get a course run's marketing URL from the course catalog service.
+
+    Arguments:
+        course_key (CourseKey): Course key object identifying the run whose marketing URL we want.
+        user (User): The user to authenticate as when making requests to the catalog service.
+
+    Returns:
+        string, the marketing URL, or None if no URL is available.
+    """
+    course_run = get_course_run(course_key, user)
+    marketing_url = course_run.get('marketing_url')
+
+    if marketing_url:
+        # This URL may include unwanted UTM parameters in the querystring.
+        # For more, see https://en.wikipedia.org/wiki/UTM_parameters.
+        return strip_querystring(marketing_url)
+    else:
+        return None
+
+
+def strip_querystring(url):
+    """Strip the querystring from the provided URL.
+
+    urlparse's ParseResult is a subclass of namedtuple. _replace is part of namedtuple's
+    public API: https://docs.python.org/2/library/collections.html#collections.somenamedtuple._replace.
+    The name starts with an underscore to prevent conflicts with field names.
+    """
+    return urlparse(url)._replace(query='').geturl()  # pylint: disable=no-member
diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py
index e0f4398db72731dd31649c1ad56a95c84e0929da..c49ff9c03e79b01525ed1ac933f2515cf70f1249 100644
--- a/openedx/core/djangoapps/programs/tests/test_utils.py
+++ b/openedx/core/djangoapps/programs/tests/test_utils.py
@@ -36,7 +36,8 @@ from xmodule.modulestore.tests.factories import CourseFactory
 
 UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
 CERTIFICATES_API_MODULE = 'lms.djangoapps.certificates.api'
-ECOMMERCE_URL_ROOT = 'http://example-ecommerce.com'
+ECOMMERCE_URL_ROOT = 'https://example-ecommerce.com'
+MARKETING_URL = 'https://www.example.com/marketing/path'
 
 
 @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@@ -677,6 +678,7 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
 @ddt.ddt
 @override_settings(ECOMMERCE_PUBLIC_URL_ROOT=ECOMMERCE_URL_ROOT)
 @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+@mock.patch(UTILS_MODULE + '.get_run_marketing_url', mock.Mock(return_value=MARKETING_URL))
 class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
     """Tests of the utility function used to supplement program data."""
     maxDiff = None
@@ -719,7 +721,7 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
                 is_course_ended=self.course.end < timezone.now(),
                 is_enrolled=False,
                 is_enrollment_open=True,
-                marketing_url=None,
+                marketing_url=MARKETING_URL,
                 start_date=strftime_localized(self.course.start, 'SHORT_DATE'),
                 upgrade_url=None,
             ),
diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py
index 2f5006f4c3865145dc43251f1da659b66ef47f40..3f9b8d9a7ab056a7ec00af03354793e0714bdde1 100644
--- a/openedx/core/djangoapps/programs/utils.py
+++ b/openedx/core/djangoapps/programs/utils.py
@@ -13,6 +13,7 @@ import pytz
 from course_modes.models import CourseMode
 from lms.djangoapps.certificates import api as certificate_api
 from lms.djangoapps.commerce.utils import EcommerceService
+from openedx.core.djangoapps.catalog.utils import get_run_marketing_url
 from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 from openedx.core.djangoapps.programs.models import ProgramsApiConfig
 from openedx.core.lib.edx_api_utils import get_edx_api_data
@@ -392,8 +393,7 @@ def supplement_program_data(program_data, user):
                 'is_course_ended': is_course_ended,
                 'is_enrolled': is_enrolled,
                 'is_enrollment_open': is_enrollment_open,
-                # TODO: Not currently available on LMS.
-                'marketing_url': None,
+                'marketing_url': get_run_marketing_url(course_key, user),
                 'start_date': start_date_string,
                 'upgrade_url': upgrade_url,
             })
diff --git a/pavelib/utils/envs.py b/pavelib/utils/envs.py
index 11c27dc1fbf71befdcde0a129a536ee3c472032d..9a16d9867989838dc729a63b6836c3f368658b0f 100644
--- a/pavelib/utils/envs.py
+++ b/pavelib/utils/envs.py
@@ -103,15 +103,20 @@ class Env(object):
             'log': BOK_CHOY_LOG_DIR / "bok_choy_edxnotes.log",
         },
 
+        'ecommerce': {
+            'port': 8043,
+            'log': BOK_CHOY_LOG_DIR / "bok_choy_ecommerce.log",
+        },
+
         'programs': {
             'port': 8090,
             'log': BOK_CHOY_LOG_DIR / "bok_choy_programs.log",
         },
 
-        'ecommerce': {
-            'port': 8043,
-            'log': BOK_CHOY_LOG_DIR / "bok_choy_ecommerce.log",
-        }
+        'catalog': {
+            'port': 8091,
+            'log': BOK_CHOY_LOG_DIR / "bok_choy_catalog.log",
+        },
     }
 
     # Mongo databases that will be dropped before/after the tests run