diff --git a/.gitignore b/.gitignore
index a8cadd0b80f9e0d36ca345bfa239763035ff08b1..12c925f9cade217df72225a9e39a8d97c9b6dc67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 .coverage
 cov.xml
 .DS_Store
+.eggs
 node_modules
 *.pyc
 coverage.xml
diff --git a/geomagio/api/secure/login.py b/geomagio/api/secure/login.py
index b687dec3bd562b92b6b30ac4657db62936e5d279..9da5838a652f781cd00b3055edae73d54bfe2745 100644
--- a/geomagio/api/secure/login.py
+++ b/geomagio/api/secure/login.py
@@ -35,16 +35,20 @@ Usage:
 """
 import logging
 import os
-import requests
 from typing import Callable, List, Optional
 
 from authlib.integrations.starlette_client import OAuth
 from fastapi import APIRouter, Depends, HTTPException
+import httpx
 from pydantic import BaseModel
 from starlette.requests import Request
 from starlette.responses import RedirectResponse
 
 
+GITLAB_HOST = os.getenv("GITLAB_HOST", "code.usgs.gov")
+GITLAB_API_URL = os.getenv("GITLAB_API_URL", f"https://{GITLAB_HOST}/api/v4")
+
+
 class User(BaseModel):
     """Information about a logged in user."""
 
@@ -64,45 +68,39 @@ async def current_user(request: Request) -> Optional[User]:
         user: Optional[User] = Depends(current_user)
 
     """
+    user = None
     if "user" in request.session:
-        return User(**request.session["user"])
-    if "apiuser" in request.session:
-        return User(**request.session["apiuser"])
-    if "Authorization" in request.headers:
-        user = get_api_user(token=request.headers["Authorization"])
+        user = User(**request.session["user"])
+    elif "Authorization" in request.headers:
+        user = await get_gitlab_user(token=request.headers["Authorization"])
         if user is not None:
-            request.session["apiuser"] = user.dict()
-            return user
-    return None
+            request.session["user"] = user.dict()
+    return user
 
 
-def get_api_user(token: str) -> Optional[User]:
-    url = os.getenv("GITLAB_API_URL")
+async def get_gitlab_user(token: str, url: str = GITLAB_API_URL) -> Optional[User]:
     header = {"PRIVATE-TOKEN": token}
     # request user information from gitlab api with access token
-    userinfo_response = requests.get(
-        f"{url}/user",
-        headers=header,
-    )
-    userinfo = userinfo_response.json()
+
     try:
-        user = User(
-            email=userinfo["email"],
-            sub=userinfo["id"],
-            name=userinfo["name"],
-            nickname=userinfo["username"],
-            picture=userinfo["avatar_url"],
-        )
-    except KeyError:
-        logging.info(f"Invalid token: {userinfo_response.status_code}")
+        # use httpx/async so this doesn't block other requests
+        async with httpx.AsyncClient() as client:
+            userinfo_response = await client.get(f"{url}/user", headers=header)
+            userinfo = userinfo_response.json()
+            user = User(
+                email=userinfo["email"],
+                sub=userinfo["id"],
+                name=userinfo["name"],
+                nickname=userinfo["username"],
+                picture=userinfo["avatar_url"],
+            )
+            # use valid token to retrieve user's groups
+            groups_response = await client.get(f"{url}/groups", headers=header)
+            user.groups = [g["full_path"] for g in groups_response.json()]
+            return user
+    except Exception:
+        logging.exception(f"Unable to get gitlab user")
         return None
