Bug: [FEATURE] Allow rotating and revoking API keys



### Feature Description

As of now, the `merchant_account` table has an `api_key` field which restricts a merchant account to only have one API key. Furthermore, there are no mechanisms in place to revoke and regenerate API keys. To improve the convenience of rotating and revoking API keys, it'd be better to allow multiple API keys to be associated with a merchant account. Furthermore, API keys are stored in plaintext as of now. It'd be great if they could be hashed as well, when this request is addressed.

### Possible Implementation

A possible approach to address this is to add an `api_keys` table with `name`, `merchant_id`, `hashed_key` fields (including any additional fields as necessary). And for the API interface, this could include endpoints to create, retrieve, update and revoke an API key, and an endpoint to list all API keys associated with the current merchant account.

### Have you spent some time to check if this feature request has been raised before?

- [X] I checked and didn't find similar issue

### Have you read the Contributing Guidelines?

- [X] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

Yes I am willing to submit a PR!
diff --git a/Cargo.lock b/Cargo.lock
index f192fbfe685..0e45915379f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -329,6 +329,18 @@ version = "1.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f907281554a3d0312bb7aab855a8e0ef6cbf1614d06de54105039ca8b34460e"
 
+[[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+
 [[package]]
 name = "assert-json-diff"
 version = "2.0.2"
@@ -873,6 +885,20 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "blake3"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "digest 0.10.6",
+]
+
 [[package]]
 name = "block-buffer"
 version = "0.9.0"
@@ -1012,6 +1038,7 @@ dependencies = [
  "nanoid",
  "once_cell",
  "proptest",
+ "rand 0.8.5",
  "regex",
  "ring",
  "router_env",
@@ -1053,6 +1080,12 @@ dependencies = [
  "yaml-rust",
 ]
 
+[[package]]
+name = "constant_time_eq"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279"
+
 [[package]]
 name = "convert_case"
 version = "0.4.0"
@@ -2993,6 +3026,7 @@ dependencies = [
  "aws-sdk-kms",
  "base64 0.21.0",
  "bb8",
+ "blake3",
  "bytes",
  "clap",
  "common_utils",
diff --git a/crates/api_models/src/api_keys.rs b/crates/api_models/src/api_keys.rs
new file mode 100644
index 00000000000..f804ce8bad9
--- /dev/null
+++ b/crates/api_models/src/api_keys.rs
@@ -0,0 +1,295 @@
+use common_utils::custom_serde;
+use masking::StrongSecret;
+use serde::{Deserialize, Serialize};
+use time::PrimitiveDateTime;
+use utoipa::ToSchema;
+
+/// The request body for creating an API Key.
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(deny_unknown_fields)]
+pub struct CreateApiKeyRequest {
+    /// A unique name for the API Key to help you identify it.
+    #[schema(max_length = 64, example = "Sandbox integration key")]
+    pub name: String,
+
+    /// A description to provide more context about the API Key.
+    #[schema(
+        max_length = 256,
+        example = "Key used by our developers to integrate with the sandbox environment"
+    )]
+    pub description: Option<String>,
+
+    /// An expiration date for the API Key. Although we allow keys to never expire, we recommend
+    /// rotating your keys once every 6 months.
+    #[schema(example = "2022-09-10T10:11:12Z")]
+    pub expiration: ApiKeyExpiration,
+}
+
+/// The response body for creating an API Key.
+#[derive(Debug, Serialize, ToSchema)]
+pub struct CreateApiKeyResponse {
+    /// The identifier for the API Key.
+    #[schema(max_length = 64, example = "5hEEqkgJUyuxgSKGArHA4mWSnX")]
+    pub key_id: String,
+
+    /// The identifier for the Merchant Account.
+    #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44")]
+    pub merchant_id: String,
+
+    /// The unique name for the API Key to help you identify it.
+    #[schema(max_length = 64, example = "Sandbox integration key")]
+    pub name: String,
+
+    /// The description to provide more context about the API Key.
+    #[schema(
+        max_length = 256,
+        example = "Key used by our developers to integrate with the sandbox environment"
+    )]
+    pub description: Option<String>,
+
+    /// The plaintext API Key used for server-side API access. Ensure you store the API Key
+    /// securely as you will not be able to see it again.
+    #[schema(value_type = String, max_length = 128)]
+    pub api_key: StrongSecret<String>,
+
+    /// The time at which the API Key was created.
+    #[schema(example = "2022-09-10T10:11:12Z")]
+    #[serde(with = "common_utils::custom_serde::iso8601")]
+    pub created: PrimitiveDateTime,
+
+    /// The expiration date for the API Key.
+    #[schema(example = "2022-09-10T10:11:12Z")]
+    pub expiration: ApiKeyExpiration,
+    /*
+    /// The date and time indicating when the API Key was last used.
+    #[schema(example = "2022-09-10T10:11:12Z")]
+    #[serde(with = "common_utils::custom_serde::iso8601::option")]
+    pub last_used: Option<PrimitiveDateTime>,
+    */
+}
+
+/// The response body for retrieving an API Key.
+#[derive(Debug, Serialize, ToSchema)]
+pub struct RetrieveApiKeyResponse {
+    /// The identifier for the API Key.
+    #[schema(max_length = 64, example = "5hEEqkgJUyuxgSKGArHA4mWSnX")]
+    pub key_id: String,
+
+    /// The identifier for the Merchant Account.
+    #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44")]
+    pub merchant_id: String,
+
+    /// The unique name for the API Key to help you identify it.
+    #[schema(max_length = 64, example = "Sandbox integration key")]
+    pub name: String,
+
+    /// The description to provide more context about the API Key.
+    #[schema(
+        max_length = 256,
+        example = "Key used by our developers to integrate with the sandbox environment"
+    )]
+    pub description: Option<String>,
+
+    /// The first few characters of the plaintext API Key to help you identify it.
+    #[schema(value_type = String, max_length = 64)]
+    pub prefix: StrongSecret<String>,
+
+    /// The time at which the API Key was created.
+    #[schema(example = "2022-09-10T10:11:12Z")]
+    #[serde(with = "common_utils::custom_serde::iso8601")]
+    pub created: PrimitiveDateTime,
+
+    /// The expiration date for the API Key.
+    #[schema(example = "2022-09-10T10:11:12Z")]
+    pub expiration: ApiKeyExpiration,
+    /*
+    /// The date and time indicating when the API Key was last used.
+    #[schema(example = "2022-09-10T10:11:12Z")]
+    #[serde(with = "common_utils::custom_serde::iso8601::option")]
+    pub last_used: Option<PrimitiveDateTime>,
+    */
+}
+
+/// The request body for updating an API Key.
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(deny_unknown_fields)]
+pub struct UpdateApiKeyRequest {
+    /// A unique name for the API Key to help you identify it.
+    #[schema(max_length = 64, example = "Sandbox integration key")]
+    pub name: Option<String>,
+
+    /// A description to provide more context about the API Key.
+    #[schema(
+        max_length = 256,
+        example = "Key used by our developers to integrate with the sandbox environment"
+    )]
+    pub description: Option<String>,
+
+    /// An expiration date for the API Key. Although we allow keys to never expire, we recommend
+    /// rotating your keys once every 6 months.
+    #[schema(example = "2022-09-10T10:11:12Z")]
+    pub expiration: Option<ApiKeyExpiration>,
+}
+
+/// The response body for revoking an API Key.
+#[derive(Debug, Serialize, ToSchema)]
+pub struct RevokeApiKeyResponse {
+    /// The identifier for the API Key.
+    #[schema(max_length = 64, example = "5hEEqkgJUyuxgSKGArHA4mWSnX")]
+    pub key_id: String,
+
+    /// Indicates whether the API key was revoked or not.
+    #[schema(example = "true")]
+    pub revoked: bool,
+}
+
+/// The constraints that are applicable when listing API Keys associated with a merchant account.
+#[derive(Clone, Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct ListApiKeyConstraints {
+    /// The maximum number of API Keys to include in the response.
+    pub limit: Option<i64>,
+
+    /// The number of API Keys to skip when retrieving the list of API keys.
+    pub skip: Option<i64>,
+}
+
+/// The expiration date and time for an API Key.
+#[derive(Debug, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum ApiKeyExpiration {
+    /// The API Key does not expire.
+    #[serde(with = "never")]
+    Never,
+
+    /// The API Key expires at the specified date and time.
+    #[serde(with = "custom_serde::iso8601")]
+    DateTime(PrimitiveDateTime),
+}
+
+impl From<ApiKeyExpiration> for Option<PrimitiveDateTime> {
+    fn from(expiration: ApiKeyExpiration) -> Self {
+        match expiration {
+            ApiKeyExpiration::Never => None,
+            ApiKeyExpiration::DateTime(date_time) => Some(date_time),
+        }
+    }
+}
+
+impl From<Option<PrimitiveDateTime>> for ApiKeyExpiration {
+    fn from(date_time: Option<PrimitiveDateTime>) -> Self {
+        date_time.map_or(Self::Never, Self::DateTime)
+    }
+}
+
+// This implementation is required as otherwise, `serde` would serialize and deserialize
+// `ApiKeyExpiration::Never` as `null`, which is not preferable.
+// Reference: https://github.com/serde-rs/serde/issues/1560#issuecomment-506915291
+mod never {
+    const NEVER: &str = "never";
+
+    pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_str(NEVER)
+    }
+
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        struct NeverVisitor;
+
+        impl<'de> serde::de::Visitor<'de> for NeverVisitor {
+            type Value = ();
+
+            fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                write!(f, r#""{NEVER}""#)
+            }
+
+            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
+                if value == NEVER {
+                    Ok(())
+                } else {
+                    Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
+                }
+            }
+        }
+
+        deserializer.deserialize_str(NeverVisitor)
+    }
+}
+
+impl<'a> ToSchema<'a> for ApiKeyExpiration {
+    fn schema() -> (
+        &'a str,
+        utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
+    ) {
+        use utoipa::openapi::{KnownFormat, ObjectBuilder, OneOfBuilder, SchemaFormat, SchemaType};
+
+        (
+            "ApiKeyExpiration",
+            OneOfBuilder::new()
+                .item(
+                    ObjectBuilder::new()
+                        .schema_type(SchemaType::String)
+                        .enum_values(Some(["never"])),
+                )
+                .item(
+                    ObjectBuilder::new()
+                        .schema_type(SchemaType::String)
+                        .format(Some(SchemaFormat::KnownFormat(KnownFormat::DateTime))),
+                )
+                .into(),
+        )
+    }
+}
+
+#[cfg(test)]
+mod api_key_expiration_tests {
+    #![allow(clippy::unwrap_used)]
+    use super::*;
+
+    #[test]
+    fn test_serialization() {
+        assert_eq!(
+            serde_json::to_string(&ApiKeyExpiration::Never).unwrap(),
+            r#""never""#
+        );
+
+        let date = time::Date::from_calendar_date(2022, time::Month::September, 10).unwrap();
+        let time = time::Time::from_hms(11, 12, 13).unwrap();
+        assert_eq!(
+            serde_json::to_string(&ApiKeyExpiration::DateTime(time::PrimitiveDateTime::new(
+                date, time
+            )))
+            .unwrap(),
+            r#""2022-09-10T11:12:13.000Z""#
+        );
+    }
+
+    #[test]
+    fn test_deserialization() {
+        assert_eq!(
+            serde_json::from_str::<ApiKeyExpiration>(r#""never""#).unwrap(),
+            ApiKeyExpiration::Never
+        );
+
+        let date = time::Date::from_calendar_date(2022, time::Month::September, 10).unwrap();
+        let time = time::Time::from_hms(11, 12, 13).unwrap();
+        assert_eq!(
+            serde_json::from_str::<ApiKeyExpiration>(r#""2022-09-10T11:12:13.000Z""#).unwrap(),
+            ApiKeyExpiration::DateTime(time::PrimitiveDateTime::new(date, time))
+        );
+    }
+
+    #[test]
+    fn test_null() {
+        let result = serde_json::from_str::<ApiKeyExpiration>("null");
+        assert!(result.is_err());
+
+        let result = serde_json::from_str::<Option<ApiKeyExpiration>>("null").unwrap();
+        assert_eq!(result, None);
+    }
+}
diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs
index 84453c1d08a..e7f77552c3d 100644
--- a/crates/api_models/src/lib.rs
+++ b/crates/api_models/src/lib.rs
@@ -1,5 +1,6 @@
 #![forbid(unsafe_code)]
 pub mod admin;
+pub mod api_keys;
 pub mod bank_accounts;
 pub mod cards;
 pub mod customers;
diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml
index e041d22d139..5ab2eb69870 100644
--- a/crates/common_utils/Cargo.toml
+++ b/crates/common_utils/Cargo.toml
@@ -15,6 +15,7 @@ futures = "0.3.25"
 hex = "0.4.3"
 nanoid = "0.4.0"
 once_cell = "1.17.0"
+rand = "0.8.5"
 regex = "1.7.1"
 ring = "0.16.20"
 serde = { version = "1.0.152", features = ["derive"] }
diff --git a/crates/common_utils/src/crypto.rs b/crates/common_utils/src/crypto.rs
index da21a317c59..d63aed438cf 100644
--- a/crates/common_utils/src/crypto.rs
+++ b/crates/common_utils/src/crypto.rs
@@ -232,6 +232,26 @@ impl GenerateDigest for Sha512 {
     }
 }
 
+/// Generate a random string using a cryptographically secure pseudo-random number generator
+/// (CSPRNG). Typically used for generating (readable) keys and passwords.
+#[inline]
+pub fn generate_cryptographically_secure_random_string(length: usize) -> String {
+    use rand::distributions::DistString;
+
+    rand::distributions::Alphanumeric.sample_string(&mut rand::rngs::OsRng, length)
+}
+
+/// Generate an array of random bytes using a cryptographically secure pseudo-random number
+/// generator (CSPRNG). Typically used for generating keys.
+#[inline]
+pub fn generate_cryptographically_secure_random_bytes<const N: usize>() -> [u8; N] {
+    use rand::RngCore;
+
+    let mut bytes = [0; N];
+    rand::rngs::OsRng.fill_bytes(&mut bytes);
+    bytes
+}
+
 #[cfg(test)]
 mod crypto_tests {
     #![allow(clippy::expect_used)]
diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml
index 0e597e91e67..277df67584b 100644
--- a/crates/router/Cargo.toml
+++ b/crates/router/Cargo.toml
@@ -34,6 +34,7 @@ aws-config = { version = "0.54.1", optional = true }
 aws-sdk-kms = { version = "0.24.0", optional = true }
 base64 = "0.21.0"
 bb8 = "0.8"
+blake3 = "1.3.3"
 bytes = "1.3.0"
 clap = { version = "4.1.4", default-features = false, features = ["std", "derive", "help", "usage"] }
 config = { version = "0.13.3", features = ["toml"] }
diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs
index 86e4583651f..9ec5a5891a4 100644
--- a/crates/router/src/compatibility/stripe/errors.rs
+++ b/crates/router/src/compatibility/stripe/errors.rs
@@ -94,6 +94,9 @@ pub enum StripeErrorCode {
     #[error(error_type = StripeErrorType::InvalidRequestError, code = "resource_missing", message = "No such mandate")]
     MandateNotFound,
 
+    #[error(error_type = StripeErrorType::InvalidRequestError, code = "resource_missing", message = "No such API key")]
+    ApiKeyNotFound,
+
     #[error(error_type = StripeErrorType::InvalidRequestError, code = "parameter_missing", message = "Return url is not available")]
     ReturnUrlUnavailable,
 
@@ -383,6 +386,7 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
                 Self::MerchantConnectorAccountNotFound
             }
             errors::ApiErrorResponse::MandateNotFound => Self::MandateNotFound,
+            errors::ApiErrorResponse::ApiKeyNotFound => Self::ApiKeyNotFound,
             errors::ApiErrorResponse::MandateValidationFailed { reason } => {
                 Self::PaymentIntentMandateInvalid { message: reason }
             }
@@ -453,6 +457,7 @@ impl actix_web::ResponseError for StripeErrorCode {
             | Self::MerchantAccountNotFound
             | Self::MerchantConnectorAccountNotFound
             | Self::MandateNotFound
+            | Self::ApiKeyNotFound
             | Self::DuplicateMerchantAccount
             | Self::DuplicateMerchantConnectorAccount
             | Self::DuplicatePaymentMethod
diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs
index 3d30069981e..2dcc711b3ef 100644
--- a/crates/router/src/consts.rs
+++ b/crates/router/src/consts.rs
@@ -16,9 +16,10 @@ pub const REQUEST_TIME_OUT: u64 = 30;
 pub(crate) const NO_ERROR_MESSAGE: &str = "No error message";
 pub(crate) const NO_ERROR_CODE: &str = "No error code";
 
-// General purpose base64 engine
+// General purpose base64 engines
 pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose =
     base64::engine::general_purpose::STANDARD;
-
 pub(crate) const BASE64_ENGINE_URL_SAFE: base64::engine::GeneralPurpose =
     base64::engine::general_purpose::URL_SAFE;
+
+pub(crate) const API_KEY_LENGTH: usize = 64;
diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs
index 5d68f5d30a8..629838deb17 100644
--- a/crates/router/src/core.rs
+++ b/crates/router/src/core.rs
@@ -1,4 +1,5 @@
 pub mod admin;
+pub mod api_keys;
 pub mod configs;
 pub mod customers;
 pub mod errors;
diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs
index ffe9fdb9370..3adcd88e127 100644
--- a/crates/router/src/core/admin.rs
+++ b/crates/router/src/core/admin.rs
@@ -6,7 +6,6 @@ use crate::{
     consts,
     core::errors::{self, RouterResponse, RouterResult, StorageErrorExt},
     db::StorageInterface,
-    env::{self, Env},
     pii::Secret,
     services::api as service_api,
     types::{
@@ -19,12 +18,11 @@ use crate::{
 
 #[inline]
 pub fn create_merchant_api_key() -> String {
-    let id = Uuid::new_v4().simple();
-    match env::which() {
-        Env::Development => format!("dev_{id}"),
-        Env::Production => format!("prd_{id}"),
-        Env::Sandbox => format!("snd_{id}"),
-    }
+    format!(
+        "{}_{}",
+        router_env::env::prefix_for_env(),
+        Uuid::new_v4().simple()
+    )
 }
 
 pub async fn create_merchant_account(
diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs
new file mode 100644
index 00000000000..22023174bdf
--- /dev/null
+++ b/crates/router/src/core/api_keys.rs
@@ -0,0 +1,219 @@
+use common_utils::{date_time, errors::CustomResult, fp_utils};
+use error_stack::{report, IntoReport, ResultExt};
+use masking::{PeekInterface, Secret};
+use router_env::{instrument, tracing};
+
+use crate::{
+    consts,
+    core::errors::{self, RouterResponse, StorageErrorExt},
+    db::StorageInterface,
+    services::ApplicationResponse,
+    types::{api, storage, transformers::ForeignInto},
+    utils,
+};
+
+// Defining new types `PlaintextApiKey` and `HashedApiKey` in the hopes of reducing the possibility
+// of plaintext API key being stored in the data store.
+pub struct PlaintextApiKey(Secret<String>);
+pub struct HashedApiKey(String);
+
+impl PlaintextApiKey {
+    const HASH_KEY_LEN: usize = 32;
+
+    const PREFIX_LEN: usize = 8;
+
+    pub fn new(length: usize) -> Self {
+        let key = common_utils::crypto::generate_cryptographically_secure_random_string(length);
+        Self(key.into())
+    }
+
+    pub fn new_hash_key() -> [u8; Self::HASH_KEY_LEN] {
+        common_utils::crypto::generate_cryptographically_secure_random_bytes()
+    }
+
+    pub fn new_key_id() -> String {
+        let env = router_env::env::prefix_for_env();
+        utils::generate_id(consts::ID_LENGTH, env)
+    }
+
+    pub fn prefix(&self) -> String {
+        self.0.peek().chars().take(Self::PREFIX_LEN).collect()
+    }
+
+    pub fn peek(&self) -> &str {
+        self.0.peek()
+    }
+
+    pub fn keyed_hash(&self, key: &[u8; Self::HASH_KEY_LEN]) -> HashedApiKey {
+        /*
+        Decisions regarding API key hashing algorithm chosen:
+
+        - Since API key hash verification would be done for each request, there is a requirement
+          for the hashing to be quick.
+        - Password hashing algorithms would not be suitable for this purpose as they're designed to
+          prevent brute force attacks, considering that the same password could be shared  across
+          multiple sites by the user.
+        - Moreover, password hash verification happens once per user session, so the delay involved
+          is negligible, considering the security benefits it provides.
+          While with API keys (assuming uniqueness of keys across the application), the delay
+          involved in hashing (with the use of a password hashing algorithm) becomes significant,
+          considering that it must be done per request.
+        - Since we are the only ones generating API keys and are able to guarantee their uniqueness,
+          a simple hash algorithm is sufficient for this purpose.
+
+        Hash algorithms considered:
+        - Password hashing algorithms: Argon2id and PBKDF2
+        - Simple hashing algorithms: HMAC-SHA256, HMAC-SHA512, BLAKE3
+
+        After benchmarking the simple hashing algorithms, we decided to go with the BLAKE3 keyed
+        hashing algorithm, with a randomly generated key for the hash key.
+        */
+
+        HashedApiKey(
+            blake3::keyed_hash(key, self.0.peek().as_bytes())
+                .to_hex()
+                .to_string(),
+        )
+    }
+
+    pub fn verify_hash(
+        &self,
+        key: &[u8; Self::HASH_KEY_LEN],
+        stored_api_key: &HashedApiKey,
+    ) -> CustomResult<(), errors::ApiKeyError> {
+        // Converting both hashes to `blake3::Hash` since it provides constant-time equality checks
+        let provided_api_key_hash = blake3::keyed_hash(key, self.0.peek().as_bytes());
+        let stored_api_key_hash = blake3::Hash::from_hex(&stored_api_key.0)
+            .into_report()
+            .change_context(errors::ApiKeyError::FailedToReadHashFromHex)?;
+
+        fp_utils::when(provided_api_key_hash != stored_api_key_hash, || {
+            Err(errors::ApiKeyError::HashVerificationFailed).into_report()
+        })
+    }
+}
+
+#[instrument(skip_all)]
+pub async fn create_api_key(
+    store: &dyn StorageInterface,
+    api_key: api::CreateApiKeyRequest,
+    merchant_id: String,
+) -> RouterResponse<api::CreateApiKeyResponse> {
+    let hash_key = PlaintextApiKey::new_hash_key();
+    let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
+    let api_key = storage::ApiKeyNew {
+        key_id: PlaintextApiKey::new_key_id(),
+        merchant_id,
+        name: api_key.name,
+        description: api_key.description,
+        hash_key: Secret::from(hex::encode(hash_key)),
+        hashed_api_key: plaintext_api_key.keyed_hash(&hash_key).into(),
+        prefix: plaintext_api_key.prefix(),
+        created_at: date_time::now(),
+        expires_at: api_key.expiration.into(),
+        last_used: None,
+    };
+
+    let api_key = store
+        .insert_api_key(api_key)
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable("Failed to insert new API key")?;
+
+    Ok(ApplicationResponse::Json(
+        (api_key, plaintext_api_key).foreign_into(),
+    ))
+}
+
+#[instrument(skip_all)]
+pub async fn retrieve_api_key(
+    store: &dyn StorageInterface,
+    key_id: &str,
+) -> RouterResponse<api::RetrieveApiKeyResponse> {
+    let api_key = store
+        .find_api_key_optional(key_id)
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
+        .attach_printable("Failed to retrieve new API key")?
+        .ok_or(report!(errors::ApiErrorResponse::ApiKeyNotFound))?; // If retrieve returned `None`
+
+    Ok(ApplicationResponse::Json(api_key.foreign_into()))
+}
+
+#[instrument(skip_all)]
+pub async fn update_api_key(
+    store: &dyn StorageInterface,
+    key_id: &str,
+    api_key: api::UpdateApiKeyRequest,
+) -> RouterResponse<api::RetrieveApiKeyResponse> {
+    let api_key = store
+        .update_api_key(key_id.to_owned(), api_key.foreign_into())
+        .await
+        .map_err(|err| err.to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound))?;
+
+    Ok(ApplicationResponse::Json(api_key.foreign_into()))
+}
+
+#[instrument(skip_all)]
+pub async fn revoke_api_key(
+    store: &dyn StorageInterface,
+    key_id: &str,
+) -> RouterResponse<api::RevokeApiKeyResponse> {
+    let revoked = store
+        .revoke_api_key(key_id)
+        .await
+        .map_err(|err| err.to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound))?;
+
+    Ok(ApplicationResponse::Json(api::RevokeApiKeyResponse {
+        key_id: key_id.to_owned(),
+        revoked,
+    }))
+}
+
+#[instrument(skip_all)]
+pub async fn list_api_keys(
+    store: &dyn StorageInterface,
+    merchant_id: String,
+    limit: Option<i64>,
+    offset: Option<i64>,
+) -> RouterResponse<Vec<api::RetrieveApiKeyResponse>> {
+    let api_keys = store
+        .list_api_keys_by_merchant_id(&merchant_id, limit, offset)
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable("Failed to list merchant API keys")?;
+    let api_keys = api_keys
+        .into_iter()
+        .map(ForeignInto::foreign_into)
+        .collect();
+
+    Ok(ApplicationResponse::Json(api_keys))
+}
+
+impl From<HashedApiKey> for storage::HashedApiKey {
+    fn from(hashed_api_key: HashedApiKey) -> Self {
+        hashed_api_key.0.into()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    #![allow(clippy::unwrap_used)]
+    use super::*;
+
+    #[test]
+    fn test_hashing_and_verification() {
+        let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
+        let hash_key = PlaintextApiKey::new_hash_key();
+        let hashed_api_key = plaintext_api_key.keyed_hash(&hash_key);
+
+        assert_ne!(
+            plaintext_api_key.0.peek().as_bytes(),
+            hashed_api_key.0.as_bytes()
+        );
+
+        plaintext_api_key
+            .verify_hash(&hash_key, &hashed_api_key)
+            .unwrap();
+    }
+}
diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs
index d7532c11528..d83f05bb28b 100644
--- a/crates/router/src/core/errors.rs
+++ b/crates/router/src/core/errors.rs
@@ -390,3 +390,11 @@ pub enum WebhooksFlowError {
     #[error("Webhook not received by merchant")]
     NotReceivedByMerchant,
 }
+
+#[derive(Debug, thiserror::Error)]
+pub enum ApiKeyError {
+    #[error("Failed to read API key hash from hexadecimal string")]
+    FailedToReadHashFromHex,
+    #[error("Failed to verify provided API key hash against stored API key hash")]
+    HashVerificationFailed,
+}
diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs
index 105ebb7ff82..ec49c7a8410 100644
--- a/crates/router/src/core/errors/api_error_response.rs
+++ b/crates/router/src/core/errors/api_error_response.rs
@@ -133,6 +133,8 @@ pub enum ApiErrorResponse {
     ResourceIdNotFound,
     #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Mandate does not exist in our records")]
     MandateNotFound,
+    #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "API Key does not exist in our records")]
+    ApiKeyNotFound,
     #[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "Return URL is not configured and not passed in payments request")]
     ReturnUrlUnavailable,
     #[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "This refund is not possible through Hyperswitch. Please raise the refund through {connector} dashboard")]
@@ -231,7 +233,8 @@ impl actix_web::ResponseError for ApiErrorResponse {
             | Self::IncorrectConnectorNameGiven
             | Self::ResourceIdNotFound
             | Self::ConfigNotFound
-            | Self::AddressNotFound => StatusCode::BAD_REQUEST, // 400
+            | Self::AddressNotFound
+            | Self::ApiKeyNotFound => StatusCode::BAD_REQUEST, // 400
             Self::DuplicateMerchantAccount
             | Self::DuplicateMerchantConnectorAccount
             | Self::DuplicatePaymentMethod
diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs
index 6c1901aec33..a0a4bb45c2b 100644
--- a/crates/router/src/db.rs
+++ b/crates/router/src/db.rs
@@ -1,4 +1,5 @@
 pub mod address;
+pub mod api_keys;
 pub mod cache;
 pub mod configs;
 pub mod connector_response;
@@ -35,23 +36,24 @@ pub trait StorageInterface:
     Send
     + Sync
     + dyn_clone::DynClone
-    + payment_attempt::PaymentAttemptInterface
-    + mandate::MandateInterface
     + address::AddressInterface
+    + api_keys::ApiKeyInterface
     + configs::ConfigInterface
+    + connector_response::ConnectorResponseInterface
     + customers::CustomerInterface
+    + ephemeral_key::EphemeralKeyInterface
     + events::EventInterface
+    + locker_mock_up::LockerMockUpInterface
+    + mandate::MandateInterface
     + merchant_account::MerchantAccountInterface
-    + merchant_connector_account::MerchantConnectorAccountInterface
     + merchant_connector_account::ConnectorAccessToken
-    + locker_mock_up::LockerMockUpInterface
+    + merchant_connector_account::MerchantConnectorAccountInterface
+    + payment_attempt::PaymentAttemptInterface
     + payment_intent::PaymentIntentInterface
     + payment_method::PaymentMethodInterface
     + process_tracker::ProcessTrackerInterface
-    + refund::RefundInterface
     + queue::QueueInterface
-    + ephemeral_key::EphemeralKeyInterface
-    + connector_response::ConnectorResponseInterface
+    + refund::RefundInterface
     + reverse_lookup::ReverseLookupInterface
     + 'static
 {
diff --git a/crates/router/src/db/api_keys.rs b/crates/router/src/db/api_keys.rs
new file mode 100644
index 00000000000..68d99eb0896
--- /dev/null
+++ b/crates/router/src/db/api_keys.rs
@@ -0,0 +1,138 @@
+use error_stack::IntoReport;
+
+use super::{MockDb, Store};
+use crate::{
+    connection::pg_connection,
+    core::errors::{self, CustomResult},
+    types::storage,
+};
+
+#[async_trait::async_trait]
+pub trait ApiKeyInterface {
+    async fn insert_api_key(
+        &self,
+        api_key: storage::ApiKeyNew,
+    ) -> CustomResult<storage::ApiKey, errors::StorageError>;
+
+    async fn update_api_key(
+        &self,
+        key_id: String,
+        api_key: storage::ApiKeyUpdate,
+    ) -> CustomResult<storage::ApiKey, errors::StorageError>;
+
+    async fn revoke_api_key(&self, key_id: &str) -> CustomResult<bool, errors::StorageError>;
+
+    async fn find_api_key_optional(
+        &self,
+        key_id: &str,
+    ) -> CustomResult<Option<storage::ApiKey>, errors::StorageError>;
+
+    async fn list_api_keys_by_merchant_id(
+        &self,
+        merchant_id: &str,
+        limit: Option<i64>,
+        offset: Option<i64>,
+    ) -> CustomResult<Vec<storage::ApiKey>, errors::StorageError>;
+}
+
+#[async_trait::async_trait]
+impl ApiKeyInterface for Store {
+    async fn insert_api_key(
+        &self,
+        api_key: storage::ApiKeyNew,
+    ) -> CustomResult<storage::ApiKey, errors::StorageError> {
+        let conn = pg_connection(&self.master_pool).await;
+        api_key
+            .insert(&conn)
+            .await
+            .map_err(Into::into)
+            .into_report()
+    }
+
+    async fn update_api_key(
+        &self,
+        key_id: String,
+        api_key: storage::ApiKeyUpdate,
+    ) -> CustomResult<storage::ApiKey, errors::StorageError> {
+        let conn = pg_connection(&self.master_pool).await;
+        storage::ApiKey::update_by_key_id(&conn, key_id, api_key)
+            .await
+            .map_err(Into::into)
+            .into_report()
+    }
+
+    async fn revoke_api_key(&self, key_id: &str) -> CustomResult<bool, errors::StorageError> {
+        let conn = pg_connection(&self.master_pool).await;
+        storage::ApiKey::revoke_by_key_id(&conn, key_id)
+            .await
+            .map_err(Into::into)
+            .into_report()
+    }
+
+    async fn find_api_key_optional(
+        &self,
+        key_id: &str,
+    ) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
+        let conn = pg_connection(&self.master_pool).await;
+        storage::ApiKey::find_optional_by_key_id(&conn, key_id)
+            .await
+            .map_err(Into::into)
+            .into_report()
+    }
+
+    async fn list_api_keys_by_merchant_id(
+        &self,
+        merchant_id: &str,
+        limit: Option<i64>,
+        offset: Option<i64>,
+    ) -> CustomResult<Vec<storage::ApiKey>, errors::StorageError> {
+        let conn = pg_connection(&self.master_pool).await;
+        storage::ApiKey::find_by_merchant_id(&conn, merchant_id, limit, offset)
+            .await
+            .map_err(Into::into)
+            .into_report()
+    }
+}
+
+#[async_trait::async_trait]
+impl ApiKeyInterface for MockDb {
+    async fn insert_api_key(
+        &self,
+        _api_key: storage::ApiKeyNew,
+    ) -> CustomResult<storage::ApiKey, errors::StorageError> {
+        // [#172]: Implement function for `MockDb`
+        Err(errors::StorageError::MockDbError)?
+    }
+
+    async fn update_api_key(
+        &self,
+        _key_id: String,
+        _api_key: storage::ApiKeyUpdate,
+    ) -> CustomResult<storage::ApiKey, errors::StorageError> {
+        // [#172]: Implement function for `MockDb`
+        Err(errors::StorageError::MockDbError)?
+    }
+
+    async fn revoke_api_key(&self, _key_id: &str) -> CustomResult<bool, errors::StorageError> {
+        // [#172]: Implement function for `MockDb`
+        Err(errors::StorageError::MockDbError)?
+    }
+
+    async fn find_api_key_optional(
+        &self,
+        _key_id: &str,
+    ) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
+        // [#172]: Implement function for `MockDb`
+        Err(errors::StorageError::MockDbError)?
+    }
+
+    async fn list_api_keys_by_merchant_id(
+        &self,
+        _merchant_id: &str,
+        _limit: Option<i64>,
+        _offset: Option<i64>,
+    ) -> CustomResult<Vec<storage::ApiKey>, errors::StorageError> {
+        // [#172]: Implement function for `MockDb`
+        Err(errors::StorageError::MockDbError)?
+    }
+}
diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs
index e10e6b81b30..28f7680992b 100644
--- a/crates/router/src/lib.rs
+++ b/crates/router/src/lib.rs
@@ -49,6 +49,7 @@ pub mod headers {
     pub const ACCEPT: &str = "Accept";
     pub const X_API_VERSION: &str = "X-ApiVersion";
     pub const DATE: &str = "Date";
+    pub const X_MERCHANT_ID: &str = "X-Merchant-Id";
 }
 
 pub mod pii {
@@ -95,7 +96,9 @@ pub fn mk_app(
 
     #[cfg(feature = "olap")]
     {
-        server_app = server_app.service(routes::MerchantAccount::server(state.clone()));
+        server_app = server_app
+            .service(routes::MerchantAccount::server(state.clone()))
+            .service(routes::ApiKeys::server(state.clone()));
     }
 
     #[cfg(feature = "stripe")]
diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs
index 5711e433ef3..d158c5316f1 100644
--- a/crates/router/src/openapi.rs
+++ b/crates/router/src/openapi.rs
@@ -17,7 +17,10 @@ Our APIs accept and return JSON in the HTTP body, and return standard HTTP respo
 You can consume the APIs directly using your favorite HTTP/REST library.
 
 We have a testing environment referred to "sandbox", which you can setup to test API calls without
-affecting production data. Currently, our sandbox environment is live while our production environment is under development and will be available soon. You can sign up on our Dashboard to get API keys to access Hyperswitch API.
+affecting production data.
+Currently, our sandbox environment is live while our production environment is under development
+and will be available soon.
+You can sign up on our Dashboard to get API keys to access Hyperswitch API.
 
 ### Environment
 
@@ -32,13 +35,13 @@ Use the following base URLs when making requests to the APIs:
 
 When you sign up on our [dashboard](https://app.hyperswitch.io) and create a merchant
 account, you are given a secret key (also referred as api-key) and a publishable key.
-You may authenticate all API requests with Hyperswitch server by providing the appropriate key in the
-request Authorization header.
+You may authenticate all API requests with Hyperswitch server by providing the appropriate key in
+the request Authorization header.
 
 | Key           |  Description                                                                                  |
 |---------------|-----------------------------------------------------------------------------------------------|
 | Sandbox       | Private key. Used to authenticate all API requests from your merchant server                  |
-| Production    | Unique identifier for your account. Used to authenticate API requests from your app’s client  |
+| Production    | Unique identifier for your account. Used to authenticate API requests from your app's client  |
 
 Never share your secret api keys. Keep them guarded and secure.
 "#,
@@ -47,13 +50,14 @@ Never share your secret api keys. Keep them guarded and secure.
         (url = "https://sandbox.hyperswitch.io", description = "Sandbox Environment")
     ),
     tags(
-        (name = "Merchant Account"),// , description = "Create and manage merchant accounts"),
-        (name = "Merchant Connector Account"),// , description = "Create and manage merchant connector accounts"),
-        (name = "Payments"),// , description = "Create and manage one-time payments, recurring payments and mandates"),
-        (name = "Refunds"),// , description = "Create and manage refunds for successful payments"),
-        (name = "Mandates"),// , description = "Manage mandates"),
-        (name = "Customers"),// , description = "Create and manage customers"),
-        (name = "Payment Methods")// , description = "Create and manage payment methods of customers")
+        (name = "Merchant Account", description = "Create and manage merchant accounts"),
+        (name = "Merchant Connector Account", description = "Create and manage merchant connector accounts"),
+        (name = "Payments", description = "Create and manage one-time payments, recurring payments and mandates"),
+        (name = "Refunds", description = "Create and manage refunds for successful payments"),
+        (name = "Mandates", description = "Manage mandates"),
+        (name = "Customers", description = "Create and manage customers"),
+        (name = "Payment Methods", description = "Create and manage payment methods of customers"),
+        (name = "API Key", description = "Create and manage API Keys"),
     ),
     paths(
         crate::routes::refunds::refunds_create,
@@ -92,6 +96,11 @@ Never share your secret api keys. Keep them guarded and secure.
         crate::routes::customers::customers_retrieve,
         crate::routes::customers::customers_update,
         crate::routes::customers::customers_delete,
+        crate::routes::api_keys::api_key_create,
+        crate::routes::api_keys::api_key_retrieve,
+        crate::routes::api_keys::api_key_update,
+        crate::routes::api_keys::api_key_revoke,
+        crate::routes::api_keys::api_key_list,
     ),
     components(schemas(
         crate::types::api::refunds::RefundRequest,
@@ -181,6 +190,12 @@ Never share your secret api keys. Keep them guarded and secure.
         crate::types::api::admin::MerchantConnectorId,
         crate::types::api::admin::MerchantDetails,
         crate::types::api::admin::WebhookDetails,
+        crate::types::api::api_keys::ApiKeyExpiration,
+        crate::types::api::api_keys::CreateApiKeyRequest,
+        crate::types::api::api_keys::CreateApiKeyResponse,
+        crate::types::api::api_keys::RetrieveApiKeyResponse,
+        crate::types::api::api_keys::RevokeApiKeyResponse,
+        crate::types::api::api_keys::UpdateApiKeyRequest
     ))
 )]
 pub struct ApiDoc;
diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs
index d40d7484068..99cf2fb7a8f 100644
--- a/crates/router/src/routes.rs
+++ b/crates/router/src/routes.rs
@@ -1,4 +1,5 @@
 pub mod admin;
+pub mod api_keys;
 pub mod app;
 pub mod configs;
 pub mod customers;
@@ -13,7 +14,7 @@ pub mod refunds;
 pub mod webhooks;
 
 pub use self::app::{
-    AppState, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount,
+    ApiKeys, AppState, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount,
     MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, Webhooks,
 };
 #[cfg(feature = "stripe")]
diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs
new file mode 100644
index 00000000000..202f599ba6f
--- /dev/null
+++ b/crates/router/src/routes/api_keys.rs
@@ -0,0 +1,209 @@
+use actix_web::{web, HttpRequest, Responder};
+use error_stack::{IntoReport, ResultExt};
+use router_env::{instrument, tracing, Flow};
+
+use super::app::AppState;
+use crate::{
+    core::{
+        api_keys,
+        errors::{self, RouterResult},
+    },
+    services::{api, authentication as auth},
+    types::api as api_types,
+};
+
+/// API Key - Create
+///
+/// Create a new API Key for accessing our APIs from your servers. The plaintext API Key will be
+/// displayed only once on creation, so ensure you store it securely.
+#[utoipa::path(
+    post,
+    path = "/api_keys",
+    request_body= CreateApiKeyRequest,
+    responses(
+        (status = 200, description = "API Key created", body = CreateApiKeyResponse),
+        (status = 400, description = "Invalid data")
+    ),
+    tag = "API Key",
+    operation_id = "Create an API Key"
+)]
+#[instrument(skip_all, fields(flow = ?Flow::ApiKeyCreate))]
+pub async fn api_key_create(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<api_types::CreateApiKeyRequest>,
+) -> impl Responder {
+    let payload = json_payload.into_inner();
+
+    api::server_wrap(
+        state.get_ref(),
+        &req,
+        payload,
+        |state, _, payload| async {
+            let merchant_id = get_merchant_id_header(&req)?;
+            api_keys::create_api_key(&*state.store, payload, merchant_id).await
+        },
+        &auth::AdminApiAuth,
+    )
+    .await
+}
+
+/// API Key - Retrieve
+///
+/// Retrieve information about the specified API Key.
+#[utoipa::path(
+    get,
+    path = "/api_keys/{key_id}",
+    params (("key_id" = String, Path, description = "The unique identifier for the API Key")),
+    responses(
+        (status = 200, description = "API Key retrieved", body = RetrieveApiKeyResponse),
+        (status = 404, description = "API Key not found")
+    ),
+    tag = "API Key",
+    operation_id = "Retrieve an API Key"
+)]
+#[instrument(skip_all, fields(flow = ?Flow::ApiKeyRetrieve))]
+pub async fn api_key_retrieve(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    path: web::Path<String>,
+) -> impl Responder {
+    let key_id = path.into_inner();
+
+    api::server_wrap(
+        state.get_ref(),
+        &req,
+        &key_id,
+        |state, _, key_id| api_keys::retrieve_api_key(&*state.store, key_id),
+        &auth::AdminApiAuth,
+    )
+    .await
+}
+
+/// API Key - Update
+///
+/// Update information for the specified API Key.
+#[utoipa::path(
+    post,
+    path = "/api_keys/{key_id}",
+    request_body = UpdateApiKeyRequest,
+    params (("key_id" = String, Path, description = "The unique identifier for the API Key")),
+    responses(
+        (status = 200, description = "API Key updated", body = RetrieveApiKeyResponse),
+        (status = 404, description = "API Key not found")
+    ),
+    tag = "API Key",
+    operation_id = "Update an API Key"
+)]
+#[instrument(skip_all, fields(flow = ?Flow::ApiKeyUpdate))]
+pub async fn api_key_update(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    path: web::Path<String>,
+    json_payload: web::Json<api_types::UpdateApiKeyRequest>,
+) -> impl Responder {
+    let key_id = path.into_inner();
+    let payload = json_payload.into_inner();
+
+    api::server_wrap(
+        state.get_ref(),
+        &req,
+        (&key_id, payload),
+        |state, _, (key_id, payload)| api_keys::update_api_key(&*state.store, key_id, payload),
+        &auth::AdminApiAuth,
+    )
+    .await
+}
+
+/// API Key - Revoke
+///
+/// Revoke the specified API Key. Once revoked, the API Key can no longer be used for
+/// authenticating with our APIs.
+#[utoipa::path(
+    delete,
+    path = "/api_keys/{key_id}",
+    params (("key_id" = String, Path, description = "The unique identifier for the API Key")),
+    responses(
+        (status = 200, description = "API Key revoked", body = RevokeApiKeyResponse),
+        (status = 404, description = "API Key not found")
+    ),
+    tag = "API Key",
+    operation_id = "Revoke an API Key"
+)]
+#[instrument(skip_all, fields(flow = ?Flow::ApiKeyRevoke))]
+pub async fn api_key_revoke(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    path: web::Path<String>,
+) -> impl Responder {
+    let key_id = path.into_inner();
+
+    api::server_wrap(
+        state.get_ref(),
+        &req,
+        &key_id,
+        |state, _, key_id| api_keys::revoke_api_key(&*state.store, key_id),
+        &auth::AdminApiAuth,
+    )
+    .await
+}
+
+/// API Key - List
+///
+/// List all API Keys associated with your merchant account.
+#[utoipa::path(
+    get,
+    path = "/api_keys/list",
+    params(
+        ("limit" = Option<i64>, Query, description = "The maximum number of API Keys to include in the response"),
+        ("skip" = Option<i64>, Query, description = "The number of API Keys to skip when retrieving the list of API keys."),
+    ),
+    responses(
+        (status = 200, description = "List of API Keys retrieved successfully", body = Vec<RetrieveApiKeyResponse>),
+    ),
+    tag = "API Key",
+    operation_id = "List all API Keys associated with a merchant account"
+)]
+#[instrument(skip_all, fields(flow = ?Flow::ApiKeyList))]
+pub async fn api_key_list(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    query: web::Query<api_types::ListApiKeyConstraints>,
+) -> impl Responder {
+    let list_api_key_constraints = query.into_inner();
+    let limit = list_api_key_constraints.limit;
+    let offset = list_api_key_constraints.skip;
+
+    api::server_wrap(
+        state.get_ref(),
+        &req,
+        (&req, limit, offset),
+        |state, _, (req, limit, offset)| async move {
+            let merchant_id = get_merchant_id_header(req)?;
+            api_keys::list_api_keys(&*state.store, merchant_id, limit, offset).await
+        },
+        &auth::AdminApiAuth,
+    )
+    .await
+}
+
+fn get_merchant_id_header(req: &HttpRequest) -> RouterResult<String> {
+    use crate::headers::X_MERCHANT_ID;
+
+    req.headers()
+        .get(X_MERCHANT_ID)
+        .ok_or_else(|| errors::ApiErrorResponse::InvalidRequestData {
+            message: format!("Missing header: `{X_MERCHANT_ID}`"),
+        })
+        .into_report()?
+        .to_str()
+        .into_report()
+        .change_context(errors::ApiErrorResponse::InvalidDataValue {
+            field_name: X_MERCHANT_ID,
+        })
+        .attach_printable(
+            "Failed to convert header value to string, \
+             possibly contains non-printable or non-ASCII characters",
+        )
+        .map(|s| s.to_owned())
+}
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 8089d9c969e..df5ab8b890b 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1,8 +1,8 @@
 use actix_web::{web, Scope};
 
