diff --git a/geomagio/TimeseriesFactory.py b/geomagio/TimeseriesFactory.py
index 22bd77cf1bf19f7b41db854191453d0ce026200e..62c6904b7ac76d3fa67dcb517dc36a8afe91905b 100644
--- a/geomagio/TimeseriesFactory.py
+++ b/geomagio/TimeseriesFactory.py
@@ -1,4 +1,6 @@
 """Abstract Timeseries Factory Interface."""
+import os
+from TimeseriesFactoryException import TimeseriesFactoryException
 
 
 class TimeseriesFactory(object):
@@ -17,13 +19,25 @@ class TimeseriesFactory(object):
     interval : {'daily', 'hourly', 'minute', 'monthly', 'second'}
         data interval, optional.
         default 'minute'.
+    urlTemplate : str
+        A string that contains any of the following replacement patterns:
+        - '%(i)s' : interval abbreviation
+        - '%(interval)s' interval name
+        - '%(julian)s' julian date
+        - '%(obs)s' lowercase observatory code
+        - '%(OBS)s' uppercase observatory code
+        - '%(t)s' type abbreviation
+        - '%(type)s' type name
+        - '%(year)s' year formatted as YYYY
+        - '%(ymd)s' time formatted as YYYYMMDD
     """
     def __init__(self, observatory=None, channels=('H', 'D', 'Z', 'F'),
-            type='variation', interval='minute'):
+            type='variation', interval='minute', urlTemplate=''):
         self.observatory = observatory
         self.channels = channels
         self.type = type
         self.interval = interval
+        self.urlTemplate = urlTemplate
 
     def get_timeseries(self, starttime, endtime, observatory=None,
             channels=None, type=None, interval=None):
@@ -94,3 +108,187 @@ class TimeseriesFactory(object):
             if any errors occur.
         """
         raise NotImplementedError('"put_timeseries" not implemented')
+
+    def _get_file_from_url(self, url):
+        """Get a file for writing.
+
+        Ensures parent directory exists.
+
+        Parameters
+        ----------
+        url : str
+            path to file
+
+        Returns
+        -------
+        str
+            path to file without file:// prefix
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if url does not start with file://
+        """
+        if not url.startswith('file://'):
+            raise TimeseriesFactoryException(
+                    'Only file urls are supported for writing')
+        filename = url.replace('file://', '')
+        parent = os.path.dirname(filename)
+        if not os.path.exists(parent):
+            os.makedirs(parent)
+        return filename
+
+    def _get_url(self, observatory, date, type='variation', interval='minute'):
+        """Get the url for a specified file.
+
+        Replaces patterns (described in class docstring) with values based on
+        parameter values.
+
+        Parameters
+        ----------
+        observatory : str
+            observatory code.
+        date : obspy.core.UTCDateTime
+            day to fetch (only year, month, day are used)
+        type : {'variation', 'quasi-definitive', 'definitive'}
+            data type.
+        interval : {'minute', 'second', 'hourly', 'daily'}
+            data interval.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if type or interval are not supported.
+        """
+        return self.urlTemplate % {
+                'i': self._get_interval_abbreviation(interval),
+                'interval': self._get_interval_name(interval),
+                'julian': date.strftime("%j"),
+                'obs': observatory.lower(),
+                'OBS': observatory.upper(),
+                't': self._get_type_abbreviation(type),
+                'type': self._get_type_name(type),
+                'year': date.strftime("%Y"),
+                'ymd': date.strftime('%Y%m%d')}
+
+    def _get_interval_abbreviation(self, interval):
+        """Get abbreviation for a data interval.
+
+        Used by ``_get_url`` to replace ``%(i)s`` in urlTemplate.
+
+        Parameters
+        ----------
+        interval : {'daily', 'hourly', 'minute', 'monthly', 'second'}
+
+        Returns
+        -------
+        abbreviation for ``interval``.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if ``interval`` is not supported.
+        """
+        interval_abbr = None
+        if interval == 'daily':
+            interval_abbr = 'day'
+        elif interval == 'hourly':
+            interval_abbr = 'hor'
+        elif interval == 'minute':
+            interval_abbr = 'min'
+        elif interval == 'monthly':
+            interval_abbr = 'mon'
+        elif interval == 'second':
+            interval_abbr = 'sec'
+        else:
+            raise TimeseriesFactoryException(
+                    'Unexpected interval "%s"' % interval)
+        return interval_abbr
+
+    def _get_interval_name(self, interval):
+        """Get name for a data interval.
+
+        Used by ``_get_url`` to replace ``%(interval)s`` in urlTemplate.
+
+        Parameters
+        ----------
+        interval : {'minute', 'second'}
+
+        Returns
+        -------
+        name for ``interval``.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if ``interval`` is not supported.
+        """
+        interval_name = None
+        if interval == 'minute':
+            interval_name = 'OneMinute'
+        elif interval == 'second':
+            interval_name = 'OneSecond'
+        else:
+            raise TimeseriesFactoryException(
+                    'Unsupported interval "%s"' % interval)
+        return interval_name
+
+    def _get_type_abbreviation(self, type):
+        """Get abbreviation for a data type.
+
+        Used by ``_get_url`` to replace ``%(t)s`` in urlTemplate.
+
+        Parameters
+        ----------
+        type : {'definitive', 'provisional', 'quasi-definitive', 'variation'}
+
+        Returns
+        -------
+        name for ``type``.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if ``type`` is not supported.
+        """
+        type_abbr = None
+        if type == 'definitive':
+            type_abbr = 'd'
+        elif type == 'provisional':
+            type_abbr = 'p'
+        elif type == 'quasi-definitive':
+            type_abbr = 'q'
+        elif type == 'variation':
+            type_abbr = 'v'
+        else:
+            raise TimeseriesFactoryException(
+                    'Unexpected type "%s"' % type)
+        return type_abbr
+
+    def _get_type_name(self, type):
+        """Get name for a data type.
+
+        Used by ``_get_url`` to replace ``%(type)s`` in urlTemplate.
+
+        Parameters
+        ----------
+        type : {'variation', 'quasi-definitive'}
+
+        Returns
+        -------
+        name for ``type``.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if ``type`` is not supported.
+        """
+        type_name = None
+        if type == 'variation':
+            type_name = ''
+        elif type == 'quasi-definitive':
+            type_name = 'QuasiDefinitive'
+        else:
+            raise TimeseriesFactoryException(
+                    'Unsupported type "%s"' % type)
+        return type_name