-    # use valid token to retrieve user's groups
-    groups_response = requests.get(
-        f"{url}/groups",
-        headers=header,
-    )
-    user.groups = [g["full_path"] for g in groups_response.json()]
-    return user
 
 
 def require_user(
@@ -143,7 +141,9 @@ oauth.register(
     name="openid",
     client_id=os.getenv("OPENID_CLIENT_ID"),
     client_secret=os.getenv("OPENID_CLIENT_SECRET"),
-    server_metadata_url=os.getenv("OPENID_METADATA_URL"),
+    server_metadata_url=os.getenv(
+        "OPENID_METADATA_URL", f"https://{GITLAB_HOST}/.well-known/openid-configuration"
+    ),
     client_kwargs={"scope": "openid email profile"},
 )
 # routes for login/logout
diff --git a/geomagio/apiclient/__init__.py b/geomagio/apiclient/__init__.py
deleted file mode 100644
index 53bb45e4c2ebd258886d2bcf0d41522b7a519bdb..0000000000000000000000000000000000000000
--- a/geomagio/apiclient/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .metadata import app
-from .MetadataFactory import MetadataFactory
-
-__all__ = ["app", "MetadataFactory"]
diff --git a/geomagio/apiclient/MetadataFactory.py b/geomagio/metadata/MetadataFactory.py
similarity index 52%
rename from geomagio/apiclient/MetadataFactory.py
rename to geomagio/metadata/MetadataFactory.py
index 288901c6856ce2a2a90ec8054555283b301d4d2e..0558bcc20efa7030953ebc4ae6be0c45b700b9d5 100644
--- a/geomagio/apiclient/MetadataFactory.py
+++ b/geomagio/metadata/MetadataFactory.py
@@ -1,57 +1,73 @@
 import os
 import requests
-from typing import List, Union
+from typing import List
 
 from obspy import UTCDateTime
 from pydantic import parse_obj_as
 
-from ..metadata import Metadata, MetadataQuery
+from .Metadata import Metadata
+from .MetadataQuery import MetadataQuery
+
+
+GEOMAG_API_HOST = os.getenv("GEOMAG_API_HOST", "geomag.usgs.gov")
+GEOMAG_API_URL = f"https://{GEOMAG_API_HOST}/ws/secure/metadata"
+if "127.0.0.1" in GEOMAG_API_URL:
+    GEOMAG_API_URL = GEOMAG_API_URL.replace("https://", "http://")
 
 
 class MetadataFactory(object):
     def __init__(
         self,
-        url: str = "http://{}/ws/secure/metadata".format(
-            os.getenv("GEOMAG_API_HOST", "127.0.0.1:8000")
-        ),
+        url: str = GEOMAG_API_URL,
         token: str = os.getenv("GITLAB_API_TOKEN"),
     ):
         self.url = url
         self.token = token
-        self.header = self._get_headers()
 
     def _get_headers(self):
         return {"Authorization": self.token} if self.token else None
 
     def delete_metadata(self, metadata: Metadata) -> bool:
-        response = requests.delete(url=f"{self.url}/{metadata.id}", headers=self.header)
+        response = requests.delete(
+            url=f"{self.url}/{metadata.id}",
+            headers=self._get_headers(),
+        )
         if response.status_code == 200:
             return True
         return False
 
     def get_metadata(self, query: MetadataQuery) -> List[Metadata]:
         if query.id:
-            response = self.get_metadata_by_id(id=query.id)
+            metadata = [self.get_metadata_by_id(id=query.id)]
         else:
-            args = parse_params(query=query)
-            response = requests.get(url=self.url, params=args, headers=self.header)
-        metadata = parse_obj_as(Union[List[Metadata], Metadata], response.json())
-        if isinstance(metadata, Metadata):
-            metadata = [metadata]
+            response = requests.get(
+                url=self.url,
+                headers=self._get_headers(),
+                params=parse_params(query=query),
+            )
+            metadata = parse_obj_as(List[Metadata], response.json())
         return metadata
 
-    def get_metadata_by_id(self, id: int) -> requests.Response:
-        return requests.get(f"{self.url}/{id}", headers=self.header)
+    def get_metadata_by_id(self, id: int) -> Metadata:
+        response = requests.get(
+            url=f"{self.url}/{id}",
+            headers=self._get_headers(),
+        )
+        return Metadata(**response.json())
 
     def create_metadata(self, metadata: Metadata) -> Metadata:
         response = requests.post(
-            url=self.url, data=metadata.json(), headers=self.header
+            url=self.url,
+            data=metadata.json(),
+            headers=self._get_headers(),
         )
         return Metadata(**response.json())
 
     def update_metadata(self, metadata: Metadata) -> Metadata:
         response = requests.put(
-            url=f"{self.url}/{metadata.id}", data=metadata.json(), headers=self.header
+            url=f"{self.url}/{metadata.id}",
+            data=metadata.json(),
+            headers=self._get_headers(),
         )
         return Metadata(**response.json())
 
diff --git a/geomagio/metadata/__init__.py b/geomagio/metadata/__init__.py
index 37b2adc332b5f6a90f02c75ac9166cf19de48089..592b2d1593a7c15bbcb08400b5e5e65c6663de2a 100644
--- a/geomagio/metadata/__init__.py
+++ b/geomagio/metadata/__init__.py
@@ -1,6 +1,7 @@
 from .Metadata import Metadata
 from .MetadataCategory import MetadataCategory
+from .MetadataFactory import MetadataFactory
 from .MetadataQuery import MetadataQuery
 
 
-__all__ = ["Metadata", "MetadataCategory", "MetadataQuery"]
+__all__ = ["Metadata", "MetadataCategory", "MetadataFactory", "MetadataQuery"]
diff --git a/geomagio/apiclient/metadata.py b/geomagio/metadata/main.py
similarity index 65%
rename from geomagio/apiclient/metadata.py
rename to geomagio/metadata/main.py
index afb3ea63e3c64354553c11c0b8b8cc4773bdb7f3..79cf886baa37a048046498d67b2bb785a9d21473 100644
--- a/geomagio/apiclient/metadata.py
+++ b/geomagio/metadata/main.py
@@ -1,13 +1,48 @@
 import sys
 import json
 import os
+import textwrap
 from typing import Dict, Optional
 
 from obspy import UTCDateTime
 import typer
 
-from ..metadata import Metadata, MetadataCategory, MetadataQuery
+from .Metadata import Metadata
+from .MetadataCategory import MetadataCategory
 from .MetadataFactory import MetadataFactory
+from .MetadataQuery import MetadataQuery
+
+
+GEOMAG_API_HOST = os.getenv("GEOMAG_API_HOST", "geomag.usgs.gov")
+GEOMAG_API_URL = f"https://{GEOMAG_API_HOST}/ws/secure/metadata"
+if "127.0.0.1" in GEOMAG_API_URL:
+    GEOMAG_API_URL = GEOMAG_API_URL.replace("https://", "http://")
+
+
+ENVIRONMENT_VARIABLE_HELP = """Environment variables:
+
+      GITLAB_API_TOKEN
+
+        (Required) Personal access token with "read_api" scope. Create at
+        https://code.usgs.gov/profile/personal_access_tokens
+
+      GEOMAG_API_HOST
+
+        Default "geomag.usgs.gov"
+
+      REQUESTS_CA_BUNDLE
+
+        Use custom certificate bundle
+    """
+
+
+app = typer.Typer(
+    help=f"""
+    Command line interface for Metadata API
+
+    {ENVIRONMENT_VARIABLE_HELP}
+    """
+)
 
 
 def load_metadata(input_file: str) -> Optional[Dict]:
@@ -21,14 +56,22 @@ def load_metadata(input_file: str) -> Optional[Dict]:
     return data
 
 
-app = typer.Typer()
+def main():
+    """Command line interface for Metadata API.
+
+    Registered as "geomag-metadata" console script in setup.py.
+    """
+    app()
+
 
+@app.command(
+    help=f"""
+    Create new metadata.
 
-@app.command()
+    {ENVIRONMENT_VARIABLE_HELP}
+    """
+)
 def create(
-    url: str = "http://{}/ws/secure/metadata".format(
-        os.getenv("GEOMAG_API_HOST", "127.0.0.1:8000")
-    ),
     category: MetadataCategory = None,
     channel: str = None,
     created_after: str = None,
@@ -42,6 +85,7 @@ def create(
     network: str = None,
     starttime: str = None,
     station: str = None,
+    url: str = GEOMAG_API_URL,
     wrap: bool = True,
 ):
     input_metadata = load_metadata(input_file=input_file)
@@ -67,12 +111,16 @@ def create(
     print(metadata.json())
 
 
-@app.command()
+@app.command(
+    help=f"""
+    Delete an existing metadata.
+
+    {ENVIRONMENT_VARIABLE_HELP}
+    """
+)
 def delete(
     input_file: str,
-    url: str = "http://{}/ws/secure/metadata".format(
-        os.getenv("GEOMAG_API_HOST", "127.0.0.1:8000")
-    ),
+    url: str = GEOMAG_API_URL,
 ):
     metadata_dict = load_metadata(input_file=input_file)
     metadata = Metadata(**metadata_dict)
@@ -81,24 +129,28 @@ def delete(
         sys.exit(1)
 
 
-@app.command()
+@app.command(
+    help=f"""
+    Search existing metadata.
+
+    {ENVIRONMENT_VARIABLE_HELP}
+    """
+)
 def get(
-    url: str = "http://{}/ws/secure/metadata".format(
-        os.getenv("GEOMAG_API_HOST", "127.0.0.1:8000")
-    ),
     category: Optional[MetadataCategory] = None,
     channel: Optional[str] = None,
     created_after: Optional[str] = None,
     created_before: Optional[str] = None,
-    data_valid: Optional[bool] = True,
+    data_valid: Optional[bool] = None,
     endtime: Optional[str] = None,
+    getone: bool = False,
     id: Optional[int] = None,
     location: Optional[str] = None,
-    metadata_valid: Optional[bool] = True,
+    metadata_valid: Optional[bool] = None,
     network: Optional[str] = None,
     starttime: Optional[str] = None,
     station: Optional[str] = None,
-    getone: bool = False,
+    url: str = GEOMAG_API_URL,
 ):
     query = MetadataQuery(
         category=category,
@@ -115,30 +167,26 @@ def get(
         station=station,
     )
     metadata = MetadataFactory(url=url).get_metadata(query=query)
-    if not metadata:
-        print([])
-        return
-
     if getone:
-        if len(metadata) > 1:
-            raise ValueError("More than one matching record")
+        if len(metadata) != 1:
+            raise ValueError(f"{len(metadata)} matching records")
         print(metadata[0].json())
-        return
-    print("[" + ",".join([m.json() for m in metadata]) + "]")
+    else:
+        print("[" + ",\n".join([m.json() for m in metadata]) + "]")
 
 
-@app.command()
+@app.command(
+    help=f"""
+    Update an existing metadata.
+
+    {ENVIRONMENT_VARIABLE_HELP}
+    """
+)
 def update(
     input_file: str,
-    url: str = "http://{}/ws/secure/metadata".format(
-        os.getenv("GEOMAG_API_HOST", "127.0.0.1:8000")
-    ),
+    url: str = GEOMAG_API_URL,
 ):
     metadata_dict = load_metadata(input_file=input_file)
     metadata = Metadata(**metadata_dict)
     response = MetadataFactory(url=url).update_metadata(metadata=metadata)
     print(response.json())
-
-
-def main():
-    app()
diff --git a/setup.py b/setup.py
index a73fd11e7b6ce8880a573af13857d342c6348f12..9a9c5ace7e577e308272665250636eeb2e1027af 100644
--- a/setup.py
+++ b/setup.py
@@ -26,7 +26,7 @@ setuptools.setup(
     entry_points={
         "console_scripts": [
             "generate-matrix=geomagio.processing.adjusted:main",
-            "geomag-apiclient=geomagio.apiclient.metadata:main",
+            "geomag-metadata=geomagio.metadata.main:main",
             "magproc-prepfiles=geomagio.processing.magproc:main",
             "obsrio-filter=geomagio.processing.obsrio:main",
         ],