diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67bcc79..070369c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # Changelog
 
+### Features
+
+* Query registries other than crates-io
+  - Additional `AsyncClient::build()` and `SyncClient::build()` functions.
+    For building a client for an alternative registry.
+
+### (Breaking) Changes
+
+* `AsyncClient::with_http_client()` now requires the crate registry url to be specified.
+* Types, make field optional: User {url}
+
 ## 0.8.1
 
 * Add `AsyncClient::with_http_client` constructor
@@ -10,7 +21,7 @@
 
 ## 0.8.0 - 2022-01-29
 
-This version has quite a few breaking changes, 
+This version has quite a few breaking changes,
 mainly to clean up and future-proof the API.
 
 ### Features
@@ -95,7 +106,7 @@ mainly to clean up and future-proof the API.
   * Crate {recent_downloads, exact_match}
   * CrateResponse {versions, keywords, categories}
   * Version {crate_size, published_by}
-* Make field optional: User {kind} 
+* Make field optional: User {kind}
 * Fix getting the reverse dependencies.
   * Rearrange the received data for simpler manipulation.
   * Add 3 new types:
diff --git a/src/async_client.rs b/src/async_client.rs
index 30cf273..7182b62 100644
--- a/src/async_client.rs
+++ b/src/async_client.rs
@@ -1,14 +1,14 @@
 use futures::future::BoxFuture;
 use futures::prelude::*;
 use futures::{future::try_join_all, try_join};
-use reqwest::{header, Client as HttpClient, StatusCode, Url};
+use reqwest::{Client as HttpClient, StatusCode, Url};
 use serde::de::DeserializeOwned;
 
 use std::collections::VecDeque;
 
 use super::Error;
 use crate::error::JsonDecodeError;
-use crate::types::*;
+use crate::{helper::*, types::*};
 
 /// Asynchronous client for the crates.io API.
 #[derive(Clone)]
@@ -124,21 +124,45 @@ impl Client {
         user_agent: &str,
         rate_limit: std::time::Duration,
     ) -> Result<Self, reqwest::header::InvalidHeaderValue> {
-        let mut headers = header::HeaderMap::new();
-        headers.insert(
-            header::USER_AGENT,
-            header::HeaderValue::from_str(user_agent)?,
-        );
+        Self::build(user_agent, rate_limit, None)
+    }
+
+    /// Build a new client.
+    ///
+    /// Returns an [`Error`] if the given user agent is invalid.
+    /// ```rust
+    /// use crates_io_api::{AsyncClient,Registry};
+    /// # fn f() -> Result<(), Box<dyn std::error::Error>> {
+    /// let client = crates_io_api::AsyncClient::build(
+    ///   "my_bot (help@my_bot.com)",
+    ///   std::time::Duration::from_millis(1000),
+    ///   Some(&Registry{
+    ///     url: "https://crates.my-registry.com/api/v1/".to_string(),
+    ///     name: Some("my_registry".to_string()),
+    ///     token: None,
+    ///     }),
+    /// ).unwrap();
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn build(
+        user_agent: &str,
+        rate_limit: std::time::Duration,
+        registry: Option<&Registry>,
+    ) -> Result<Self, reqwest::header::InvalidHeaderValue> {
+        let headers = setup_headers(user_agent, registry)?;
 
         let client = HttpClient::builder()
             .default_headers(headers)
             .build()
             .unwrap();
 
-        Ok(Self::with_http_client(client, rate_limit))
+        let base_url = base_url(registry);
+
+        Ok(Self::with_http_client(client, rate_limit, base_url))
     }
 
-    /// Instantiate a new client.
+    /// Instantiate a new client, for the registry sepcified by base_url.
     ///
     /// To respect the offical [Crawler Policy](https://crates.io/policies#crawlers),
     /// you must specify both a descriptive user agent and a rate limit interval.
