diff --git a/README.rst b/README.rst
index 97a61ea..45fc2d5 100644
--- a/README.rst
+++ b/README.rst
@@ -94,6 +94,14 @@ The following methods are available to both collecton and granule queries:
     # only include granules that are unavailable for download
     >>> api.online_only()
 
+    # search for collections/granules associated with or identified by concept IDs
+    # note: often the ECHO collection ID can be used here as well
+    # note: when using CollectionQuery, only collection concept IDs can be passed
+    # note: when uses GranuleQuery, passing a collection's concept ID will filter by granules associated
+    #       with that particular collection.
+    >>> api.concept_id("C1299783579-LPDAAC_ECS")
+    >>> api.concept_id(["G1327299284-LPDAAC_ECS", "G1326330014-LPDAAC_ECS"])
+
 
 Granule searches support these methods (in addition to the shared methods above):
 
diff --git a/cmr/queries.py b/cmr/queries.py
index fb19fa2..7e44fa9 100644
--- a/cmr/queries.py
+++ b/cmr/queries.py
@@ -592,6 +592,25 @@ class GranuleQuery(Query):
         self.params['granule_ur'] = granule_ur
         return self
 
+    def concept_id(self, IDs):
+        """
+        Filter by concept ID (ex: C1299783579-LPDAAC_ECS or G1327299284-LPDAAC_ECS)
+
+        Collections and granules are uniquely identified with this ID. If providing a collection's concept ID
+        here, it will filter by granules associated with that collection. If providing a granule's concept ID
+        here, it will uniquely identify those granules.
+
+        :param IDs: concept ID(s) to search by. Can be provided as a string or list of strings.
+        :returns: Query instance
+        """
+
+        if isinstance(IDs, str):
+            IDs = [IDs]
+        
+        self.params["concept_id"] = IDs
+
+        return self
+
     def _valid_state(self):
 
         # spatial params must be paired with a collection limiting parameter
@@ -646,5 +665,27 @@ class CollectionQuery(Query):
 
         return self
 
+    def concept_id(self, IDs):
+        """
+        Filter by concept ID (ex: C1299783579-LPDAAC_ECS)
+
+        Collections are uniquely identified with this ID.
+
+        :param IDs: concept ID(s) to search by. Can be provided as a string or list of strings.
+        :returns: Query instance
+        """
+
+        if isinstance(IDs, str):
+            IDs = [IDs]
+        
+        # verify we weren't provided any granule concept IDs
+        for ID in IDs:
+            if ID.strip()[0] != "C":
+                raise ValueError("Only collection concept ID's can be provided (begin with 'C'): {}".format(ID))
+        
+        self.params["concept_id"] = IDs
+
+        return self
+
     def _valid_state(self):
         return True
diff --git a/requirements.txt b/requirements.txt
index 0feba0e..2351d9c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,7 @@
-appdirs==1.4.0
-astroid==1.4.9
-isort==4.2.5
-lazy-object-proxy==1.2.2
-mccabe==0.5.3
-packaging==16.8
-pylint==1.6.4
-pyparsing==2.1.10
-requests==2.12.4
-six==1.10.0
-wrapt==1.10.8
+certifi==2019.6.16
+chardet==3.0.4
+idna==2.8
+pkg-resources==0.0.0
+python-cmr==0.3.3
+requests==2.22.0
+urllib3==1.25.3

Add GranuleQuery.collection_id() function
Hi @jddeal we've been using this toolkit... in short _**thank you**_ for publishing it, it;s saved us some time and cleaned up quite a fair bit if code. 
I have a feature request which I would like to discuss with you.
Ideally, we want to search for granules as follows
```
        gq = GranuleQuery()
        gq = gq.collection_id(collection_id)
```
In this user scenario we obtain the collection_id's from the UMM-S = UMM-C associations we curate.

Does adding a function to the GranuleQuery Class make sense?
Thanks

diff --git a/tests/test_collection.py b/tests/test_collection.py
index 3241e79..afdaf51 100644
--- a/tests/test_collection.py
+++ b/tests/test_collection.py
@@ -36,3 +36,22 @@ class TestCollectionClass(unittest.TestCase):
             query.format("invalid")
             query.format("jsonn")
             query.format("iso19116")
+    
+    def test_valid_concept_id(self):
+        query = CollectionQuery()
+
+        query.concept_id("C1299783579-LPDAAC_ECS")
+        self.assertEqual(query.params["concept_id"], ["C1299783579-LPDAAC_ECS"])
+        
+        query.concept_id(["C1299783579-LPDAAC_ECS", "C1441380236-PODAAC"])
+        self.assertEqual(query.params["concept_id"], ["C1299783579-LPDAAC_ECS", "C1441380236-PODAAC"])
+    
+    def test_invalid_concept_id(self):
+        query = CollectionQuery()
+
+        with self.assertRaises(ValueError):
+            query.concept_id("G1327299284-LPDAAC_ECS")
+        
+        with self.assertRaises(ValueError):
+            query.concept_id(["C1299783579-LPDAAC_ECS", "G1327299284-LPDAAC_ECS"])
+
diff --git a/tests/test_granule.py b/tests/test_granule.py
index 190f6cc..e4bb297 100644
--- a/tests/test_granule.py
+++ b/tests/test_granule.py
@@ -419,3 +419,13 @@ class TestGranuleClass(unittest.TestCase):
         url = query._build_url()
         self.assertNotIn("True", url)
         self.assertNotIn("False", url)
