diff --git a/geomagio/Controller.py b/geomagio/Controller.py
index d05f45ae4e8d4848da8e062adddb8540f839e3ef..4185cdea0ea7531721b0d3d7b2825809273c7333 100644
--- a/geomagio/Controller.py
+++ b/geomagio/Controller.py
@@ -513,7 +513,7 @@ def get_input_factory(args):
             host=args.input_host,
             port=args.input_port,
             locationCode=args.locationcode,
-            **input_factory_args
+            **input_factory_args,
         )
     elif input_type == "miniseed":
         input_factory = edge.MiniSeedFactory(
@@ -521,7 +521,7 @@ def get_input_factory(args):
             port=args.input_port,
             locationCode=args.locationcode,
             convert_channels=args.convert_voltbin,
-            **input_factory_args
+            **input_factory_args,
         )
     elif input_type == "goes":
         # TODO: deal with other goes arguments
@@ -531,7 +531,15 @@ def get_input_factory(args):
             password=args.input_goes_password,
             server=args.input_goes_server,
             user=args.input_goes_user,
-            **input_factory_args
+            **input_factory_args,
+        )
+    elif input_type == "iris":
+        input_factory = edge.IRISFactory(
+            base_url=args.iris_url,
+            network=args.iris_network,
+            locationCode=args.locationcode,
+            convert_channels=args.convert_voltbin,
+            **input_factory_args,
         )
     else:
         # stream compatible factories
@@ -600,7 +608,7 @@ def get_output_factory(args):
             locationCode=locationcode,
             tag=args.output_edge_tag,
             forceout=args.output_edge_forceout,
-            **output_factory_args
+            **output_factory_args,
         )
     elif output_type == "miniseed":
         # TODO: deal with other miniseed arguments
@@ -610,7 +618,7 @@ def get_output_factory(args):
             port=args.output_read_port,
             write_port=args.output_port,
             locationCode=locationcode,
-            **output_factory_args
+            **output_factory_args,
         )
     elif output_type == "plot":
         output_factory = PlotTimeseriesFactory()
@@ -763,7 +771,16 @@ def parse_args(args):
     input_type_group = input_group.add_mutually_exclusive_group(required=True)
     input_type_group.add_argument(
         "--input",
-        choices=("edge", "goes", "iaga2002", "imfv122", "imfv283", "miniseed", "pcdcp"),
+        choices=(
+            "edge",
+            "goes",
+            "iaga2002",
+            "imfv122",
+            "imfv283",
+            "iris",
+            "miniseed",
+            "pcdcp",
+        ),
         default="edge",
         help='Input format (Default "edge")',
     )
@@ -828,6 +845,16 @@ def parse_args(args):
         help='Data interval, default "minute"',
         metavar="INTERVAL",
     )