-#[cfg(feature = "olap")]
-use super::admin::*;
 use super::health::*;
+#[cfg(feature = "olap")]
+use super::{admin::*, api_keys::*};
 #[cfg(any(feature = "olap", feature = "oltp"))]
 use super::{configs::*, customers::*, mandates::*, payments::*, payouts::*, refunds::*};
 #[cfg(feature = "oltp")]
@@ -329,3 +329,21 @@ impl Configs {
             )
     }
 }
+
+pub struct ApiKeys;
+
+#[cfg(feature = "olap")]
+impl ApiKeys {
+    pub fn server(state: AppState) -> Scope {
+        web::scope("/api_keys")
+            .app_data(web::Data::new(state))
+            .service(web::resource("").route(web::post().to(api_key_create)))
+            .service(web::resource("/list").route(web::get().to(api_key_list)))
+            .service(
+                web::resource("/{key_id}")
+                    .route(web::get().to(api_key_retrieve))
+                    .route(web::post().to(api_key_update))
+                    .route(web::delete().to(api_key_revoke)),
+            )
+    }
+}
diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs
index 0d9603e275a..8493d80bf1c 100644
--- a/crates/router/src/types/api.rs
+++ b/crates/router/src/types/api.rs
@@ -1,4 +1,5 @@
 pub mod admin;
