diff --git a/cartoframes/context.py b/cartoframes/context.py
index e5885a97..c88bee6f 100644
--- a/cartoframes/context.py
+++ b/cartoframes/context.py
@@ -18,7 +18,7 @@ from carto.sql import SQLClient, BatchSQLClient
 from carto.exceptions import CartoException
 
 from .credentials import Credentials
-from .utils import dict_items, normalize_colnames, norm_colname
+from .utils import dict_items, normalize_colnames, norm_colname, join_url
 from .layer import BaseMap
 from .maps import non_basemap_layers, get_map_name, get_map_template
 
@@ -217,7 +217,7 @@ class CartoContext(object):
                     'minutes.\n'
                     '\033[1mNote:\033[0m `CartoContext.map` will not work on '
                     'this table until its geometries are created.'.format(
-                               table_url='/'.join((self.creds.base_url(),
+                               table_url=join_url((self.creds.base_url(),
                                                    'dataset',
                                                    final_table_name, )),
                                job_id=status.get('job_id'),
@@ -227,7 +227,7 @@ class CartoContext(object):
             self.sql_client.send(query)
 
         tqdm.write('Table successfully written to CARTO: {table_url}'.format(
-                       table_url='/'.join((self.creds.base_url(),
+                       table_url=join_url((self.creds.base_url(),
                                            'dataset',
                                            final_table_name, ))))
 
@@ -679,7 +679,7 @@ class CartoContext(object):
         elif not base_layers:
             # default basemap is dark with labels in back
             # labels will be changed if all geoms are non-point
-            layers.insert(0, BaseMap(source='dark', labels='back'))
+            layers.insert(0, BaseMap())
             geoms = set()
 
         # Setup layers
@@ -734,7 +734,7 @@ class CartoContext(object):
             options.update(self._get_bounds(nb_layers))
 
         map_name = self._send_map_template(layers, has_zoom=has_zoom)
-        api_url = '/'.join((self.creds.base_url(), 'api/v1/map', ))
+        api_url = join_url((self.creds.base_url(), 'api/v1/map', ))
 
         static_url = ('{api_url}/static/named/{map_name}'
                       '/{width}/{height}.png?{params}').format(
diff --git a/cartoframes/layer.py b/cartoframes/layer.py
index 821390a1..6ae19117 100644
--- a/cartoframes/layer.py
+++ b/cartoframes/layer.py
@@ -6,7 +6,7 @@ for example usage.
 import pandas as pd
 import webcolors
 
-from cartoframes.utils import cssify
+from cartoframes.utils import cssify, join_url
 from cartoframes.styling import BinMethod, mint, antique, get_scheme_cartocss
 
 # colors map data layers without color specified
@@ -53,21 +53,30 @@ class BaseMap(AbstractLayer):
     """
     is_basemap = True
 
-    def __init__(self, source='dark', labels='back', only_labels=False):
+    def __init__(self, source='voyager', labels='back', only_labels=False):
         if labels not in ('front', 'back', None):
             raise ValueError("labels must be None, 'front', or 'back'")
 
         self.source = source
         self.labels = labels
+        stem = 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/'
+        if source == 'voyager':
+            stem += 'rastertiles'
 
         if self.is_basic():
             if only_labels:
                 style = source + '_only_labels'
             else:
-                style = source + ('_all' if labels == 'back' else '_nolabels')
-
-            self.url = ('https://cartodb-basemaps-{{s}}.global.ssl.fastly.net/'
-                        '{style}/{{z}}/{{x}}/{{y}}.png').format(style=style)
+                if source in ('dark', 'light', ):
+                    label_type = '_all'
+                else:
+                    label_type = '_labels_under'
+                style = source + (label_type if labels == 'back'
+                                  else '_nolabels')
+            self.url = join_url((stem,
+                                 '{style}/{{z}}/{{x}}/{{y}}.png'.format(
+                                     style=style)
+                                 ))
         elif self.source.startswith('http'):
             # TODO: Remove this once baselayer urls can be passed in named
             # map config
@@ -75,16 +84,17 @@ class BaseMap(AbstractLayer):
                              'moment')
             # self.url = source
         else:
-            raise ValueError("`source` must be one of 'dark' or 'light'")
+            raise ValueError("`source` must be one of 'dark', 'light', or "
+                             "'voyager'")
 
     def is_basic(self):
         """Does BaseMap pull from CARTO default basemaps?
 
         Returns:
-            bool: `True` if using a CARTO basemap (Dark Matter or Positron),
-            `False` otherwise.
+            bool: `True` if using a CARTO basemap (Dark Matter, Positron or
+            Voyager), `False` otherwise.
         """
-        return self.source in ('dark', 'light')
+        return self.source in ('dark', 'light', 'voyager', )
 
 
 class QueryLayer(AbstractLayer):
diff --git a/cartoframes/maps.py b/cartoframes/maps.py
index b5d33ee0..701cdc1f 100644
--- a/cartoframes/maps.py
+++ b/cartoframes/maps.py
@@ -19,6 +19,7 @@ def get_map_name(layers, has_zoom):
     num_layers = len(non_basemap_layers(layers))
     has_labels = len(layers) > 1 and layers[-1].is_basemap
     has_time = has_time_layer(layers)
+    basemap_id = dict(light=0, dark=1, voyager=2)[layers[0].source]
 
     return ('cartoframes_ver{version}'
             '_layers{layers}'
@@ -31,7 +32,7 @@ def get_map_name(layers, has_zoom):
                 has_time=('1' if has_time else '0'),
                 # TODO: Remove this once baselayer urls can be passed in named
                 #       map config
-                baseid=('1' if layers[0].source == 'dark' else '0'),
+                baseid=basemap_id,
                 has_labels=('1' if has_labels else '0'),
                 has_zoom=('1' if has_zoom else '0')
             )
diff --git a/cartoframes/utils.py b/cartoframes/utils.py
index ae5750e4..5f8b95f3 100644
--- a/cartoframes/utils.py
+++ b/cartoframes/utils.py
@@ -73,3 +73,8 @@ def norm_colname(colname):
     if final_name[0].isdigit():
         return '_' + final_name
     return final_name
+
+
+def join_url(parts):
+    """join parts of URL into complete url"""
+    return '/'.join(s.strip('/') for s in parts)

include voyager as a basemap option
e.g.,
```
https://cartodb-basemaps-a.global.ssl.fastly.net/rastertiles/voyager_nolabels/{z}/{x}/{y}.png
```
diff --git a/test/test_context.py b/test/test_context.py
index b8abcd28..a5654d01 100644
--- a/test/test_context.py
+++ b/test/test_context.py
@@ -16,6 +16,7 @@ from carto.sql import SQLClient
 import pandas as pd
 
 WILL_SKIP = False
+warnings.filterwarnings("ignore")
 
 
 class TestCartoContext(unittest.TestCase):
@@ -533,21 +534,21 @@ class TestCartoContext(unittest.TestCase):
         # baseid1 = dark, labels1 = labels on top in named map name
         labels_polygon = cc.map(layers=Layer(self.test_read_table))
         self.assertRegexpMatches(labels_polygon.__html__(),
-                                 '.*baseid1_labels1.*',
+                                 '.*baseid2_labels1.*',
                                  msg='labels should be on top since only a '
                                      'polygon layer is present')
 
-        # baseid1 = dark, labels0 = labels on bottom
+        # baseid2 = voyager, labels0 = labels on bottom
         labels_point = cc.map(layers=Layer(self.test_point_table))
         self.assertRegexpMatches(labels_point.__html__(),
-                                 '.*baseid1_labels0.*',
+                                 '.*baseid2_labels0.*',
                                  msg='labels should be on bottom because a '
                                      'point layer is present')
 
         labels_multi = cc.map(layers=[Layer(self.test_point_table),
                                       Layer(self.test_read_table)])
         self.assertRegexpMatches(labels_multi.__html__(),
-                                 '.*baseid1_labels0.*',
+                                 '.*baseid2_labels0.*',
                                  msg='labels should be on bottom because a '
                                      'point layer is present')
         # create a layer with points and polys, but with more polys
@@ -566,7 +567,7 @@ class TestCartoContext(unittest.TestCase):
                    points=self.test_point_table))
         multi_geom = cc.map(layers=multi_geom_layer)
         self.assertRegexpMatches(multi_geom.__html__(),
-                                 '.*baseid1_labels1.*',
+                                 '.*baseid2_labels1.*',
                                  msg='layer has more polys than points, so it '
                                      'should default to polys labels (on top)')
 
diff --git a/test/test_layer.py b/test/test_layer.py
index 428c88a5..e13f140e 100644
--- a/test/test_layer.py
+++ b/test/test_layer.py
@@ -15,18 +15,23 @@ class TestBaseMap(unittest.TestCase):
         # basemaps with baked-in labels
         self.dark_map_all = BaseMap(source='dark')
         self.light_map_all = BaseMap(source='light')
+        self.voyager_labels_under = BaseMap(source='voyager')
 
         # basemaps with no labels
         self.dark_map_no_labels = BaseMap(source='dark',
                                           labels=None)
         self.light_map_no_labels = BaseMap(source='light',
                                            labels=None)
+        self.voyager_map_no_labels = BaseMap(source='voyager',
+                                             labels=None)
 
         # labels with no basemaps
         self.dark_only_labels = BaseMap(source='dark',
                                         only_labels=True)
         self.light_only_labels = BaseMap(source='light',
                                          only_labels=True)
+        self.voyager_only_labels = BaseMap(source='voyager',
+                                           only_labels=True)
 
     def test_basemap_invalid(self):
         """layer.Basemap exceptions on invalid source"""
@@ -53,23 +58,34 @@ class TestBaseMap(unittest.TestCase):
         self.assertEqual(self.light_map_all.url,
                          'https://cartodb-basemaps-{s}.global.ssl.fastly.net/'
                          'light_all/{z}/{x}/{y}.png')
+        self.assertEqual(self.voyager_labels_under.url,
+                         'https://cartodb-basemaps-{s}.global.ssl.fastly.net/'
+                         'rastertiles/voyager_labels_under/{z}/{x}/{y}.png')
         self.assertEqual(self.dark_map_no_labels.url,
                          'https://cartodb-basemaps-{s}.global.ssl.fastly.net/'
                          'dark_nolabels/{z}/{x}/{y}.png')
         self.assertEqual(self.light_map_no_labels.url,
                          'https://cartodb-basemaps-{s}.global.ssl.fastly.net/'
                          'light_nolabels/{z}/{x}/{y}.png')
+        self.assertEqual(self.voyager_map_no_labels.url,
+                         'https://cartodb-basemaps-{s}.global.ssl.fastly.net/'
+                         'rastertiles/voyager_nolabels/{z}/{x}/{y}.png')
         self.assertEqual(self.light_only_labels.url,
                          'https://cartodb-basemaps-{s}.global.ssl.fastly.net/'
                          'light_only_labels/{z}/{x}/{y}.png')
         self.assertEqual(self.dark_only_labels.url,
                          'https://cartodb-basemaps-{s}.global.ssl.fastly.net/'
                          'dark_only_labels/{z}/{x}/{y}.png')
+        self.assertEqual(self.voyager_only_labels.url,
+                         'https://cartodb-basemaps-{s}.global.ssl.fastly.net/'
+                         'rastertiles/voyager_only_labels/{z}/{x}/{y}.png')
 
         # ensure self.is_basic() works as intended
         self.assertTrue(self.light_map_all.is_basic(),
                         msg='is a basic carto basemap')
         self.assertTrue(self.dark_map_all.is_basic())
+        self.assertTrue(self.voyager_labels_under.is_basic(),
+                        msg='is a basic carto basemap')
 
 
 class TestQueryLayer(unittest.TestCase):

pip install -e .
parse_log_pytest
requirements.txt
nose nose-cov pytest
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
appdirs==1.4.4
attrs==22.2.0
backcall==0.2.0
carto==1.11.3
-e git+https://github.com/CartoDB/cartoframes.git@a694cfa6e9f7ff39954ef5045649eb2518632338#egg=cartoframes
certifi==2021.5.30
charset-normalizer==2.0.12
cov-core==1.15.0
coverage==6.2
decorator==5.1.1
future==1.0.0
idna==3.10
importlib-metadata==4.8.3
importlib-resources==5.4.0
iniconfig==1.1.1
ipython==7.16.3
ipython-genutils==0.2.0
jedi==0.17.2
nose==1.3.7
nose-cov==1.6
numpy==1.19.5
packaging==21.3
pandas==1.1.5
parso==0.7.1
pexpect==4.9.0
pickleshare==0.7.5
pluggy==1.0.0
prompt-toolkit==3.0.36
ptyprocess==0.7.0
py==1.11.0
Pygments==2.14.0
pyparsing==3.1.4
pyrestcli==0.6.11
pytest==7.0.1
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.27.1
Shapely==1.8.5.post1
six==1.17.0
tomli==1.2.3
tqdm==4.64.1
traitlets==4.3.3
typing_extensions==4.1.1
urllib3==1.26.20
wcwidth==0.2.13
webcolors==1.7
zipp==3.6.0

name: cartoframes
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - appdirs==1.4.4
      - attrs==22.2.0
      - backcall==0.2.0
      - carto==1.11.3
      - charset-normalizer==2.0.12
      - cov-core==1.15.0
      - coverage==6.2
      - decorator==5.1.1
      - future==1.0.0
      - idna==3.10
      - importlib-metadata==4.8.3
      - importlib-resources==5.4.0
      - iniconfig==1.1.1
      - ipython==7.16.3
      - ipython-genutils==0.2.0
      - jedi==0.17.2
      - nose==1.3.7
      - nose-cov==1.6
      - numpy==1.19.5
      - packaging==21.3
      - pandas==1.1.5
      - parso==0.7.1
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - pluggy==1.0.0
      - prompt-toolkit==3.0.36
      - ptyprocess==0.7.0
      - py==1.11.0
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pyrestcli==0.6.11
      - pytest==7.0.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - requests==2.27.1
      - shapely==1.8.5.post1
      - six==1.17.0
      - tomli==1.2.3
      - tqdm==4.64.1
      - traitlets==4.3.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - wcwidth==0.2.13
      - webcolors==1.7
      - zipp==3.6.0
prefix: /opt/conda/envs/cartoframes

test/test_layer.py::TestBaseMap::test_basemap_invalid test/test_layer.py::TestBaseMap::test_basemap_source
test/test_context.py::TestCartoContext::test_add_encoded_geom test/test_context.py::TestCartoContext::test_cartocontext test/test_context.py::TestCartoContext::test_cartocontext_check_query test/test_context.py::TestCartoContext::test_cartocontext_credentials test/test_context.py::TestCartoContext::test_cartocontext_delete test/test_context.py::TestCartoContext::test_cartocontext_handle_import test/test_context.py::TestCartoContext::test_cartocontext_isorguser test/test_context.py::TestCartoContext::test_cartocontext_map test/test_context.py::TestCartoContext::test_cartocontext_map_geom_type test/test_context.py::TestCartoContext::test_cartocontext_mixed_case test/test_context.py::TestCartoContext::test_cartocontext_read test/test_context.py::TestCartoContext::test_cartocontext_table_exists test/test_context.py::TestCartoContext::test_cartocontext_write test/test_context.py::TestCartoContext::test_cartoframes_query test/test_context.py::TestCartoContext::test_cartoframes_sync test/test_context.py::TestCartoContext::test_data_obs_functions test/test_context.py::TestCartoContext::test_debug_print test/test_context.py::TestCartoContext::test_get_bounds test/test_context.py::TestBatchJobStatus::test_batchjobstatus test/test_context.py::TestBatchJobStatus::test_batchjobstatus_methods test/test_context.py::TestBatchJobStatus::test_batchjobstatus_repr
test/test_context.py::TestCartoContext::test_cartocontext_send_dataframe test/test_context.py::TestCartoContext::test_decode_geom test/test_context.py::TestCartoContext::test_df2pg_schema test/test_context.py::TestCartoContext::test_dtypes2pg test/test_context.py::TestCartoContext::test_encode_geom test/test_context.py::TestCartoContext::test_pg2dtypes test/test_layer.py::TestAbstractLayer::test_class test/test_layer.py::TestQueryLayer::test_querylayer_colors test/test_layer.py::TestQueryLayer::test_querylayer_size_and_time test/test_layer.py::TestQueryLayer::test_querylayer_size_column_key test/test_layer.py::TestQueryLayer::test_querylayer_size_default test/test_layer.py::TestQueryLayer::test_querylayer_size_defaults test/test_layer.py::TestQueryLayer::test_querylayer_time_default test/test_layer.py::TestQueryLayer::test_querylayer_time_errors
cartoframes/context.py cartoframes/maps.py cartoframes/layer.py cartoframes/utils.py
cartoframes/context.py cartoframes/maps.py cartoframes/layer.py cartoframes/utils.py