diff --git a/geomagio/API/data_api.py b/geomagio/API/data_api.py
index 3905850caaeb1eaef0b3e1a1bf4aba62c59d6ecd..be73b2ce1fef572861af8d5257d3e48d9e62da81 100644
--- a/geomagio/API/data_api.py
+++ b/geomagio/API/data_api.py
@@ -1,22 +1,24 @@
-from fastapi import FastAPI, Query, HTTPException, Request
-from fastapi.responses import JSONResponse
-from pydantic import BaseModel
+from datetime import datetime
+from fastapi import FastAPI, Query, Request
+from json import dumps
 from obspy import UTCDateTime
+import os
+from pydantic import BaseModel
+from starlette.responses import Response
 from typing import List, Any
-from datetime import datetime
 
-from fastapi.exceptions import RequestValidationError
-from fastapi.responses import PlainTextResponse
-from starlette.exceptions import HTTPException as StarletteHTTPException
-from flask.json import dumps
 
+from ..edge import EdgeFactory
+from ..iaga2002 import IAGA2002Writer
+from ..imfjson import IMFJSONWriter
+from ..TimeseriesUtility import get_interval_from_delta
 
-app = FastAPI()
 
 DEFAULT_DATA_TYPE = "variation"
 DEFAULT_ELEMENTS = ["X", "Y", "Z", "F"]
 DEFAULT_OUTPUT_FORMAT = "iaga2002"
 DEFAULT_SAMPLING_PERIOD = "60"