+pub mod api_keys;
 pub mod configs;
 pub mod customers;
 pub mod enums;
@@ -13,7 +14,8 @@ use std::{fmt::Debug, str::FromStr};
 use error_stack::{report, IntoReport, ResultExt};
 
 pub use self::{
-    admin::*, configs::*, customers::*, payment_methods::*, payments::*, refunds::*, webhooks::*,
+    admin::*, api_keys::*, configs::*, customers::*, payment_methods::*, payments::*, refunds::*,
+    webhooks::*,
 };
 use super::ErrorResponse;
 use crate::{
diff --git a/crates/router/src/types/api/api_keys.rs b/crates/router/src/types/api/api_keys.rs
new file mode 100644
index 00000000000..0db79dae126
--- /dev/null
+++ b/crates/router/src/types/api/api_keys.rs
@@ -0,0 +1,4 @@
+pub use api_models::api_keys::{
+    ApiKeyExpiration, CreateApiKeyRequest, CreateApiKeyResponse, ListApiKeyConstraints,
+    RetrieveApiKeyResponse, RevokeApiKeyResponse, UpdateApiKeyRequest,
+};
diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs
index 54471190778..66944a4535a 100644
--- a/crates/router/src/types/storage.rs
+++ b/crates/router/src/types/storage.rs
@@ -1,4 +1,5 @@
 pub mod address;
+pub mod api_keys;
 pub mod configs;
 pub mod connector_response;
 pub mod customers;
@@ -22,7 +23,8 @@ pub mod refund;
 pub mod kv;
 
 pub use self::{
-    address::*, configs::*, connector_response::*, customers::*, events::*, locker_mock_up::*,
-    mandate::*, merchant_account::*, merchant_connector_account::*, payment_attempt::*,
-    payment_intent::*, payment_method::*, process_tracker::*, refund::*, reverse_lookup::*,
+    address::*, api_keys::*, configs::*, connector_response::*, customers::*, events::*,
+    locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*,
+    payment_attempt::*, payment_intent::*, payment_method::*, process_tracker::*, refund::*,
+    reverse_lookup::*,
 };
diff --git a/crates/router/src/types/storage/api_keys.rs b/crates/router/src/types/storage/api_keys.rs
new file mode 100644
index 00000000000..1d74787eaa1
--- /dev/null
+++ b/crates/router/src/types/storage/api_keys.rs
@@ -0,0 +1 @@
+pub use storage_models::api_keys::{ApiKey, ApiKeyNew, ApiKeyUpdate, HashedApiKey};
diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs
index c08af2f8b95..6d4a3511fd6 100644
--- a/crates/router/src/types/transformers.rs
+++ b/crates/router/src/types/transformers.rs
@@ -398,3 +398,68 @@ impl From<F<api_models::payments::AddressDetails>> for F<storage_models::address
         .into()
     }
 }