@@ -146,14 +170,18 @@ impl Client {
     /// At most one request will be executed in the specified duration.
     /// The guidelines suggest 1 per second or less.
     /// (Only one request is executed concurrenly, even if the given Duration is 0).
-    pub fn with_http_client(client: HttpClient, rate_limit: std::time::Duration) -> Self {
+    pub fn with_http_client(
+        client: HttpClient,
+        rate_limit: std::time::Duration,
+        base_url: &str,
+    ) -> Self {
         let limiter = std::sync::Arc::new(tokio::sync::Mutex::new(None));
 
         Self {
             rate_limit,
             last_request_time: limiter,
             client,
-            base_url: Url::parse("https://crates.io/api/v1/").unwrap(),
+            base_url: Url::parse(base_url).unwrap(),
         }
     }
 
diff --git a/src/helper.rs b/src/helper.rs
new file mode 100644
index 0000000..2b78e41
--- /dev/null
+++ b/src/helper.rs
@@ -0,0 +1,138 @@
+//! Helper functions for querying crate registries
+
+use crate::types::*;
+use reqwest::header;
+use std::env;
+
+/// Setup the headers for a sync or async request
+pub fn setup_headers(
+    user_agent: &str,
+    registry: Option<&Registry>,
+) -> Result<header::HeaderMap, header::InvalidHeaderValue> {
+    let mut headers = header::HeaderMap::new();
+    headers.insert(
+        header::USER_AGENT,
+        header::HeaderValue::from_str(user_agent)?,
+    );
+
+    match &registry {
+        Some(registry) => match &registry.name {
+            Some(name) => {
+                if let Ok(token) =
+                    env::var(format!("CARGO_REGISTRIES_{}_TOKEN", name.to_uppercase()))
+                {
+                    headers.insert(
+                        header::AUTHORIZATION,
+                        header::HeaderValue::from_str(&token)?,
+                    );
+                }
+            }
+            None => match &registry.token {
+                Some(token) => {
+                    headers.insert(header::AUTHORIZATION, header::HeaderValue::from_str(token)?);
+                }
+                None => (),
+            },
+        },
+        None => (),
+    }
+
+    Ok(headers)
+}
+
+/// Determine the url of the crate registry being queried.
+pub fn base_url(registry: Option<&Registry>) -> &str {
+    match registry {
+        Some(reg) => reg.url.as_str(),
+        None => "https://crates.io/api/v1/",
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::Error;
+
+    #[test]
+    fn test_base_url_default() -> Result<(), Error> {
+        assert_eq!(base_url(None), "https://crates.io/api/v1/");
+        Ok(())
+    }
+
+    #[test]
+    fn test_base_url_private() -> Result<(), Error> {
+        let reg = &Registry {
+            url: "https://crates.foobar.com/api/v1/".to_string(),
+            name: None,
+            token: None,
+        };
+        assert_eq!(base_url(Some(reg)), "https://crates.foobar.com/api/v1/");
+        Ok(())
+    }
+
+    #[test]
+    fn test_crates_io_headers() -> Result<(), Error> {
+        let reg = None;
+        let user_agent = "crates-io-api-continuous-integration (github.com/theduke/crates-io-api)";
+        let headers = setup_headers(user_agent, reg).unwrap();
+
+        let mut exp_headers = header::HeaderMap::new();
+        exp_headers.insert(
+            header::USER_AGENT,
+            header::HeaderValue::from_str(user_agent).unwrap(),
+        );
+
+        assert_eq!(headers, exp_headers);
+        Ok(())
+    }
+
+    #[test]
+    fn test_private_registry_name_headers() -> Result<(), Error> {
+        let reg = &Registry {
+            url: "https://crates.foobar.com/api/v1/".to_string(),
+            name: Some("foobar".to_string()),
+            token: None,
+        };
+        env::set_var("CARGO_REGISTRIES_FOOBAR_TOKEN", "baz");
+        let user_agent = "crates-io-api-continuous-integration (github.com/theduke/crates-io-api)";
+        let headers = setup_headers(user_agent, Some(reg)).unwrap();
+
+        let mut exp_headers = header::HeaderMap::new();
+        exp_headers.insert(
+            header::USER_AGENT,
+            header::HeaderValue::from_str(user_agent).unwrap(),
+        );
+        exp_headers.insert(
+            header::AUTHORIZATION,
+            header::HeaderValue::from_str("baz").unwrap(),
+        );
+
+        assert_eq!(headers, exp_headers);
+        Ok(())
+    }
+
+    #[test]
+    fn test_private_registry_token_headers() -> Result<(), Error> {
+        let reg = &Registry {
+            url: "https://crates.foobar.com/api/v1/".to_string(),
+            name: None,
+            token: Some("foobar".to_string()),
+        };
+        env::set_var("CARGO_REGISTRIES_FOOBAR_TOKEN", "baz");
+        let user_agent = "crates-io-api-continuous-integration (github.com/theduke/crates-io-api)";
+        let headers = setup_headers(user_agent, Some(reg)).unwrap();
+
+        let mut exp_headers = header::HeaderMap::new();
+        exp_headers.insert(
+            header::USER_AGENT,
+            header::HeaderValue::from_str(user_agent).unwrap(),
+        );
+        exp_headers.insert(
+            header::AUTHORIZATION,
+            header::HeaderValue::from_str("foobar").unwrap(),
+        );
+
+        assert_eq!(headers, exp_headers);
+        Ok(())
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index b304da1..d77eb98 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -40,18 +40,29 @@
 //!     Ok(())
 //! }
 //! ```
+//! Instantiate a client for a private registry with environment variable authentication
+//!
+//! ```rust
+//! use crates_io_api::{SyncClient,Registry};
+//! let client = SyncClient::new(
+//!          "my-user-agent (my-contact@domain.com)",
+//!          std::time::Duration::from_millis(1000),
+//!     ).unwrap();
+//! ```
 
 #![recursion_limit = "128"]
 #![deny(missing_docs)]
 
 mod async_client;
 mod error;
+mod helper;
 mod sync_client;
 mod types;
 
 pub use crate::{
     async_client::Client as AsyncClient,
     error::{Error, NotFoundError, PermissionDeniedError},
+    helper::*,
     sync_client::SyncClient,
     types::*,
 };
diff --git a/src/sync_client.rs b/src/sync_client.rs
index 5e4535d..e4305e0 100644
--- a/src/sync_client.rs
+++ b/src/sync_client.rs
@@ -2,10 +2,10 @@ use super::*;
 use std::iter::Extend;
 
 use log::trace;
-use reqwest::{blocking::Client as HttpClient, header, StatusCode, Url};
+use reqwest::{blocking::Client as HttpClient, StatusCode, Url};
 use serde::de::DeserializeOwned;
 
-use crate::{error::JsonDecodeError, types::*};
+use crate::{error::JsonDecodeError, helper::*, types::*};
 
 /// A synchronous client for the crates.io API.
 pub struct SyncClient {
@@ -41,18 +41,38 @@ impl SyncClient {
         user_agent: &str,
         rate_limit: std::time::Duration,
     ) -> Result<Self, reqwest::header::InvalidHeaderValue> {
-        let mut headers = header::HeaderMap::new();
-        headers.insert(
-            header::USER_AGENT,
-            header::HeaderValue::from_str(user_agent)?,
-        );
+        Self::build(user_agent, rate_limit, None)
+    }
+
+    /// ```rust
+    /// use crates_io_api::{SyncClient,Registry};
+    /// # fn f() -> Result<(), Box<dyn std::error::Error>> {
+    /// let client = crates_io_api::SyncClient::build(
+    ///   "my_bot (help@my_bot.com)",
+    ///   std::time::Duration::from_millis(1000),
+    ///   Some(&Registry{
+    ///     url: "https://crates.my-registry.com/api/v1/".to_string(),
+    ///     name: Some("my_registry".to_string()),
+    ///     token: None,
+    ///     }),
+    ///  ).unwrap();
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub fn build(
+        user_agent: &str,
+        rate_limit: std::time::Duration,
+        registry: Option<&Registry>,
+    ) -> Result<Self, reqwest::header::InvalidHeaderValue> {
+        let headers = setup_headers(user_agent, registry)?;
+        let base_url = base_url(registry);
 
         Ok(Self {
             client: HttpClient::builder()
                 .default_headers(headers)
                 .build()
                 .unwrap(),
-            base_url: Url::parse("https://crates.io/api/v1/").unwrap(),
+            base_url: Url::parse(base_url).unwrap(),
             rate_limit,
             last_request_time: std::sync::Mutex::new(None),
         })
diff --git a/src/types.rs b/src/types.rs
index 5dff9e9..297972c 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -4,6 +4,16 @@ use chrono::{DateTime, NaiveDate, Utc};
 use serde_derive::*;
 use std::collections::HashMap;
 
+/// Used to specify the registry being queried by either client.
+pub struct Registry {
+    /// Url of the registry
+    pub url: String,
+    /// Name of the registry
+    pub name: Option<String>,
+    /// Token used to authenticate registry requests.
+    pub token: Option<String>,
+}
+
 /// Used to specify the sort behaviour of the `Client::crates()` method.
 #[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
 pub struct ApiErrors {
@@ -432,7 +442,7 @@ pub struct User {
     pub kind: Option<String>,
     pub login: String,
     pub name: Option<String>,
-    pub url: String,
+    pub url: Option<String>,
 }
 
 /// Additional crate author metadata.