- Introduction
- Prerequisites
- Understanding Connectors and Payment Methods
- Integration Steps
- Implementing the Traits
- Set the Currency Unit
- Connector utility functions
- Connector configs for control center
- Update
ConnectorTypes.resandConnectorUtils.res - Add Connector Icon
- Test the Connector
- Build Payment Request and Response from JSON Schema
This guide provides instructions on integrating a new connector with Router, from setting up the environment to implementing API interactions.
- Familiarity with the Connector API you’re integrating
- A locally set up and running Router repository
- API credentials for testing (sign up for sandbox/UAT credentials on the connector’s website).
- Rust nightly toolchain installed for code formatting:
rustup toolchain install nightly
A Connector processes payments (e.g., Stripe, Adyen) or manages fraud risk (e.g., Signifyd). A Payment Method is a specific way to transact (e.g., credit card, PayPal). See the Hyperswitch Payment Matrix for details.
Integrating a connector is mainly an API integration task. You'll define request and response types and implement required traits.
This tutorial covers card payments via Billwerk. Review the API reference and test APIs before starting.
Follow these steps to integrate a new connector.
Run the following script to create the connector structure:
sh scripts/add_connector.sh <connector-name> <connector-base-url>Example folder structure:
hyperswitch_connectors/src/connectors
├── billwerk
│ └── transformers.rs
└── billwerk.rs
crates/router/tests/connectors
└── billwerk.rs
Note: move the file crates/hyperswitch_connectors/src/connectors/connector_name/test.rs to crates/router/tests/connectors/connector_name.rs
Define API request/response types and conversions in hyperswitch_connectors/src/connector/billwerk/transformers.rs
Implement connector traits in hyperswitch_connectors/src/connector/billwerk.rs
Write basic payment flow tests in crates/router/tests/connectors/billwerk.rs
Boilerplate code with todo!() is provided—follow the guide and complete the necessary implementations.
Integrating a new connector involves transforming Router's core data into the connector's API format. Since the Connector module is stateless, Router handles data persistence.
Design request/response structures based on the connector's API spec.
Define request format in transformers.rs:
#[derive(Debug, Serialize)]
pub struct BillwerkCustomerObject {
handle: Option<id_type::CustomerId>,
email: Option<Email>,
address: Option<Secret<String>>,
address2: Option<Secret<String>>,
city: Option<String>,
country: Option<common_enums::CountryAlpha2>,
first_name: Option<Secret<String>>,
last_name: Option<Secret<String>>,
}
#[derive(Debug, Serialize)]
pub struct BillwerkPaymentsRequest {
handle: String,
amount: MinorUnit,
source: Secret<String>,
currency: common_enums::Currency,
customer: BillwerkCustomerObject,
metadata: Option<SecretSerdeValue>,
settle: bool,
}Since Router is connector agnostic, only minimal data is sent to connector and optional fields may be ignored.
We transform our PaymentsAuthorizeRouterData into Billwerk's PaymentsRequest structure by employing the try_from function.
impl TryFrom<&BillwerkRouterData<&types::PaymentsAuthorizeRouterData>> for BillwerkPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: &BillwerkRouterData<&types::PaymentsAuthorizeRouterData>,
) -> Result<Self, Self::Error> {
if item.router_data.is_three_ds() {
return Err(errors::ConnectorError::NotImplemented(
"Three_ds payments through Billwerk".to_string(),
)
.into());
};
let source = match item.router_data.get_payment_method_token()? {
PaymentMethodToken::Token(pm_token) => Ok(pm_token),
_ => Err(errors::ConnectorError::MissingRequiredField {
field_name: "payment_method_token",
}),
}?;
Ok(Self {
handle: item.router_data.connector_request_reference_id.clone(),
amount: item.amount,
source,
currency: item.router_data.request.currency,
customer: BillwerkCustomerObject {
handle: item.router_data.customer_id.clone(),
email: item.router_data.request.email.clone(),
address: item.router_data.get_optional_billing_line1(),
address2: item.router_data.get_optional_billing_line2(),
city: item.router_data.get_optional_billing_city(),
country: item.router_data.get_optional_billing_country(),
first_name: item.router_data.get_optional_billing_first_name(),
last_name: item.router_data.get_optional_billing_last_name(),
},
metadata: item.router_data.request.metadata.clone().map(Into::into),
settle: item.router_data.request.is_auto_capture()?,
})
}
}When implementing the response type, the key enum to define for each connector is PaymentStatus. It represents the different status types returned by the connector, as specified in its API spec. Below is the definition for Billwerk.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum BillwerkPaymentState {
Created,
Authorized,
Pending,
Settled,
Failed,
Cancelled,
}
impl From<BillwerkPaymentState> for enums::AttemptStatus {
fn from(item: BillwerkPaymentState) -> Self {
match item {
BillwerkPaymentState::Created | BillwerkPaymentState::Pending => Self::Pending,
BillwerkPaymentState::Authorized => Self::Authorized,
BillwerkPaymentState::Settled => Self::Charged,
BillwerkPaymentState::Failed => Self::Failure,
BillwerkPaymentState::Cancelled => Self::Voided,
}
}
}Here are common payment attempt statuses:
- Charged: Payment succeeded.
- Pending: Payment is processing.
- Failure: Payment failed.
- Authorized: Payment authorized; can be voided, captured, or partially captured.
- AuthenticationPending: Customer action required.
- Voided: Payment voided, funds returned to the customer.
Note: Default status should be Pending. Only explicit success or failure from the connector should mark the payment as Charged or Failure.
Define response format in transformers.rs:
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BillwerkPaymentsResponse {
state: BillwerkPaymentState,
handle: String,
error: Option<String>,
error_state: Option<String>,
}We transform our ResponseRouterData into PaymentsResponseData by employing the try_from function.
impl<F, T> TryFrom<ResponseRouterData<F, BillwerkPaymentsResponse, T, PaymentsResponseData>>
for RouterData<F, T, PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: ResponseRouterData<F, BillwerkPaymentsResponse, T, PaymentsResponseData>,
) -> Result<Self, Self::Error> {
let error_response = if item.response.error.is_some() || item.response.error_state.is_some()
{
Some(ErrorResponse {
code: item
.response
.error_state
.clone()
.unwrap_or(NO_ERROR_CODE.to_string()),
message: item
.response
.error_state
.unwrap_or(NO_ERROR_MESSAGE.to_string()),
reason: item.response.error,
status_code: item.http_code,
attempt_status: None,
connector_transaction_id: Some(item.response.handle.clone()),
})
} else {
None
};
let payments_response = PaymentsResponseData::TransactionResponse {
resource_id: ResponseId::ConnectorTransactionId(item.response.handle.clone()),
redirection_data: Box::new(None),
mandate_reference: Box::new(None),
connector_metadata: None,
network_txn_id: None,
connector_response_reference_id: Some(item.response.handle),
incremental_authorization_allowed: None,
charges: None,
};
Ok(Self {
status: enums::AttemptStatus::from(item.response.state),
response: error_response.map_or_else(|| Ok(payments_response), Err),
..item.data
})
}
}- connector_request_reference_id: Merchant's reference ID in the payment request (e.g.,
referencein Checkout).
reference: item.router_data.connector_request_reference_id.clone(),- connector_response_reference_id: ID used for transaction identification in the connector dashboard, linked to merchant_reference or connector_transaction_id.
connector_response_reference_id: item.response.reference.or(Some(item.response.id)),- resource_id: The connector's connector_transaction_id is used as the resource_id. If unavailable, set to NoResponseId.
resource_id: types::ResponseId::ConnectorTransactionId(item.response.id.clone()),- redirection_data: For redirection flows (e.g., 3D Secure), assign the redirection link.
let redirection_data = item.response.links.redirect.map(|href| {
services::RedirectForm::from((href.redirection_url, services::Method::Get))
});Define error responses:
#[derive(Debug, Serialize, Deserialize)]
pub struct BillwerkErrorResponse {
pub code: Option<i32>,
pub error: String,
pub message: Option<String>,
}By following these steps, you can integrate a new connector efficiently while ensuring compatibility with Router's architecture.
The mod.rs file contains trait implementations using connector types in transformers. A struct with the connector name holds these implementations. Below are the mandatory traits:
Contains common description of the connector, like the base endpoint, content-type, error response handling, id, currency unit.
Within the ConnectorCommon trait, you'll find the following methods :
idmethod corresponds directly to the connector name.
fn id(&self) -> &'static str {
"Billwerk"
}get_currency_unitmethod anticipates you to specify the accepted currency unit for the connector.
fn get_currency_unit(&self) -> api::CurrencyUnit {
api::CurrencyUnit::Minor
}common_get_content_typemethod requires you to provide the accepted content type for the connector API.
fn common_get_content_type(&self) -> &'static str {
"application/json"
}get_auth_headermethod accepts common HTTP Authorization headers that are accepted in allConnectorIntegrationflows.
fn get_auth_header(
&self,
auth_type: &ConnectorAuthType,
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
let auth = BillwerkAuthType::try_from(auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?;
let encoded_api_key = BASE64_ENGINE.encode(format!("{}:", auth.api_key.peek()));
Ok(vec![(
headers::AUTHORIZATION.to_string(),
format!("Basic {encoded_api_key}").into_masked(),
)])
}base_urlmethod is for fetching the base URL of connector's API. Base url needs to be consumed from configs.
fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str {
connectors.billwerk.base_url.as_ref()
}build_error_responsemethod is common error response handling for a connector if it is same in all cases
fn build_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
let response: BillwerkErrorResponse = res
.response
.parse_struct("BillwerkErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
Ok(ErrorResponse {
status_code: res.status_code,
code: response
.code
.map_or(NO_ERROR_CODE.to_string(), |code| code.to_string()),
message: response.message.unwrap_or(NO_ERROR_MESSAGE.to_string()),
reason: Some(response.error),
attempt_status: None,
connector_transaction_id: None,
})
}For every api endpoint contains the url, using request transform and response transform and headers.
Within the ConnectorIntegration trait, you'll find the following methods implemented(below mentioned is example for authorized flow):
get_urlmethod defines endpoint for authorize flow, base url is consumed fromConnectorCommontrait.
fn get_url(
&self,
_req: &TokenizationRouterData,
connectors: &Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let base_url = connectors
.billwerk
.secondary_base_url
.as_ref()
.ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)?;
Ok(format!("{base_url}v1/token"))
}get_headersmethod accepts HTTP headers that are accepted for authorize flow. In this context, it is utilized from theConnectorCommonExttrait, as the connector adheres to common headers across various flows.
fn get_headers(
&self,
req: &TokenizationRouterData,
connectors: &Connectors,
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
self.build_headers(req, connectors)
}get_request_bodymethod uses transformers to convert the Hyperswitch payment request to the connector's format. If successful, it returns the request asRequestContent::Json, supporting formats like JSON, form-urlencoded, XML, and raw bytes.
fn get_request_body(
&self,
req: &TokenizationRouterData,
_connectors: &Connectors,
) -> CustomResult<RequestContent, errors::ConnectorError> {
let connector_req = BillwerkTokenRequest::try_from(req)?;
Ok(RequestContent::Json(Box::new(connector_req)))
}build_requestmethod assembles the API request by providing the method, URL, headers, and request body as parameters.
fn build_request(
&self,
req: &TokenizationRouterData,
connectors: &Connectors,
) -> CustomResult<Option<Request>, errors::ConnectorError> {
Ok(Some(
RequestBuilder::new()
.method(Method::Post)
.url(&types::TokenizationType::get_url(self, req, connectors)?)
.attach_default_headers()
.headers(types::TokenizationType::get_headers(self, req, connectors)?)
.set_body(types::TokenizationType::get_request_body(
self, req, connectors,
)?)
.build(),
))
}handle_responsemethod calls transformers where connector response data is transformed into hyperswitch response.
fn handle_response(
&self,
data: &TokenizationRouterData,
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<TokenizationRouterData, errors::ConnectorError>
where
PaymentsResponseData: Clone,
{
let response: BillwerkTokenResponse = res
.response
.parse_struct("BillwerkTokenResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}get_error_responsemethod to manage error responses. As the handling of checkout errors remains consistent across various flows, we've incorporated it from thebuild_error_responsemethod within theConnectorCommontrait.
fn get_error_response(
&self,
res: Response,
event_builder: Option<&mut ConnectorEvent>,
) -> CustomResult<ErrorResponse, errors::ConnectorError> {
self.build_error_response(res, event_builder)
}Adds functions with a generic type, including the build_headers method. This method constructs both common headers and Authorization headers (from get_auth_header), returning them as a vector.
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Billwerk
where
Self: ConnectorIntegration<Flow, Request, Response>,
{
fn build_headers(
&self,
req: &RouterData<Flow, Request, Response>,
_connectors: &Connectors,
) -> CustomResult<Vec<(String, masking::Maskable<String>)>, errors::ConnectorError> {
let mut header = vec![(
headers::CONTENT_TYPE.to_string(),
self.get_content_type().to_string().into(),
)];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
}Payment : This trait includes several other traits and is meant to represent the functionality related to payments.
PaymentAuthorize : This trait extends the api::ConnectorIntegration trait with specific types related to payment authorization.
PaymentCapture : This trait extends the api::ConnectorIntegration trait with specific types related to manual payment capture.
PaymentSync : This trait extends the api::ConnectorIntegration trait with specific types related to payment retrieve.
Refund : This trait includes several other traits and is meant to represent the functionality related to Refunds.
RefundExecute : This trait extends the api::ConnectorIntegration trait with specific types related to refunds create.
RefundSync : This trait extends the api::ConnectorIntegration trait with specific types related to refunds retrieve.
And the below derive traits
- Debug
- Clone
- Copy
Part of the ConnectorCommon trait, it allows connectors to specify their accepted currency unit as either Base or Minor. For example, PayPal uses the base unit (e.g., USD), while Hyperswitch uses the minor unit (e.g., cents). Conversion is required if the connector uses the base unit.
impl<T>
TryFrom<(
&types::api::CurrencyUnit,
types::storage::enums::Currency,
i64,
T,
)> for PaypalRouterData<T>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
(currency_unit, currency, amount, item): (
&types::api::CurrencyUnit,
types::storage::enums::Currency,
i64,
T,
),
) -> Result<Self, Self::Error> {
let amount = utils::get_amount_as_string(currency_unit, amount, currency)?;
Ok(Self {
amount,
router_data: item,
})
}
}Contains utility functions for constructing connector requests and responses. Use these helpers for retrieving fields like get_billing_country, get_browser_info, and get_expiry_date_as_yyyymm, as well as for validations like is_three_ds and is_auto_capture.
let json_wallet_data: CheckoutGooglePayData = wallet_data.get_wallet_token_as_json()?;This section is for developers using the Hyperswitch Control Center. Update the connector configuration in development.toml and run the wasm-pack build command. Replace placeholders with actual paths.
- Install wasm-pack:
cargo install wasm-pack-
Add connector configuration:
Open the development.toml file located at crates/connector_configs/toml/development.toml in your Hyperswitch project. Find the [stripe] section and add the configuration for example_connector. Example:
# crates/connector_configs/toml/development.toml # Other connector configurations... [stripe] [stripe.connector_auth.HeaderKey] api_key="Secret Key" # Add any other Stripe-specific configuration here [example_connector] # Your specific connector configuration for reference # ...
-
Update paths: Replace /absolute/path/to/hyperswitch-control-center and /absolute/path/to/hyperswitch with actual paths.
-
Run
wasm-packbuild: wasm-pack build --target web --out-dir /absolute/path/to/hyperswitch-control-center/public/hyperswitch/wasm --out-name euclid /absolute/path/to/hyperswitch/crates/euclid_wasm -- --features dummy_connector
By following these steps, you should be able to update the connector configuration and build the WebAssembly files successfully.
-
Update
ConnectorTypes.res:- Open
src/screens/HyperSwitch/Connectors/ConnectorTypes.res. - Add your connector to the
connectorNameenum:type connectorName = | Stripe | DummyConnector | YourNewConnector
- Save the file.
- Open
-
Update
ConnectorUtils.res:- Open
src/screens/HyperSwitch/Connectors/ConnectorUtils.res. - Update functions with your connector:
let connectorList : array<connectorName> = [Stripe, YourNewConnector] let getConnectorNameString = (connectorName: connectorName) => switch connectorName { | Stripe => "Stripe" | YourNewConnector => "Your New Connector" }; let getConnectorInfo = (connectorName: connectorName) => switch connectorName { | Stripe => "Stripe description." | YourNewConnector => "Your New Connector description." };
- Save the file.
- Open
-
Prepare the Icon:
Name your connector icon in uppercase (e.g.,YOURCONNECTOR.SVG). -
Add the Icon:
Navigate topublic/hyperswitch/Gatewayand copy your SVG icon file there. -
Verify Structure:
Ensure the file is correctly placed inpublic/hyperswitch/Gateway:public └── hyperswitch └── Gateway └── YOURCONNECTOR.SVGSave the changes made to the
Gatewayfolder.
-
Template Code
The template script generates a test file with 20 sanity tests. Implement these tests when adding a new connector.
Example test:
#[serial_test::serial] #[actix_web::test] async fn should_only_authorize_payment() { let response = CONNECTOR .authorize_payment(payment_method_details(), get_default_payment_info()) .await .expect("Authorize payment response"); assert_eq!(response.status, enums::AttemptStatus::Authorized); }
-
Utility Functions
Helper functions for tests are available in
tests/connector/utils, making test writing easier. -
Set API Keys
Before running tests, configure API keys in sample_auth.toml and set the environment variable:
export CONNECTOR_AUTH_FILE_PATH="/hyperswitch/crates/router/tests/connectors/sample_auth.toml" cargo test --package router --test connectors -- checkout --test-threads=1
-
Install OpenAPI Generator:
brew install openapi-generator
-
Generate Rust Code:
export CONNECTOR_NAME="<CONNECTOR-NAME>" export SCHEMA_PATH="<PATH-TO-SCHEMA>" openapi-generator generate -g rust -i ${SCHEMA_PATH} -o temp && cat temp/src/models/* > crates/router/src/connector/${CONNECTOR_NAME}/temp.rs && rm -rf temp && sed -i'' -r "s/^pub use.*//;s/^pub mod.*//;s/^\/.*//;s/^.\*.*//;s/crate::models:://g;" crates/router/src/connector/${CONNECTOR_NAME}/temp.rs && cargo +nightly fmt