+
+impl
+    From<
+        F<(
+            storage_models::api_keys::ApiKey,
+            crate::core::api_keys::PlaintextApiKey,
+        )>,
+    > for F<api_models::api_keys::CreateApiKeyResponse>
+{
+    fn from(
+        item: F<(
+            storage_models::api_keys::ApiKey,
+            crate::core::api_keys::PlaintextApiKey,
+        )>,
+    ) -> Self {
+        use masking::StrongSecret;
+
+        let (api_key, plaintext_api_key) = item.0;
+        api_models::api_keys::CreateApiKeyResponse {
+            key_id: api_key.key_id.clone(),
+            merchant_id: api_key.merchant_id,
+            name: api_key.name,
+            description: api_key.description,
+            api_key: StrongSecret::from(format!(
+                "{}-{}",
+                api_key.key_id,
+                plaintext_api_key.peek().to_owned()
+            )),
+            created: api_key.created_at,
+            expiration: api_key.expires_at.into(),
+        }
+        .into()
+    }
+}
+
+impl From<F<storage_models::api_keys::ApiKey>> for F<api_models::api_keys::RetrieveApiKeyResponse> {
+    fn from(item: F<storage_models::api_keys::ApiKey>) -> Self {
+        let api_key = item.0;
+        api_models::api_keys::RetrieveApiKeyResponse {
+            key_id: api_key.key_id.clone(),
+            merchant_id: api_key.merchant_id,
+            name: api_key.name,
+            description: api_key.description,
+            prefix: format!("{}-{}", api_key.key_id, api_key.prefix).into(),
+            created: api_key.created_at,
+            expiration: api_key.expires_at.into(),
+        }
+        .into()
+    }
+}
+
+impl From<F<api_models::api_keys::UpdateApiKeyRequest>>
+    for F<storage_models::api_keys::ApiKeyUpdate>
+{
+    fn from(item: F<api_models::api_keys::UpdateApiKeyRequest>) -> Self {
+        let api_key = item.0;
+        storage_models::api_keys::ApiKeyUpdate::Update {
+            name: api_key.name,
+            description: api_key.description,
+            expires_at: api_key.expiration.map(Into::into),
+            last_used: None,
+        }
+        .into()
+    }
+}
diff --git a/crates/router_env/src/env.rs b/crates/router_env/src/env.rs
index 634ee58a983..60302da4554 100644
--- a/crates/router_env/src/env.rs
+++ b/crates/router_env/src/env.rs
@@ -37,6 +37,16 @@ pub fn which() -> Env {
     std::env::var(RUN_ENV).map_or_else(|_| default_env, |v| v.parse().unwrap_or(default_env))
 }
 