+    input_group.add_argument(
+        "--iris-network",
+        default="NT",
+        help="data network",
+    )
+    input_group.add_argument(
+        "--iris-url",
+        default="http://service.iris.edu/irisws",
+        help="IRIS web service url",
+    )
     input_group.add_argument(
         "--locationcode",
         help="""
diff --git a/geomagio/api/ws/Observatory.py b/geomagio/api/ws/Observatory.py
index fe857c9acfed34b9fbe96da7b2ad1b85fc9b3589..ecb38c91037b81cb4d822137316c786536b09261 100644
--- a/geomagio/api/ws/Observatory.py
+++ b/geomagio/api/ws/Observatory.py
@@ -424,4 +424,90 @@ OBSERVATORIES = [
     ),
 ]
 
+ASL_OBSERVATORIES = [
+    Observatory(
+        id="ANMO",
+        elevation=1820,
+        latitude=34.946,
+        longitude=-106.457,
+        name="Albuquerque",
+        agency="USGS",
+        declination_base=0,
+    ),
+    Observatory(
+        id="CASY",
+        elevation=10,
+        latitude=-66.279,
+        longitude=110.535,
+        name="Casey",
+        agency="USGS",
+        declination_base=0,
+    ),
+    Observatory(
+        id="COLA",
+        elevation=200,
+        latitude=64.874,
+        longitude=-147.862,
+        name="College Outpost",
+        agency="USGS",
+        declination_base=0,
+    ),
+    Observatory(
+        id="COR",
+        elevation=110,
+        latitude=44.586,
+        longitude=-123.305,
+        name="Corvallis",
+        agency="USGS",
+        declination_base=0,
+    ),
+    Observatory(
+        id="QSPA",
+        elevation=2850,
+        latitude=-89.929,
+        longitude=144.438,
+        name="South Pole Remote Earth Science Observatory",
+        agency="USGS",
+        declination_base=0,
+    ),
+    Observatory(
+        id="RSSD",
+        elevation=2090,
+        latitude=44.121,
+        longitude=-104.036,
+        name="Black Hills",
+        agency="USGS",
+        declination_base=0,
+    ),
+    Observatory(
+        id="SBA",
+        elevation=50,
+        latitude=-77.849,
+        longitude=166.757,
+        name="Scott Base",
+        agency="USGS",
+        declination_base=0,
+    ),
+    Observatory(
+        id="SFJD",
+        elevation=330,
+        latitude=66.996,
+        longitude=-50.621,
+        name="Sondre Stromfjord",
+        agency="USGS",
+        declination_base=0,
+    ),
+    Observatory(
+        id="SPA",
+        elevation=2927,
+        latitude=-90.0,
+        longitude=0.0,
+        name="South Pole",
+        agency="USGS",
+        declination_base=0,
+    ),
+]
+
+ASL_OBSERVATORY_INDEX = {o.id: o for o in ASL_OBSERVATORIES}
 OBSERVATORY_INDEX = {o.id: o for o in OBSERVATORIES}
+OBSERVATORY_INDEX.update(ASL_OBSERVATORY_INDEX)
diff --git a/geomagio/api/ws/data.py b/geomagio/api/ws/data.py
index db204eee3f5243986e7e2dce7eb8ed271005b80d..961c6fae990057502ee332a65dd1595f81a8713d 100644
--- a/geomagio/api/ws/data.py
+++ b/geomagio/api/ws/data.py
@@ -6,9 +6,10 @@ from obspy import UTCDateTime, Stream
 from starlette.responses import Response
 
 from ... import DerivedTimeseriesFactory, TimeseriesFactory, TimeseriesUtility
-from ...edge import EdgeFactory, MiniSeedFactory
+from ...edge import EdgeFactory, IRISFactory, MiniSeedFactory
 from ...iaga2002 import IAGA2002Writer
 from ...imfjson import IMFJSONWriter
+from .Observatory import ASL_OBSERVATORY_INDEX
 from .DataApiQuery import (
     DEFAULT_ELEMENTS,
     DataApiQuery,
@@ -30,7 +31,9 @@ def get_data_factory(
     """
     host = os.getenv("DATA_HOST", "cwbpub.cr.usgs.gov")
     sampling_period = query.sampling_period
-    if sampling_period in [
+    if query.id in ASL_OBSERVATORY_INDEX:
+        factory = IRISFactory(network="IU", locationCode="40")
+    elif sampling_period in [
         SamplingPeriod.TEN_HERTZ,
         SamplingPeriod.HOUR,
         SamplingPeriod.DAY,
diff --git a/geomagio/api/ws/observatories.py b/geomagio/api/ws/observatories.py
index 1a8c797c258952ba8793f12a903ffb70b93b6abf..4297fe9b825d82bdd08819ef4057b75eba2f936c 100644
--- a/geomagio/api/ws/observatories.py
+++ b/geomagio/api/ws/observatories.py
@@ -2,7 +2,7 @@ from typing import Dict
 
 from fastapi import APIRouter, Response
 
-from .Observatory import OBSERVATORIES, OBSERVATORY_INDEX
+from .Observatory import OBSERVATORY_INDEX
 
 
 router = APIRouter()
@@ -15,7 +15,9 @@ router = APIRouter()
 def get_observatories() -> Dict:
     return {
         "type": "FeatureCollection",
-        "features": [o.geojson() for o in OBSERVATORIES],
+        "features": [
+            OBSERVATORY_INDEX[id].geojson() for id in OBSERVATORY_INDEX.keys()
+        ],
     }
 
 
diff --git a/geomagio/edge/IRISFactory.py b/geomagio/edge/IRISFactory.py
new file mode 100644
index 0000000000000000000000000000000000000000..950fb61f58db636edeb33b39c13a6488182bcd39
--- /dev/null
+++ b/geomagio/edge/IRISFactory.py
@@ -0,0 +1,154 @@
+from typing import List, Optional
+
+from obspy import Trace, UTCDateTime
+from obspy.clients.iris.client import Client
+
+from ..geomag_types import DataInterval, DataType
+from ..ObservatoryMetadata import ObservatoryMetadata
+from .. import TimeseriesUtility
+from .IRISSNCL import IRISSNCL
+from .MiniSeedFactory import MiniSeedFactory
+
+
+class IRISFactory(MiniSeedFactory):
+    """MiniSeedFactory for IRIS related data.
+
+    Parameters
+    ----------
+    base_url: str
+        iris web service url
+    host: str
+        a string representing the IP number of the host to connect to
+    write_port: int
+        the port number the miniseed client writes data
+    network: str
+        data network
+    observatory: str
+        observatory id
+    channels: array
+        list of channels
+    type: {"adjusted", "definitive", "quasi-definitive", "variation"}
+        data type
+    interval: {"tenhertz", "second", "minute", "hour", "day"}
+        data interval
+    observatoryMetadata: ObservatoryMetadata
+        observatory metadata to replace the default ObservatoryMetadata
+    locationCode: str
+        the location code for the given edge server
+    convert_channels: array
+        list of channels to convert from volt/bin to nT
+
+    See Also
+    --------
+    MiniSeedFactory
+
+    Notes
+    -----
+    This factory is designed to receive data from IRIS and write to edge.
+    convert_channels should only be used with tenhertz NT data (U, V, W).
+    Non-NT data conversions are automatically handled by the IRIS web service.
+
+
+    """
+
+    def __init__(
+        self,
+        base_url: str = "http://service.iris.edu/irisws",
+        host: str = "cwbpub.cr.usgs.gov",
+        write_port: int = 7974,
+        network: str = "NT",
+        observatory: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        type: Optional[DataType] = None,
+        interval: Optional[DataInterval] = None,
+        observatoryMetadata: Optional[ObservatoryMetadata] = None,
+        locationCode: Optional[str] = None,
+        convert_channels: Optional[List[str]] = None,
+    ):
+        super().__init__(
+            host=host,
+            write_port=write_port,
+            observatory=observatory,
+            channels=channels,
+            type=type,
+            interval=interval,
+            observatoryMetadata=observatoryMetadata or ObservatoryMetadata(),
+            locationCode=locationCode,
+            convert_channels=convert_channels,
+        )
+        self.base_url = base_url
+        self.network = network
+        self.client = Client(base_url=base_url)
+
+    def _get_timeseries(
+        self,
+        starttime: UTCDateTime,
+        endtime: UTCDateTime,
+        observatory: str,
+        channel: str,
+        type: DataType,
+        interval: DataInterval,
+        add_empty_channels: bool = True,
+    ) -> Trace:
+        """get timeseries data for a single channel
+
+        Parameters
+        ----------
+        starttime: UTCDateTime
+            the starttime of requested data
+        endtime: UTCDateTime
+            the endtime of requested data
+        observatory: str
+            observatory id
+        channel: str
+            channel name
+        type: {"adjusted", "definitive", "quasi-definitive", "variation"}
+            data type
+        interval: {"tenhertz", "second", "minute", "hour", "day"}
+            data interval
+        add_empty_channels: bool
+            if True, returns channels without data as empty traces
+
+        Returns
+        -------
+        data: Trace
+            timeseries trace of the requested channel data
+        """
+        sncl = IRISSNCL.get_sncl(
+            data_type=type,
+            element=channel,
+            interval=interval,
+            station=observatory,
+            network=self.network,
+            location=self.locationCode,
+        )
+        filter = ["correct"]
+        if sncl.channel[1] in ["E", "Y"]:
+            filter = []
+        data = self.client.timeseries(
+            network=sncl.network,
+            station=sncl.station,
+            location=sncl.location,
+            channel=sncl.channel,
+            starttime=starttime,
+            endtime=endtime,
+            filter=filter,
+        )
+        data.merge()
+        if data.count() == 0 and add_empty_channels:
+            data += self._get_empty_trace(
+                starttime=starttime,
+                endtime=endtime,
+                observatory=observatory,
+                channel=channel,
+                data_type=type,
+                interval=interval,
+                network=sncl.network,
+                location=sncl.location,
+            )
+        if data.count() != 0:
+            TimeseriesUtility.pad_and_trim_trace(
+                trace=data[0], starttime=starttime, endtime=endtime
+            )
+        self._set_metadata(data, observatory, channel, type, interval)
+        return data
diff --git a/geomagio/edge/IRISSNCL.py b/geomagio/edge/IRISSNCL.py
new file mode 100644
index 0000000000000000000000000000000000000000..0fdcc417047fe8906bd1f01a8e3789fccdf60085
--- /dev/null
+++ b/geomagio/edge/IRISSNCL.py
@@ -0,0 +1,83 @@
+from typing import Optional
+
+from ..geomag_types import DataInterval, DataType
+from .SNCL import SNCL, get_channel, get_location, _get_channel_start
+
+
+class IRISSNCL(SNCL):
+    @classmethod
+    def get_sncl(
+        cls,
+        data_type: DataType,
+        element: str,
+        interval: DataInterval,
+        station: str,
+        network: str = "NT",
+        location: Optional[str] = None,
+    ) -> "IRISSNCL":
+        if (
+            network == "IU"
+            and location == "40"
+            and data_type not in ["40", "variation"]  # ASL only supports variation data
+        ):
+            raise ValueError(f"Unsupported data type: {data_type}")
+        location = location or get_location(element=element, data_type=data_type)
+        return IRISSNCL(
+            station=station,
+            network=network,
+            channel=get_iris_channel(
+                element=element,
+                data_type=data_type,
+                interval=interval,
+                network=network,
+                location=location,
+            ),
+            location=location,
+        )
+
+    @property
+    def data_type(self) -> str:
+        if self.location == "40" and self.network == "IU":
+            return "variation"
+        return super().data_type
+
+    @property
+    def element(self) -> str:
+        if self.location == "40" and self.network == "IU":
+            return _get_iris_element(channel=self.channel)
+        return super().element
+
+
+def _get_iris_element(channel: str) -> str:
+    channel_end = channel[1:]
+    if channel_end == "F1":
+        return "U"
+    elif channel_end == "F2":
+        return "V"
+    elif channel_end == "FZ":
+        return "W"
+    raise ValueError(f"Unsupported channel: {channel}")
+
+
+def get_iris_channel(
+    element: str,
+    data_type: DataType,
+    interval: DataInterval,
+    network: Optional[str] = None,
+    location: Optional[str] = None,
+) -> str:
+    if location == "40" and network == "IU":
+        return _get_channel_start(interval=interval) + _get_channel_end(element=element)
+    return get_channel(element=element, interval=interval, data_type=data_type)
+
+
+def _get_channel_end(element: str) -> str:
+    if element in ["H", "U"]:
+        return "F1"
+    elif element in ["E", "V"]:
+        return "F2"
+    elif element in ["Z", "W"]:
+        return "FZ"
+    elif element[0:2] == "LF":  # predefined element
+        return element[1:]
+    raise ValueError(f"Unsupported element: {element}")
diff --git a/geomagio/edge/__init__.py b/geomagio/edge/__init__.py
index 45b3d49c3ac29b3dd213d0f83cf1017ea5d95549..ebb15ef67872a2a22773695a22f15c9e9f94151c 100644
--- a/geomagio/edge/__init__.py
+++ b/geomagio/edge/__init__.py
@@ -3,19 +3,23 @@
 from __future__ import absolute_import
 
 from .EdgeFactory import EdgeFactory
+from .IRISFactory import IRISFactory
 from .LocationCode import LocationCode
 from .MiniSeedFactory import MiniSeedFactory
 from .MiniSeedInputClient import MiniSeedInputClient
 from .RawInputClient import RawInputClient
+from .IRISSNCL import IRISSNCL
 from .SNCL import SNCL
 from .LegacySNCL import LegacySNCL
 
 __all__ = [
     "EdgeFactory",
+    "IRISFactory",
     "LocationCode",
     "MiniSeedFactory",
     "MiniSeedInputClient",
     "RawInputClient",
-    "LegacySNCL",
+    "IRISSNCL",
     "SNCL",
+    "LegacySNCL",
 ]
diff --git a/test/edge_test/IRISSNCL_test.py b/test/edge_test/IRISSNCL_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..b67cc6400b5954c1f4020e791fb55775913520a1
--- /dev/null
+++ b/test/edge_test/IRISSNCL_test.py
@@ -0,0 +1,165 @@
+import pytest
+
+from geomagio.edge import IRISSNCL
+from geomagio.edge.IRISSNCL import get_iris_channel, get_location
+
+
+def test_data_type():
+    """edge_test.IRISSNCL_test.test_data_type()"""
+    assert (
+        IRISSNCL(station="ANMO", network="IU", channel="LF1", location="40").data_type
+        == "variation"
+    )
+
+
+def test_element():
+    """edge_test.IRISSNCL_test.test_element()"""
+    assert (
+        IRISSNCL(station="ANMO", network="IU", channel="LF1", location="40").element
+        == "U"
+    )
+    assert (
+        IRISSNCL(station="ANMO", network="IU", channel="LF2", location="40").element
+        == "V"
+    )
+    assert (
+        IRISSNCL(station="ANMO", network="IU", channel="LFZ", location="40").element
+        == "W"
+    )
+    with pytest.raises(ValueError) as error:
+        IRISSNCL(station="ANMO", network="IU", channel="LFH", location="40").element
+        assert error.value == "Unsupported channel LFH"
+
+
+def test_get_iris_channel():
+    """edge_test.IRISSNCL_test.test_get_iris_channel()"""
+    assert (
+        get_iris_channel(
+            element="H",
+            interval="second",
+            data_type="variation",
+            network="IU",
+            location="40",
+        )
+        == "LF1"
+    )
+    assert (
+        get_iris_channel(
+            element="U",
+            interval="second",
+            data_type="variation",
+            network="IU",
+            location="40",
+        )
+        == "LF1"
+    )
+    assert (
+        get_iris_channel(
+            element="E",
+            interval="second",
+            data_type="variation",
+            network="IU",
+            location="40",
+        )
+        == "LF2"
+    )
+    assert (
+        get_iris_channel(
+            element="V",
+            interval="second",
+            data_type="variation",
+            network="IU",
+            location="40",
+        )
+        == "LF2"
+    )
+    assert (
+        get_iris_channel(
+            element="W",
+            interval="second",
+            data_type="variation",
+            network="IU",
+            location="40",
+        )
+        == "LFZ"
+    )
+    # predefined channel
+    assert (
+        get_iris_channel(
+            element="LFZ",
+            interval="second",
+            data_type="variation",
+            network="IU",
+            location="40",
+        )
+        == "LFZ"
+    )
+    with pytest.raises(ValueError) as error:
+        get_iris_channel(
+            element="D",
+            interval="second",
+            data_type="variation",
+            network="IU",
+            location="40",
+        )
+        assert error.value == "Unsupported element: D"
+
+
+def test_get_sncl():
+    """edge_test.IRISSNCL_test.test_get_sncl()"""
+    assert (
+        IRISSNCL.get_sncl(
+            data_type="variation",
+            element="H",
+            interval="second",
+            station="ANMO",
+            network="IU",
+            location="40",
+        )
+        == IRISSNCL(station="ANMO", network="IU", channel="LF1", location="40")
+    )
+    with pytest.raises(ValueError) as error:
+        IRISSNCL.get_sncl(
+            data_type="adjusted",
+            element="H",
+            interval="second",
+            station="ANMO",
+            network="IU",
+            location="40",
+        )
+        assert error.value == "Unsupported data type: adjusted"
+    assert (
+        IRISSNCL.get_sncl(
+            data_type="adjusted",
+            element="H",
+            interval="second",
+            station="BOU",
+        )
+        == IRISSNCL(station="BOU", network="NT", channel="LFH", location="A0")
+    )
+
+
+def test_interval():
+    """edge_test.IRISSNCL_test.test_interval()"""
+    assert (
+        IRISSNCL(
+            station="ANMO",
+            network="IU",
+            channel="LF1",
+            location="40",
+        ).interval
+        == "second"
+    )
+
+
+def test_parse_sncl():
+    """edge_test.IRISSNCL_test.test_parse_sncl()"""
+    assert IRISSNCL(
+        station="ANMO", network="IU", channel="LF1", location="40"
+    ).parse_sncl() == {
+        "station": "ANMO",
+        "network": "IU",
+        "data_type": "variation",
+        "element": "U",
+        "interval": "second",
+    }