+DEFAULT_STARTTIME = datetime.now()
 ERROR_CODE_MESSAGES = {
     204: "No Data",
     400: "Bad Request",
@@ -74,6 +76,10 @@ VALID_OBSERVATORIES = [
 ]
 VALID_OUTPUT_FORMATS = ["iaga2002", "json"]
 VALID_SAMPLING_PERIODS = [0.1, 1, 60, 3600, 86400]
+VERSION = "version"
+
+
+app = FastAPI(docs_url="/data")
 
 
 class WebServiceException(Exception):
@@ -92,22 +98,17 @@ class WebServiceQuery(BaseModel):
     output_format: str = DEFAULT_OUTPUT_FORMAT
 
 
-@app.get("/")
-def read_root():
-    return {"Hello": "World"}
-
-
 @app.get("/data/")
-async def read_query(
+def read_query(
+    request: Request,
     id: str,
-    starttime: Any = Query(UTCDateTime(datetime.now())),
-    endtime: str = Query(None),
+    starttime: Any = Query(DEFAULT_STARTTIME),
+    endtime: Any = Query(None),
     elements: List[str] = Query(DEFAULT_ELEMENTS),
-    sampling_period: float = DEFAULT_SAMPLING_PERIOD,
-    data_type: str = DEFAULT_DATA_TYPE,
-    output_format: str = DEFAULT_OUTPUT_FORMAT,
+    sampling_period: float = Query(DEFAULT_SAMPLING_PERIOD),
+    data_type: str = Query(DEFAULT_DATA_TYPE),
+    format: str = Query(DEFAULT_OUTPUT_FORMAT),
 ):
-    starttime = UTCDateTime(starttime)
     if len(elements) == 1 and "," in elements[0]:
         elements = [e.strip() for e in elements[0].split(",")]
 
@@ -118,23 +119,128 @@ async def read_query(
         "elements": elements,
         "sampling_period": sampling_period,
         "data_type": data_type,
-        "output_format": output_format,
+        "output_format": format,
     }
 
     try:
         parsed_query = parse_query(observatory)
         validate_query(parsed_query)
     except Exception as e:
-        return format_error(400, e)
+        return format_error(400, e, format, request)
+
+    try:
+        timeseries = get_timeseries(parsed_query)
+        return format_timeseries(timeseries, parsed_query)
+    except Exception as e:
+        return format_error(500, e, format, request)
+
+
+def format_error(status_code, exception, format, request):
+    if format == "json":
+        error = Response(
+            json_error(status_code, exception, request), media_type="application/json"
+        )
+    else:
+        error = Response(
+            iaga2002_error(status_code, exception, request), media_type="text/plain"
+        )
 
-    return parsed_query
+    return error
 
 
-def format_error(status_code, exception):
-    return JSONResponse(json_error(status_code, exception), mimetype="application/json")
+def format_timeseries(timeseries, query):
+    """Formats timeseries into JSON or IAGA data
 
+    Parameters
+    ----------
+    obspy.core.Stream
+        timeseries object with requested data
+
+    WebServiceQuery
+        parsed query object
+
+    Returns
+    -------
+    unicode
+        IAGA2002 or JSON formatted string.
+    """
+    if query.output_format == "json":
+        return Response(
+            IMFJSONWriter.format(timeseries, query.elements),
+            media_type="application/json",
+        )
+    else:
+        return Response(
+            IAGA2002Writer.format(timeseries, query.elements), media_type="text/plain",
+        )
+
+
+def get_data_factory():
+    """Reads environment variable to determine the factory to be used
+
+    Returns
+    -------
+    data_factory
+        Edge or miniseed factory object
+    """
+    data_type = os.getenv("DATA_TYPE", "edge")
+    data_host = os.getenv("DATA_HOST", "cwbpub.cr.usgs.gov")
+    data_port = os.getenv("DATA_PORT", 2060)
+
+    if data_type == "edge":
+        data_factory = EdgeFactory(host=data_host, port=data_port)
+        return data_factory
+    else:
+        return None
 
-def json_error(code: int, error: Exception):
+
+def get_timeseries(query):
+    """Get timeseries data
+
+    Parameters
+    ----------
+     WebServiceQuery
+        parsed query object
+
+    Returns
+    -------
+    obspy.core.Stream
+        timeseries object with requested data
+    """
+    data_factory = get_data_factory()
+
+    timeseries = data_factory.get_timeseries(
+        query.starttime,
+        query.endtime,
+        query.observatory_id,
+        query.elements,
+        query.data_type,
+        get_interval_from_delta(query.sampling_period),
+    )
+    return timeseries
+
+
+def iaga2002_error(status_code, error, request):
+    status_message = ERROR_CODE_MESSAGES[status_code]
+    error_body = f"""Error {status_code}: {status_message}
+
+{error}
+
+Usage details are available from
+
+Request:
+{request.url}
+
+Request Submitted:
+{UTCDateTime().isoformat()}Z
+
+Service Version:
+{VERSION}
+"""
+    return error_body
+
+
+def json_error(code: int, error: Exception, request):
     """Format json error message.
 
     Returns
@@ -143,22 +249,32 @@ def json_error(code: int, error: Exception):
         body of json error message.
     """
     status_message = ERROR_CODE_MESSAGES[code]
+    url = request.url.__dict__
     error_dict = {
         "type": "Error",
         "metadata": {
-            "status": code,
-            "generated": UTCDateTime().isoformat() + "Z",
-            "url": "url",
             "title": status_message,
+            "status": code,
             "error": str(error),
+            "generated": UTCDateTime().isoformat() + "Z",
+            "url": url,
         },
     }
-    return dumps(error_dict, sort_keys=True).encode("utf8")
+    return dumps(error_dict).encode("utf8")
 
 
 def parse_query(query):
 
-    if not query["endtime"]:
+    try:
+        query["starttime"] = UTCDateTime(query["starttime"])
+
+    except Exception as e:
+        raise WebServiceException(
+            f"Bad starttime value '{query['starttime']}'."
+            " Valid values are ISO-8601 timestamps."
+        ) from e
+
+    if query["endtime"] == None:
         endtime = query["starttime"] + (24 * 60 * 60 - 1)
         query["endtime"] = endtime
 
@@ -172,6 +288,7 @@ def parse_query(query):
                 f"Bad endtime value '{query['endtime']}'."
                 " Valid values are ISO-8601 timestamps."
             ) from e
+
     params = WebServiceQuery(**query)
     return params
 
@@ -210,13 +327,3 @@ def validate_query(query):
     )
     if samples > REQUEST_LIMIT:
         raise WebServiceException(f"Query exceeds request limit ({samples} > 345600)")
-
-
-@app.exception_handler(RequestValidationError)
-async def validation_exception_handler(request, exc):
-    return PlainTextResponse(str(exc), status_code=400)
-
-
-@app.exception_handler(StarletteHTTPException)
-async def http_exception_handler(request, exc):
-    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)