+/// Three letter (lowercase) prefix corresponding to the current environment.
+/// Either `dev`, `snd` or `prd`.
+pub fn prefix_for_env() -> &'static str {
+    match which() {
+        Env::Development => "dev",
+        Env::Sandbox => "snd",
+        Env::Production => "prd",
+    }
+}
+
 ///
 /// Base path to look for config and logs directories.
 /// Application expects to find `./config/` and `./logs/` relative this directories.
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index fc2f6ef9df3..d2bc3a08b0a 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -150,6 +150,16 @@ pub enum Flow {
     IncomingWebhookReceive,
     /// Validate payment method flow
     ValidatePaymentMethod,
+    /// API Key create flow
+    ApiKeyCreate,
+    /// API Key retrieve flow
+    ApiKeyRetrieve,
+    /// API Key update flow
+    ApiKeyUpdate,
+    /// API Key revoke flow
+    ApiKeyRevoke,
+    /// API Key list flow
+    ApiKeyList,
 }
 
 /// Category of log event.
diff --git a/crates/storage_models/src/api_keys.rs b/crates/storage_models/src/api_keys.rs
new file mode 100644
index 00000000000..42a6a8d3c53
--- /dev/null
+++ b/crates/storage_models/src/api_keys.rs
@@ -0,0 +1,133 @@
+use diesel::{AsChangeset, AsExpression, Identifiable, Insertable, Queryable};
+use masking::Secret;
+use time::PrimitiveDateTime;
+
+use crate::schema::api_keys;
+
+#[derive(Debug, Identifiable, Queryable)]
+#[diesel(table_name = api_keys, primary_key(key_id))]
+pub struct ApiKey {
+    pub key_id: String,
+    pub merchant_id: String,
+    pub name: String,
+    pub description: Option<String>,
+    pub hash_key: Secret<String>,
+    pub hashed_api_key: HashedApiKey,
+    pub prefix: String,
+    pub created_at: PrimitiveDateTime,
+    pub expires_at: Option<PrimitiveDateTime>,
+    pub last_used: Option<PrimitiveDateTime>,
+}
+
+#[derive(Debug, Insertable)]
+#[diesel(table_name = api_keys)]
+pub struct ApiKeyNew {
+    pub key_id: String,
+    pub merchant_id: String,
+    pub name: String,
+    pub description: Option<String>,
+    pub hash_key: Secret<String>,
+    pub hashed_api_key: HashedApiKey,
+    pub prefix: String,
+    pub created_at: PrimitiveDateTime,
+    pub expires_at: Option<PrimitiveDateTime>,
+    pub last_used: Option<PrimitiveDateTime>,
+}
+
+#[derive(Debug)]
+pub enum ApiKeyUpdate {
+    Update {
+        name: Option<String>,
+        description: Option<String>,
+        expires_at: Option<Option<PrimitiveDateTime>>,
+        last_used: Option<PrimitiveDateTime>,
+    },
+    LastUsedUpdate {
+        last_used: PrimitiveDateTime,
+    },
+}
+
+#[derive(Debug, AsChangeset)]
+#[diesel(table_name = api_keys)]
+pub(crate) struct ApiKeyUpdateInternal {
+    pub name: Option<String>,
+    pub description: Option<String>,
+    pub expires_at: Option<Option<PrimitiveDateTime>>,
+    pub last_used: Option<PrimitiveDateTime>,
+}
+
+impl From<ApiKeyUpdate> for ApiKeyUpdateInternal {
+    fn from(api_key_update: ApiKeyUpdate) -> Self {
+        match api_key_update {
+            ApiKeyUpdate::Update {
+                name,
+                description,
+                expires_at,
+                last_used,
+            } => Self {
+                name,
+                description,
+                expires_at,
+                last_used,
+            },
+            ApiKeyUpdate::LastUsedUpdate { last_used } => Self {
+                last_used: Some(last_used),
+                name: None,
+                description: None,
+                expires_at: None,
+            },
+        }
+    }
+}
+
+#[derive(Debug, AsExpression)]
+#[diesel(sql_type = diesel::sql_types::Text)]
+pub struct HashedApiKey(String);
+
+impl From<String> for HashedApiKey {
+    fn from(hashed_api_key: String) -> Self {
+        Self(hashed_api_key)
+    }
+}
+
+mod diesel_impl {
+    use diesel::{
+        backend::Backend,
+        deserialize::FromSql,
+        serialize::{Output, ToSql},
+        sql_types::Text,
+        Queryable,
+    };
+
+    impl<DB> ToSql<Text, DB> for super::HashedApiKey
+    where
+        DB: Backend,
+        String: ToSql<Text, DB>,
+    {
+        fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, DB>) -> diesel::serialize::Result {
+            self.0.to_sql(out)
+        }
+    }
+
+    impl<DB> FromSql<Text, DB> for super::HashedApiKey
+    where
+        DB: Backend,
+        String: FromSql<Text, DB>,
+    {
+        fn from_sql(bytes: diesel::backend::RawValue<'_, DB>) -> diesel::deserialize::Result<Self> {
+            Ok(Self(String::from_sql(bytes)?))
+        }
+    }
+
+    impl<DB> Queryable<Text, DB> for super::HashedApiKey
+    where
+        DB: Backend,
+        Self: FromSql<Text, DB>,
+    {
+        type Row = Self;
+
+        fn build(row: Self::Row) -> diesel::deserialize::Result<Self> {
+            Ok(row)
+        }
+    }
+}
diff --git a/crates/storage_models/src/lib.rs b/crates/storage_models/src/lib.rs
index 6a92b6df1e4..5d098b1ebf2 100644
--- a/crates/storage_models/src/lib.rs
+++ b/crates/storage_models/src/lib.rs
@@ -1,4 +1,5 @@
 pub mod address;
