Hyperswitch represents outbound PSP credentials through a single enumeration, ConnectorAuthType, that unifies the diverse credential shapes payment service providers require. One PSP accepts a single API key in an Authorization header. Another expects two keys base64-encoded as HTTP Basic. A third needs three keys to compute an HMAC signature over every request. A fourth issues short-lived access tokens that have to be refreshed before they expire. A fifth needs a TLS client certificate presented during the handshake.
Authentication looks simple from outside the orchestrator. Inside, it is one of the more diverse layers of integration code in the system. Every PSP exposes its own credential shape, its own transmission rule, and its own per request security overhead. The orchestrator's job is to make all of that look like one configuration knob to the merchant.
Because Hyperswitch is open-source, Rust-based, and self-hostable, everything below isn't a description of a black box, it's the actual code path your credentials travel. You can audit exactly how a secret is validated, masked in logs, signed over, and stored, and you can run the whole thing inside your own cloud so the secret never leaves it.
Three aspects need handling: the credentials, how they travel and get transmitted, and the security overhead some PSPs require on every request.
Credential normalization
The first question is what the merchant has to provide. Some PSPs need a single API key. Some need a key and a sub-account identifier. Some need three values to compute a signed-request HMAC. Some need a certificate and a private key.
Hyperswitch encodes every shape supported by any of its connectors in one enum, ConnectorAuthType, at crates/hyperswitch_domain_models/src/router_data.rs:319.
pub enum ConnectorAuthType {
HeaderKey { api_key: Secret<String> },
BodyKey { api_key: Secret<String>, key1: Secret<String> },
SignatureKey {
api_key: Secret<String>,
key1: Secret<String>,
api_secret: Secret<String>,
},
MultiAuthKey {
api_key: Secret<String>,
key1: Secret<String>,
api_secret: Secret<String>,
key2: Secret<String>,
},
CurrencyAuthKey {
auth_key_map: HashMap<Currency, SecretSerdeValue>,
},
CertificateAuth {
certificate: Secret<String>,
private_key: Secret<String>,
},
NoKey,
TemporaryAuth,
}The same eight variants cover the full connector catalog (100+ PSP’s today) because every PSP's authentication scheme reduces to one of these shapes. Every value is wrapped in Secret<String>, ensuring it is never accidentally logged or serialized in clear text.
The table below shows which credentials shape each major PSP needs.
| Processor | Credential | ConnectorAuthType variant |
| Stripe | API key | HeaderKey { api_key } |
| Adyen | API key | HeaderKey { api_key } |
| Braintree | Public key plus private key | BodyKey { api_key, key1 } |
| Klarna | Username plus password | BodyKey { api_key, key1 } |
| PayPal | Client ID plus client secret | BodyKey { api_key, key1 } |
| Worldpay | Username plus password | BodyKey { api_key, key1 } |
| Cybersource | API key, merchant ID, API secret | SignatureKey { api_key, key1, api_secret } |
| Chase | Merchant ID plus key plus secret | SignatureKey { api_key, key1, api_secret } |
| Nuvei | Merchant, gateway, terminal, secret | MultiAuthKey { api_key, key1, api_secret, key2 } |
| Netcetera | Client certificate plus private key | CertificateAuth { certificate, private_key } |
Two different things can go wrong. Supply the wrong credential shape and Hyperswitch rejects it locally the connector's TryFrom conversion fails with FailedToObtainAuthType before a single byte hits the PSP. Supply the right shape with a bad or wrong-environment value and you get the PSP's own 401, which is often a generic 'authentication failed' with no hint as to whether the key is missing, malformed, or simply not valid for that mode. The first class of failure is caught at config time; the second only surfaces on the first live transaction.
The transformer step. The canonical enum is intentionally generic: key1, api_secret, key2 are positional, not semantic. Each connector closes that gap with its own typed auth struct and a TryFrom conversion. Stripe's looks like this:
pub struct StripeAuthType {pub(super) api_key: Secret<String> }
impl TryFrom<&ConnectorAuthType> for StripeAuthType {
type Error = error_stack::Report<ConnectorError>;
fn try_from(auth_type: &ConnectorAuthType) -> Result<Self, Self::Error> {
if let ConnectorAuthType::HeaderKey { api_key } = auth_type {
Ok(Self { api_key: api_key.to_owned() })
} else {
Err(ConnectorError::FailedToObtainAuthType.into())
}
}
}
Braintree's transformer maps the same generic BodyKey { api_key, key1 } onto domain names: public_key and private_key so the rest of the connector reads naturally. This is where the orchestrator both checks the shape and gives each field its real-world meaning.
Transmission normalization
The second question is how the credential travels. Same credential plus wrong header name equals rejected. X-API-Key and Authorization: Bearer ... are not interchangeable. Body-embedded credentials need to be in exactly the field the PSP expects, with exactly the casing the PSP expects.
Hyperswitch handles transmission through a single trait method on every connector. The ConnectorCommon trait at crates/hyperswitch_interfaces/src/api.rs:351 declares:
pub trait ConnectorCommon {
fn id(&self) -> &'static str;
fn get_auth_header(
&self,
_auth_type: &ConnectorAuthType,
) -> CustomResult<Vec<(String, Maskable<String>)>, errors::ConnectorError> {
Ok(Vec::new())
}
}Every connector implements get_auth_header to turn the canonical credential into the exact wire format the PSP expects. The variety is wide.
| Processor | Transmission | Header or body field |
| Stripe | Header, Bearer token | Authorization: Bearer <api_key> |
| Adyen | Header, custom name | X-API-Key: <api_key> |
| Braintree | Header, HTTP Basic | Authorization: Basic <base64(pub:priv)> |
| Klarna | Header, HTTP Basic | Authorization: Basic <base64(user:pass)> |
| Worldpay | Header, HTTP Basic | Authorization: Basic <base64(user:pass)> |
| Cybersource | Header, HTTP Signature | Signature: keyid="...", signature="..." |
| PayPal | Header, Bearer access token (post-exchange) | Authorization: Bearer <access_token> |
| Nuvei | Body, multi-field | Credentials embedded in JSON body fields |
A few examples make the spread concrete.
Stripe at crates/hyperswitch_connectors/src/connectors/stripe.rs:146 produces a Bearer header:
Ok(vec![(
AUTHORIZATION.to_string(),
format!("Bearer {}", auth.api_key.peek()).into_masked(),
)])Adyen at crates/hyperswitch_connectors/src/connectors/adyen.rs:129 uses a custom header name with the same HeaderKey variant:
Ok(vec![(
headers::X_API_KEY.to_string(),
auth.api_key.into_masked(),
)])Braintree at crates/hyperswitch_connectors/src/connectors/braintree.rs:137 constructs an HTTP Basic header from two keys:
let auth_key = format!("{}:{}", auth.public_key.peek(), auth.private_key.peek());
let auth_header = format!("Basic {}", BASE64_ENGINE.encode(auth_key));
The trait does not abstract over header construction. It admits that header construction is per-connector and gives each connector a single, well-defined hook to write its own.
Security overhead
The third question is what else has to happen on every request, beyond putting the credential in the right place. This is the most complex layer to get wrong. HMAC signing has to happen on every request, in the right order, over the right fields. mTLS certificate expiry silently breaks production. OAuth token refresh has to be handled, or every payment fails the moment the cached token expires.
Hyperswitch handles three classes of security overhead.
Signed requests
Cybersource is the canonical example. Every API call is signed with HMAC-SHA256 over a specific set of request metadata: host, date, request-target, optional body digest, and merchant ID. The signature is base64-encoded and emitted in a Signature header. The implementation lives at crates/hyperswitch_connectors/src/connectors/cybersource.rs:102:
let key_value = consts::BASE64_ENGINE.decode(api_secret.expose())?;
let key = hmac::Key::new(hmac::HMAC_SHA256, &key_value);
let signature_value = consts::BASE64_ENGINE.encode(
hmac::sign(&key, signature_string.as_bytes()).as_ref()
);
let signature_header = format!(
r#"keyid="{}", algorithm="HmacSHA256", headers="{headers}", signature="{signature_value}""#,
api_key.peek()
);Cybersource uses the SignatureKey { api_key, key1, api_secret } variant. api_key becomes the keyid in the header, key1 is the merchant ID, and api_secret is the base64-encoded HMAC key. The signature is recomputed for every request, over the request being sent at that moment.
OAuth2 token exchange and refresh
PayPal does not accept client credentials directly on payment API calls. The orchestrator first exchanges client_id and client_secret for a short-lived access_token at the OAuth2 endpoint, then uses the access token in Authorization: Bearer for subsequent calls. When the token expires, the exchange has to happen again.
Hyperswitch models this as a separate flow, AccessTokenAuth, with its own integration trait implementation. The PayPal handler at crates/hyperswitch_connectors/src/connectors/paypal.rs:518 exchanges credentials at v1/oauth2/token and stores the resulting access token in the request context so downstream payment calls can pick it up.
The merchant configures PayPal with the same BodyKey { api_key, key1 } shape used by Braintree or Klarna. The token-refresh mechanics are entirely on the hyperswitch side. The merchant never sees the exchange and never has to track token expiry.
mTLS
Some PSPs require client certificate authentication at the TLS layer. The credential is not a string in a header; it is a certificate and a private key presented during the TLS handshake. Netcetera at crates/hyperswitch_connectors/src/connectors/netcetera/transformers.rs:227 uses the CertificateAuth variant:
ConnectorAuthType::CertificateAuth { certificate, private_key } => Ok(Self {
certificate,
private_key,
}),The certificate and private key are stored as PEM-encoded strings in the same encrypted credential store as every other connector's secrets. The HTTP client picks them up at request time and presents them during the TLS handshake. Certificate rotation is handled by updating the credential record in the Control Centre, without code changes.
The security-overhead column is where the trait-based abstraction earns its keep. From the merchant's point of view, all three of these flows look the same: configure the connector once and forget. From the orchestrator's point of view, they are entirely different code paths, and the trait gives each PSP a clean place to put its own.
Credential storage and multi-account routing
Once a merchant has configured a PSP, the credentials live in a single encrypted column on the MerchantConnectorAccount record at crates/hyperswitch_domain_models/src/merchant_connector_account.rs:40:
pub struct MerchantConnectorAccount {
pub merchant_id: id_type::MerchantId,
pub connector_name: String,
#[encrypt]
pub connector_account_details: Encryptable<Secret<Value>>,
pub merchant_connector_id: id_type::MerchantConnectorAccountId,
pub connector_label: Option<String>,
pub business_country: Option<enums::CountryAlpha2>,
pub business_label: Option<String>,
// ...
}connector_account_details is JSON, encrypted with the merchant's key, and parsed into the ConnectorAuthType enum at runtime. The encryption is at the application layer, not at the database layer, so the database operator never sees plaintext credentials.
Multi-account support is built into the same record. A merchant can configure several MerchantConnectorAccount rows for the same PSP, distinguished by connector_label (for example stripe_us, stripe_eu) and selected at payment time by label-based routing. This supports credential rotation, regional routing, marketplace structures with separate MIDs per geography, and A/B testing of credential sets without touching integration code.
The query at crates/storage_impl/src/merchant_connector_account.rs:25 lets the routing layer pick a specific credential set by label:
async fn find_merchant_connector_account_by_merchant_id_connector_label(
&self,
merchant_id: &id_type::MerchantId,
connector_label: &str,
key_store: &MerchantKeyStore,
) -> CustomResult<Self, StorageError>Implementation
What a merchant sees is a single API for adding a connector, regardless of credential shape, transmission rule, or security overhead. The same endpoint accepts a Stripe Bearer key, an Adyen X-API-Key, a Braintree public/private pair, a PayPal client_id/secret pair that the orchestrator will OAuth-exchange in the background, a Cybersource signing trio, or a Netcetera certificate and private key.
{
"merchant_id": "merchant_1234",
"connector_name": "stripe",
"connector_label": "stripe_us",
"connector_account_details": {
"auth_type": "HeaderKey",
"api_key": "sk_live_..."
},
"test_mode": false,
"disabled": false,
"business_country": "US",
"business_label": "default"
}The same shape, with a different auth_type, configures Cybersource:
{
"merchant_id": "merchant_1234",
"connector_name": "cybersource",
"connector_account_details": {
"auth_type": "SignatureKey",
"api_key": "<key_id>",
"key1": "<merchant_id>",
"api_secret": "<base64_hmac_secret>"
},
"test_mode": false
}A short glossary of auth_type values:
| auth_type | Use case | Example PSPs |
| HeaderKey | Single API key in any header | Stripe, Adyen, Checkout |
| BodyKey | Two credentials | Braintree, Klarna, PayPal, Worldpay |
| SignatureKey | Three credentials for HMAC-signed requests | Cybersource, Chase |
| MultiAuthKey | Four credentials | Nuvei and similar |
| CertificateAuth | mTLS client cert plus private key | Netcetera, Santander |
| CurrencyAuthKey | Different credentials per currency | Cross-border PSPs with regional MIDs |
| NoKey | Public or test endpoints | Sandboxes, mocks |
The fastest way to validate a Hyperswitch connector integration is to add the connector in the Control Centre with test credentials, run a sandbox payment, and observe the connector accept the credentials on the first call. Every subsequent PSP added later uses the same configuration shape. The credential format changes; the merchant-facing configuration model does not.
For end-to-end connector configuration references, the full set of supported auth_type values per connector, and the dashboard walkthrough for credential rotation, see the Connector Configuration documentation.
If you want this credential-normalization layer without the full orchestrator (routing, retries, and vaulting), that's exactly what Hyperswitch Prism provides: a stateless connector library that maps a single request schema to multiple PSPs, stores and logs nothing, and exists only for the lifetime of your HTTP client.