+    
+    def test_valid_concept_id(self):
+        query = GranuleQuery()
+
+        query.concept_id("C1299783579-LPDAAC_ECS")
+        self.assertEqual(query.params["concept_id"], ["C1299783579-LPDAAC_ECS"])
+        
+        query.concept_id(["C1299783579-LPDAAC_ECS", "G1441380236-PODAAC"])
+        self.assertEqual(query.params["concept_id"], ["C1299783579-LPDAAC_ECS", "G1441380236-PODAAC"])
+

pip install -e .[dev]
parse_log_pytest
pytest
pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi==2025.1.31
charset-normalizer==3.4.1
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
idna==3.10
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pytest @ file:///croot/pytest_1738938843180/work
-e git+https://github.com/jddeal/python-cmr.git@79b5556b72886c00901fb6e927f1c8aa009ca967#egg=python_cmr
requests==2.32.3
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
urllib3==2.3.0

name: python-cmr
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
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - 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=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - idna==3.10
      - requests==2.32.3
      - urllib3==2.3.0
prefix: /opt/conda/envs/python-cmr

tests/test_collection.py::TestCollectionClass::test_invalid_concept_id tests/test_collection.py::TestCollectionClass::test_valid_concept_id tests/test_granule.py::TestGranuleClass::test_valid_concept_id
tests/test_collection.py::TestCollectionClass::test_archive_center tests/test_collection.py::TestCollectionClass::test_invalid_format tests/test_collection.py::TestCollectionClass::test_keyword tests/test_collection.py::TestCollectionClass::test_valid_formats tests/test_granule.py::TestGranuleClass::test_bounding_box_invalid_set tests/test_granule.py::TestGranuleClass::test_bounding_box_set tests/test_granule.py::TestGranuleClass::test_cloud_cover_all tests/test_granule.py::TestGranuleClass::test_cloud_cover_max_only tests/test_granule.py::TestGranuleClass::test_cloud_cover_min_only tests/test_granule.py::TestGranuleClass::test_cloud_cover_none tests/test_granule.py::TestGranuleClass::test_day_night_flag_day_set tests/test_granule.py::TestGranuleClass::test_day_night_flag_invalid_set tests/test_granule.py::TestGranuleClass::test_day_night_flag_invalid_type_set tests/test_granule.py::TestGranuleClass::test_day_night_flag_night_set tests/test_granule.py::TestGranuleClass::test_day_night_flag_unspecified_set tests/test_granule.py::TestGranuleClass::test_downloadable_invalid tests/test_granule.py::TestGranuleClass::test_downloadable_set tests/test_granule.py::TestGranuleClass::test_empty_granule_ur tests/test_granule.py::TestGranuleClass::test_empty_instrument tests/test_granule.py::TestGranuleClass::test_empty_platform tests/test_granule.py::TestGranuleClass::test_entry_title_set tests/test_granule.py::TestGranuleClass::test_flags_invalidate_the_other tests/test_granule.py::TestGranuleClass::test_granule_ur tests/test_granule.py::TestGranuleClass::test_instrument tests/test_granule.py::TestGranuleClass::test_invalid_format tests/test_granule.py::TestGranuleClass::test_invalid_mode tests/test_granule.py::TestGranuleClass::test_invalid_mode_constructor tests/test_granule.py::TestGranuleClass::test_invalid_parameters tests/test_granule.py::TestGranuleClass::test_invalid_spatial_state tests/test_granule.py::TestGranuleClass::test_line_invalid_set tests/test_granule.py::TestGranuleClass::test_line_set tests/test_granule.py::TestGranuleClass::test_lowercase_bool_url tests/test_granule.py::TestGranuleClass::test_online_only_invalid tests/test_granule.py::TestGranuleClass::test_online_only_set tests/test_granule.py::TestGranuleClass::test_orbit_number_encode tests/test_granule.py::TestGranuleClass::test_orbit_number_set tests/test_granule.py::TestGranuleClass::test_platform tests/test_granule.py::TestGranuleClass::test_point_invalid_set tests/test_granule.py::TestGranuleClass::test_point_set tests/test_granule.py::TestGranuleClass::test_polygon_invalid_set tests/test_granule.py::TestGranuleClass::test_polygon_set tests/test_granule.py::TestGranuleClass::test_short_name tests/test_granule.py::TestGranuleClass::test_temporal_invalid_date_order tests/test_granule.py::TestGranuleClass::test_temporal_invalid_strings tests/test_granule.py::TestGranuleClass::test_temporal_invalid_types tests/test_granule.py::TestGranuleClass::test_temporal_option_set tests/test_granule.py::TestGranuleClass::test_temporal_set tests/test_granule.py::TestGranuleClass::test_valid_formats tests/test_granule.py::TestGranuleClass::test_valid_parameters tests/test_granule.py::TestGranuleClass::test_valid_spatial_state tests/test_granule.py::TestGranuleClass::test_version
README.rst cmr/queries.py requirements.txt
README.rst cmr/queries.py requirements.txt