+pub mod api_keys;
 pub mod configs;
 pub mod connector_response;
 pub mod customers;
diff --git a/crates/storage_models/src/query.rs b/crates/storage_models/src/query.rs
index 379617571bd..d234c76237c 100644
--- a/crates/storage_models/src/query.rs
+++ b/crates/storage_models/src/query.rs
@@ -1,4 +1,5 @@
 pub mod address;
+pub mod api_keys;
 pub mod configs;
 pub mod connector_response;
 pub mod customers;
diff --git a/crates/storage_models/src/query/api_keys.rs b/crates/storage_models/src/query/api_keys.rs
new file mode 100644
index 00000000000..dde38dc6f82
--- /dev/null
+++ b/crates/storage_models/src/query/api_keys.rs
@@ -0,0 +1,84 @@
+use diesel::{associations::HasTable, ExpressionMethods};
+use router_env::{instrument, tracing};
+
+use super::generics;
+use crate::{
+    api_keys::{ApiKey, ApiKeyNew, ApiKeyUpdate, ApiKeyUpdateInternal},
+    errors,
+    schema::api_keys::dsl,
+    PgPooledConn, StorageResult,
+};
+
+impl ApiKeyNew {
+    #[instrument(skip(conn))]
+    pub async fn insert(self, conn: &PgPooledConn) -> StorageResult<ApiKey> {
+        generics::generic_insert(conn, self).await
+    }
+}
+
+impl ApiKey {
+    #[instrument(skip(conn))]
+    pub async fn update_by_key_id(
+        conn: &PgPooledConn,
+        key_id: String,
+        api_key_update: ApiKeyUpdate,
+    ) -> StorageResult<Self> {
+        match generics::generic_update_by_id::<<Self as HasTable>::Table, _, _, _>(
+            conn,
+            key_id.clone(),
+            ApiKeyUpdateInternal::from(api_key_update),
+        )
+        .await
+        {
+            Err(error) => match error.current_context() {
+                errors::DatabaseError::NotFound => {
+                    Err(error.attach_printable("API key with the given key ID does not exist"))
+                }
+                errors::DatabaseError::NoFieldsToUpdate => {
+                    generics::generic_find_by_id::<<Self as HasTable>::Table, _, _>(conn, key_id)
+                        .await
+                }
+                _ => Err(error),
+            },
+            result => result,
+        }
+    }
+
+    #[instrument(skip(conn))]
+    pub async fn revoke_by_key_id(conn: &PgPooledConn, key_id: &str) -> StorageResult<bool> {
+        generics::generic_delete::<<Self as HasTable>::Table, _>(
+            conn,
+            dsl::key_id.eq(key_id.to_owned()),
+        )
+        .await
+    }
+
+    #[instrument(skip(conn))]
+    pub async fn find_optional_by_key_id(
+        conn: &PgPooledConn,
+        key_id: &str,
+    ) -> StorageResult<Option<Self>> {
+        generics::generic_find_by_id_optional::<<Self as HasTable>::Table, _, _>(
+            conn,
+            key_id.to_owned(),
+        )
+        .await
+    }
+
+    #[instrument(skip(conn))]
+    pub async fn find_by_merchant_id(
+        conn: &PgPooledConn,
+        merchant_id: &str,
+        limit: Option<i64>,
+        offset: Option<i64>,
+    ) -> StorageResult<Vec<Self>> {
+        generics::generic_filter::<<Self as HasTable>::Table, _, _, _>(
+            conn,
+            dsl::merchant_id.eq(merchant_id.to_owned()),
+            limit,
+            offset,
+            Some(dsl::created_at.asc()),
+        )
+        .await
+    }
+}
diff --git a/crates/storage_models/src/schema.rs b/crates/storage_models/src/schema.rs
index fb3e01573ab..d2fd26ec384 100644
--- a/crates/storage_models/src/schema.rs
+++ b/crates/storage_models/src/schema.rs
@@ -25,6 +25,24 @@ diesel::table! {
     }
 }
 
+diesel::table! {
+    use diesel::sql_types::*;
+    use crate::enums::diesel_exports::*;
+
+    api_keys (key_id) {
+        key_id -> Varchar,
+        merchant_id -> Varchar,
+        name -> Varchar,
+        description -> Nullable<Varchar>,
+        hash_key -> Varchar,
+        hashed_api_key -> Varchar,
+        prefix -> Varchar,
+        created_at -> Timestamp,
+        expires_at -> Nullable<Timestamp>,
+        last_used -> Nullable<Timestamp>,
+    }
+}
+
 diesel::table! {
     use diesel::sql_types::*;
     use crate::enums::diesel_exports::*;
@@ -347,6 +365,7 @@ diesel::table! {
 
 diesel::allow_tables_to_appear_in_same_query!(
     address,
+    api_keys,
     configs,
     connector_response,
     customers,
diff --git a/migrations/2023-02-01-135102_create_api_keys_table/down.sql b/migrations/2023-02-01-135102_create_api_keys_table/down.sql
new file mode 100644
index 00000000000..02660a56810
--- /dev/null
+++ b/migrations/2023-02-01-135102_create_api_keys_table/down.sql
@@ -0,0 +1 @@
+DROP TABLE api_keys;
diff --git a/migrations/2023-02-01-135102_create_api_keys_table/up.sql b/migrations/2023-02-01-135102_create_api_keys_table/up.sql
new file mode 100644
index 00000000000..5f7c2518b40
--- /dev/null
+++ b/migrations/2023-02-01-135102_create_api_keys_table/up.sql
@@ -0,0 +1,12 @@
+CREATE TABLE api_keys (
+    key_id VARCHAR(64) NOT NULL PRIMARY KEY,
+    merchant_id VARCHAR(64) NOT NULL,
+    NAME VARCHAR(64) NOT NULL,
+    description VARCHAR(256) DEFAULT NULL,
+    hash_key VARCHAR(64) NOT NULL,
+    hashed_api_key VARCHAR(128) NOT NULL,
+    prefix VARCHAR(16) NOT NULL,
+    created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP,
+    expires_at TIMESTAMP DEFAULT NULL,
+    last_used TIMESTAMP DEFAULT NULL
+);
diff --git a/openapi/generated.json b/openapi/generated.json
index d09ec556f11..34ead266ca7 100644
--- a/openapi/generated.json
+++ b/openapi/generated.json
@@ -70,7 +70,6 @@
             "in": "path",
             "description": "The unique identifier for the merchant account",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -106,7 +105,6 @@
             "in": "path",
             "description": "The unique identifier for the merchant account",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -152,7 +150,6 @@
             "in": "path",
             "description": "The unique identifier for the merchant account",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -190,7 +187,6 @@
             "in": "path",
             "description": "The unique identifier for the merchant account",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -268,7 +264,6 @@
             "in": "path",
             "description": "The unique identifier for the merchant account",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -278,7 +273,6 @@
             "in": "path",
             "description": "The unique identifier for the payment connector",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "integer",
               "format": "int32"
@@ -318,7 +312,6 @@
             "in": "path",
             "description": "The unique identifier for the merchant account",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -328,7 +321,6 @@
             "in": "path",
             "description": "The unique identifier for the payment connector",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "integer",
               "format": "int32"
@@ -378,7 +370,6 @@
             "in": "path",
             "description": "The unique identifier for the merchant account",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -388,7 +379,6 @@
             "in": "path",
             "description": "The unique identifier for the payment connector",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "integer",
               "format": "int32"
@@ -416,6 +406,207 @@
         "deprecated": false
       }
     },
+    "/api_keys": {
+      "post": {
+        "tags": [
+          "API Key"
+        ],
+        "summary": "API Key - Create",
+        "description": "API Key - Create\n\nCreate a new API Key for accessing our APIs from your servers. The plaintext API Key will be\ndisplayed only once on creation, so ensure you store it securely.",
+        "operationId": "Create an API Key",
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/CreateApiKeyRequest"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "API Key created",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/CreateApiKeyResponse"
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "Invalid data"
+          }
+        },
+        "deprecated": false
+      }
+    },
+    "/api_keys/list": {
+      "get": {
+        "tags": [
+          "API Key"
+        ],
+        "summary": "API Key - List",
+        "description": "API Key - List\n\nList all API Keys associated with your merchant account.",
+        "operationId": "List all API Keys associated with a merchant account",
+        "parameters": [
+          {
+            "name": "limit",
+            "in": "query",
+            "description": "The maximum number of API Keys to include in the response",
+            "required": false,
+            "schema": {
+              "type": "integer",
+              "format": "int64"
+            }
+          },
+          {
+            "name": "skip",
+            "in": "query",
+            "description": "The number of API Keys to skip when retrieving the list of API keys.",
+            "required": false,
+            "schema": {
+              "type": "integer",
+              "format": "int64"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "List of API Keys retrieved successfully",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/RetrieveApiKeyResponse"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "deprecated": false
+      }
+    },
+    "/api_keys/{key_id}": {
+      "get": {
+        "tags": [
+          "API Key"
+        ],
+        "summary": "API Key - Retrieve",
+        "description": "API Key - Retrieve\n\nRetrieve information about the specified API Key.",
+        "operationId": "Retrieve an API Key",
+        "parameters": [
+          {
+            "name": "key_id",
+            "in": "path",
+            "description": "The unique identifier for the API Key",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "API Key retrieved",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/RetrieveApiKeyResponse"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "API Key not found"
+          }
+        },
+        "deprecated": false
+      },
+      "post": {
+        "tags": [
+          "API Key"
+        ],
+        "summary": "API Key - Update",
+        "description": "API Key - Update\n\nUpdate information for the specified API Key.",
+        "operationId": "Update an API Key",
+        "parameters": [
+          {
+            "name": "key_id",
+            "in": "path",
+            "description": "The unique identifier for the API Key",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/UpdateApiKeyRequest"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "description": "API Key updated",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/RetrieveApiKeyResponse"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "API Key not found"
+          }
+        },
+        "deprecated": false
+      },
+      "delete": {
+        "tags": [
+          "API Key"
+        ],
+        "summary": "API Key - Revoke",
+        "description": "API Key - Revoke\n\nRevoke the specified API Key. Once revoked, the API Key can no longer be used for\nauthenticating with our APIs.",
+        "operationId": "Revoke an API Key",
+        "parameters": [
+          {
+            "name": "key_id",
+            "in": "path",
+            "description": "The unique identifier for the API Key",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "API Key revoked",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/RevokeApiKeyResponse"
+                }
+              }
+            }
+          },
+          "404": {
+            "description": "API Key not found"
+          }
+        },
+        "deprecated": false
+      }
+    },
     "/customers": {
       "post": {
         "tags": [
@@ -466,7 +657,6 @@
             "in": "path",
             "description": "The unique identifier for the Customer",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -502,7 +692,6 @@
             "in": "path",
             "description": "The unique identifier for the Customer",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -548,7 +737,6 @@
             "in": "path",
             "description": "The unique identifier for the Customer",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -586,7 +774,6 @@
             "in": "path",
             "description": "The identifier for mandate",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -624,7 +811,6 @@
             "in": "path",
             "description": "The identifier for mandate",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -698,7 +884,6 @@
             "in": "path",
             "description": "The unique identifier for the merchant account",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -708,7 +893,6 @@
             "in": "query",
             "description": "The two-letter ISO currency code",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "array",
               "items": {
@@ -721,7 +905,6 @@
             "in": "path",
             "description": "The three-letter ISO currency code",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "array",
               "items": {
@@ -734,7 +917,6 @@
             "in": "query",
             "description": "The minimum amount accepted for processing by the particular payment method.",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "integer",
               "format": "int64"
@@ -745,7 +927,6 @@
             "in": "query",
             "description": "The maximum amount amount accepted for processing by the particular payment method.",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "integer",
               "format": "int64"
@@ -756,7 +937,6 @@
             "in": "query",
             "description": "Indicates whether the payment method is eligible for recurring payments",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "boolean"
             }
@@ -766,7 +946,6 @@
             "in": "query",
             "description": "Indicates whether the payment method is eligible for installment payments",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "boolean"
             }
@@ -807,7 +986,6 @@
             "in": "path",
             "description": "The unique identifier for the customer account",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -817,7 +995,6 @@
             "in": "query",
             "description": "The two-letter ISO currency code",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "array",
               "items": {
@@ -830,7 +1007,6 @@
             "in": "path",
             "description": "The three-letter ISO currency code",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "array",
               "items": {
@@ -843,7 +1019,6 @@
             "in": "query",
             "description": "The minimum amount accepted for processing by the particular payment method.",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "integer",
               "format": "int64"
@@ -854,7 +1029,6 @@
             "in": "query",
             "description": "The maximum amount amount accepted for processing by the particular payment method.",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "integer",
               "format": "int64"
@@ -865,7 +1039,6 @@
             "in": "query",
             "description": "Indicates whether the payment method is eligible for recurring payments",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "boolean"
             }
@@ -875,7 +1048,6 @@
             "in": "query",
             "description": "Indicates whether the payment method is eligible for installment payments",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "boolean"
             }
@@ -916,7 +1088,6 @@
             "in": "path",
             "description": "The unique identifier for the Payment Method",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -952,7 +1123,6 @@
             "in": "path",
             "description": "The unique identifier for the Payment Method",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -998,7 +1168,6 @@
             "in": "path",
             "description": "The unique identifier for the Payment Method",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1072,7 +1241,6 @@
             "in": "query",
             "description": "The identifier for the customer",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1082,7 +1250,6 @@
             "in": "query",
             "description": "A cursor for use in pagination, fetch the next list after some object",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1092,7 +1259,6 @@
             "in": "query",
             "description": "A cursor for use in pagination, fetch the previous list before some object",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1102,7 +1268,6 @@
             "in": "query",
             "description": "Limit on the number of objects to return",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "integer",
               "format": "int64"
@@ -1113,7 +1278,6 @@
             "in": "query",
             "description": "The time at which payment is created",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string",
               "format": "date-time"
@@ -1124,7 +1288,6 @@
             "in": "query",
             "description": "Time less than the payment created time",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string",
               "format": "date-time"
@@ -1135,7 +1298,6 @@
             "in": "query",
             "description": "Time greater than the payment created time",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string",
               "format": "date-time"
@@ -1146,7 +1308,6 @@
             "in": "query",
             "description": "Time less than or equals to the payment created time",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string",
               "format": "date-time"
@@ -1157,7 +1318,6 @@
             "in": "query",
             "description": "Time greater than or equals to the payment created time",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string",
               "format": "date-time"
@@ -1225,7 +1385,6 @@
             "in": "path",
             "description": "The identifier for payment",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1271,7 +1430,6 @@
             "in": "path",
             "description": "The identifier for payment",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1319,7 +1477,6 @@
             "in": "path",
             "description": "The identifier for payment",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1360,7 +1517,6 @@
             "in": "path",
             "description": "The identifier for payment",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1408,7 +1564,6 @@
             "in": "path",
             "description": "The identifier for payment",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1492,7 +1647,6 @@
             "in": "query",
             "description": "The identifier for the payment",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1502,7 +1656,6 @@
             "in": "query",
             "description": "Limit on the number of objects to return",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "integer",
               "format": "int64"
@@ -1513,7 +1666,6 @@
             "in": "query",
             "description": "The time at which refund is created",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string",
               "format": "date-time"
@@ -1524,7 +1676,6 @@
             "in": "query",
             "description": "Time less than the refund created time",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string",
               "format": "date-time"
@@ -1535,7 +1686,6 @@
             "in": "query",
             "description": "Time greater than the refund created time",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string",
               "format": "date-time"
@@ -1546,7 +1696,6 @@
             "in": "query",
             "description": "Time less than or equals to the refund created time",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string",
               "format": "date-time"
@@ -1557,7 +1706,6 @@
             "in": "query",
             "description": "Time greater than or equals to the refund created time",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string",
               "format": "date-time"
@@ -1596,7 +1744,6 @@
             "in": "path",
             "description": "The identifier for refund",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1632,7 +1779,6 @@
             "in": "path",
             "description": "The identifier for refund",
             "required": true,
-            "deprecated": false,
             "schema": {
               "type": "string"
             }
@@ -1758,6 +1904,20 @@
           "afterpay_clearpay"
         ]
       },
+      "ApiKeyExpiration": {
+        "oneOf": [
+          {
+            "type": "string",
+            "enum": [
+              "never"
+            ]
+          },
+          {
+            "type": "string",
+            "format": "date-time"
+          }
+        ]
+      },
       "AuthenticationType": {
         "type": "string",
         "enum": [
@@ -1905,6 +2065,83 @@
           "non_banking_finance"
         ]
       },
+      "CreateApiKeyRequest": {
+        "type": "object",
+        "description": "The request body for creating an API Key.",
+        "required": [
+          "name",
+          "expiration"
+        ],
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "A unique name for the API Key to help you identify it.",
+            "example": "Sandbox integration key",
+            "maxLength": 64
+          },
+          "description": {
+            "type": "string",
+            "description": "A description to provide more context about the API Key.",
+            "example": "Key used by our developers to integrate with the sandbox environment",
+            "maxLength": 256
+          },
+          "expiration": {
+            "$ref": "#/components/schemas/ApiKeyExpiration"
+          }
+        }
+      },
+      "CreateApiKeyResponse": {
+        "type": "object",
+        "description": "The response body for creating an API Key.",
+        "required": [
+          "key_id",
+          "merchant_id",
+          "name",
+          "api_key",
+          "created",
+          "expiration"
+        ],
+        "properties": {
+          "key_id": {
+            "type": "string",
+            "description": "The identifier for the API Key.",
+            "example": "5hEEqkgJUyuxgSKGArHA4mWSnX",
+            "maxLength": 64
+          },
+          "merchant_id": {
+            "type": "string",
+            "description": "The identifier for the Merchant Account.",
+            "example": "y3oqhf46pyzuxjbcn2giaqnb44",
+            "maxLength": 64
+          },
+          "name": {
+            "type": "string",
+            "description": "The unique name for the API Key to help you identify it.",
+            "example": "Sandbox integration key",
+            "maxLength": 64
+          },
+          "description": {
+            "type": "string",
+            "description": "The description to provide more context about the API Key.",
+            "example": "Key used by our developers to integrate with the sandbox environment",
+            "maxLength": 256
+          },
+          "api_key": {
+            "type": "string",
+            "description": "The plaintext API Key used for server-side API access. Ensure you store the API Key\nsecurely as you will not be able to see it again.",
+            "maxLength": 64
+          },
+          "created": {
+            "type": "string",
+            "format": "date-time",
+            "description": "The time at which the API Key was created.",
+            "example": "2022-09-10T10:11:12Z"
+          },
+          "expiration": {
+            "$ref": "#/components/schemas/ApiKeyExpiration"
+          }
+        }
+      },
       "CreateMerchantAccount": {
         "type": "object",
         "required": [
@@ -4376,6 +4613,79 @@
           }
         }
       },
+      "RetrieveApiKeyResponse": {
+        "type": "object",
+        "description": "The response body for retrieving an API Key.",
+        "required": [
+          "key_id",
+          "merchant_id",
+          "name",
+          "prefix",
+          "created",
+          "expiration"
+        ],
+        "properties": {
+          "key_id": {
+            "type": "string",
+            "description": "The identifier for the API Key.",
+            "example": "5hEEqkgJUyuxgSKGArHA4mWSnX",
+            "maxLength": 64
+          },
+          "merchant_id": {
+            "type": "string",
+            "description": "The identifier for the Merchant Account.",
+            "example": "y3oqhf46pyzuxjbcn2giaqnb44",
+            "maxLength": 64
+          },
+          "name": {
+            "type": "string",
+            "description": "The unique name for the API Key to help you identify it.",
+            "example": "Sandbox integration key",
+            "maxLength": 64
+          },
+          "description": {
+            "type": "string",
+            "description": "The description to provide more context about the API Key.",
+            "example": "Key used by our developers to integrate with the sandbox environment",
+            "maxLength": 256
+          },
+          "prefix": {
+            "type": "string",
+            "description": "The first few characters of the plaintext API Key to help you identify it.",
+            "maxLength": 16
+          },
+          "created": {
+            "type": "string",
+            "format": "date-time",
+            "description": "The time at which the API Key was created.",
+            "example": "2022-09-10T10:11:12Z"
+          },
+          "expiration": {
+            "$ref": "#/components/schemas/ApiKeyExpiration"
+          }
+        }
+      },
+      "RevokeApiKeyResponse": {
+        "type": "object",
+        "description": "The response body for revoking an API Key.",
+        "required": [
+          "key_id",
+          "revoked"
+        ],
+        "properties": {
+          "key_id": {
+            "type": "string",
+            "description": "The identifier for the API Key.",
+            "example": "5hEEqkgJUyuxgSKGArHA4mWSnX",
+            "maxLength": 64
+          },
+          "revoked": {
+            "type": "boolean",
+            "description": "Indicates whether the API key was revoked or not.",
+            "example": "true"
+          }
+        }
+      },
       "RoutingAlgorithm": {
         "type": "string",
         "description": "The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom'",
@@ -4552,6 +4862,27 @@
           "gpay"
         ]
       },
+      "UpdateApiKeyRequest": {
+        "type": "object",
+        "description": "The request body for updating an API Key.",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "A unique name for the API Key to help you identify it.",
+            "example": "Sandbox integration key",
+            "maxLength": 64
+          },
+          "description": {
+            "type": "string",
+            "description": "A description to provide more context about the API Key.",
+            "example": "Key used by our developers to integrate with the sandbox environment",
+            "maxLength": 256
+          },
+          "expiration": {
+            "$ref": "#/components/schemas/ApiKeyExpiration"
+          }
+        }
+      },
       "UpdatePaymentMethod": {
         "type": "object",
         "properties": {
@@ -4633,25 +4964,36 @@
   },
   "tags": [
     {
-      "name": "Merchant Account"
+      "name": "Merchant Account",
+      "description": "Create and manage merchant accounts"
+    },
+    {
+      "name": "Merchant Connector Account",
+      "description": "Create and manage merchant connector accounts"
     },
     {
-      "name": "Merchant Connector Account"
+      "name": "Payments",
+      "description": "Create and manage one-time payments, recurring payments and mandates"
     },
     {
-      "name": "Payments"
+      "name": "Refunds",
+      "description": "Create and manage refunds for successful payments"
     },
     {
-      "name": "Refunds"
+      "name": "Mandates",
+      "description": "Manage mandates"
     },
     {
-      "name": "Mandates"
+      "name": "Customers",
+      "description": "Create and manage customers"
     },
     {
-      "name": "Customers"
+      "name": "Payment Methods",
+      "description": "Create and manage payment methods of customers"
     },
     {
-      "name": "Payment Methods"
+      "name": "API Key",
+      "description": "Create and manage API Keys"
     }
   ]
 }
\ No newline at end of file

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates

## Description
<!-- Describe your changes in detail -->
This PR implements the endpoints to create, retrieve, update, revoke and list API keys. 

- As of now, the `last_used` field is included in the storage model, but I don't expect it to be actively used until API keys are cached in Redis or in-memory.
- The endpoints use admin API key authentication, but require an `X-Merchant-Id` header to be sent for creating an API key and to list API keys for a specific merchant.

This PR only implements the endpoints for managing API keys, I'll update merchant authentication to use these API keys in a separate PR.

### Additional Changes

- [x] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!-- 
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->
The relevant migrations are [`up.sql`](https://github.com/juspay/hyperswitch/blob/api-keys-endpoints/migrations/2023-02-01-135102_create_api_keys_table/up.sql) and [`down.sql`](https://github.com/juspay/hyperswitch/blob/api-keys-endpoints/migrations/2023-02-01-135102_create_api_keys_table/down.sql).

## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless its an obvious bug or documentation fix
that will have little conversation).
-->
Closes #501.

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
I have added a few unit tests where database accesses are not involved.

I'll attach invocations of the APIs via `curl` to give an idea of how they look.

- Create API Key:
  
  ```shell
  curl --location --request POST 'http://localhost:8080/api_keys' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'X-Merchant-Id: merchant123' \
  --header 'api-key: MyVerySecretAdminApiKey' \
  --data-raw '{
  "name": "API Key 1",
  "description": null,
  "expiration": "2023-09-23T01:02:03.000Z"
  }'
  {
    "key_id": "dev_PwX5u10PjAbs9XWac0B2",
    "merchant_id": "merchant123",
    "name": "API Key 1",
    "description": null,
    "api_key": "dev_PwX5u10PjAbs9XWac0B2-MyVerySecret64CharacterLongApiKey",
    "created": "2023-02-06T14:16:46.144Z",
    "expiration": "2023-09-23T01:02:03.000Z"
  }
  ```

- Retrieve API Key:

  ```shell
  curl --location --request GET 'http://localhost:8080/api_keys/dev_PwX5u10PjAbs9XWac0B2' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'api-key: MyVerySecretAdminApiKey'
  {
    "key_id": "dev_PwX5u10PjAbs9XWac0B2",
    "merchant_id": "merchant123",
    "name": "API Key 1",
    "description": null,
    "prefix": "dev_PwX5u10PjAbs9XWac0B2-MyVerySe",
    "created": "2023-02-06T14:16:46.144Z",
    "expiration": "2023-09-23T01:02:03.000Z"
  }
  ```

- Update API Key:

  ```shell
  curl --location --request POST 'http://localhost:8080/api_keys/dev_PwX5u10PjAbs9XWac0B2' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'api-key: MyVerySecretAdminApiKey' \
  --data-raw '{
    "description": "My very awesome API key"
  }'
  {
    "key_id": "dev_PwX5u10PjAbs9XWac0B2",
    "merchant_id": "merchant123",
    "name": "API Key 1",
    "description": "My very awesome API key",
    "prefix": "dev_PwX5u10PjAbs9XWac0B2-MyVerySe",
    "created": "2023-02-06T14:16:46.144Z",
    "expiration": "2023-09-23T01:02:03.000Z"
  }
  ```

- Revoke API Key:

  ```shell
  curl --location --request DELETE 'http://localhost:8080/api_keys/dev_PwX5u10PjAbs9XWac0B2' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'api-key: MyVerySecretAdminApiKey' \
  --data-raw ''
  {
    "key_id": "dev_PwX5u10PjAbs9XWac0B2",
    "revoked": true
  }
  ```

- List API Keys

  ```shell
  curl --location --request GET 'http://localhost:8080/api_keys/list' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --header 'X-Merchant-Id: merchant123' \
  --header 'api-key: MyVerySecretAdminApiKey' \
  --data-raw ''
  [
    {
      "key_id": "dev_PZ7uBeZrBmBrhXmLo3W0",
      "merchant_id": "merchant123",
      "name": "API Key 1",
      "description": null,
      "prefix": "dev_PZ7uBeZrBmBrhXmLo3W0-ApiKey01",
      "created": "2023-02-05T18:51:04.903Z",
      "expiration": "2023-09-23T01:02:03.000Z"
    },
    {
      "key_id": "dev_gd6zpyE2GDnjqVGyYGVN",
      "merchant_id": "merchant123",
      "name": "API Key 2",
      "description": null,
      "prefix": "dev_gd6zpyE2GDnjqVGyYGVN-ApiKey02",
      "created": "2023-02-05T18:51:08.272Z",
      "expiration": "2023-09-23T01:02:03.000Z"
    },
    {
      "key_id": "dev_8RPcI7zCs3c37KjsGYT0",
      "merchant_id": "merchant123",
      "name": "API Key 3",
      "description": null,
      "prefix": "dev_8RPcI7zCs3c37KjsGYT0-ApiKey03",
      "created": "2023-02-05T18:51:11.651Z",
      "expiration": "2023-09-23T01:02:03.000Z"
    }
  ]
  ```
## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed submitted code
- [ ] I added unit tests for my changes where possible
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable
