juspay/hyperswitch-prism

One library | Many payment processors | Scale to multiple processors with few lines of code.

1,390 stars Rust 8 components

10 hidden assumptions · 8-stage pipeline · 8 components

Like any codebase, this library makes assumptions it never checks — most are routine. The ones worth your attention are below, in plain language with what to do about each.

Translates a single payment API call into any of 100+ processor-specific wire formats

A payment call enters through one of three surfaces: a language SDK (Python/Node/Java/Rust) that speaks gRPC or FFI, the gRPC server directly, or the Rust library linked in-process. In all cases the caller provides a unified payment intent (amount in minor units, currency enum, payment method details wrapped in PII-safe Secret types, and connector credentials from a creds.json file). The system validates and converts this into the connector's proprietary request format — different serialization (JSON vs XML vs form-encoded), different field names, different amount units — fires it as an HTTP request to the processor's sandbox or production endpoint, then deserializes the raw bytes of the response, maps processor-specific status codes and error codes to canonical enums, and returns a unified PaymentResponse. No state is written anywhere; every call is independent.

Under the hood, the system uses 3 feedback loops, 4 data pools, 5 control points to manage its runtime behavior.

A 8-component library. 1472 files analyzed. Data flows through 8 distinct pipeline stages.

Hidden Assumptions

Most of what this code assumes is routine. These 2 are the ones most likely to cause trouble here — in plain terms, with what to do about each. The rest are minor; they're under "Show everything".

Worth your attention first

The system needs a file containing secret API keys for every payment processor. It figures out where that file is from an environment variable you have to set yourself. If you forget to set that variable, set it to the wrong path, or use a file that's missing keys for the processor you're trying to use, the system quietly tries to connect anyway — with blank or missing credentials. The processor rejects it with a confusing error that doesn't say 'your key file is wrong or missing.'

What to do: Before your first real run, double-check that the environment variable pointing to your credentials file is set correctly and that the file actually contains entries for every processor you intend to use.

Worth your attention first

Different currencies work differently: Japanese Yen has no decimal places, Bahraini Dinar has three, most others have two. Each payment processor integration has to know this and convert accordingly. The system has types that help, but there is no automatic check that the conversion was written correctly for every currency a connector might receive. If it's wrong, a charge could go through at 100 times too little or some other wrong amount — and everything looks fine in the logs because the system just echoes back what you originally sent.

What to do: When adding a new processor or testing an existing one with an unusual currency (especially zero-decimal ones like JPY or three-decimal ones like BHD and KWD), run a small test transaction and verify the settled amount in the processor's own dashboard, not just in the API response.

Show everything (8 more)
Contract

When an automation step is told to wait for both a page element and a URL change at the same time, both waits share the same countdown clock. If one of them takes a long time, there may not be enough time left for the other one to complete, causing the whole step to fail even though the page was perfectly fine — just slow.

What to do: If you see flaky failures on steps that wait for two things at once, split them into two separate steps, each with its own timeout, so slowness in one doesn't kill the other.

browser-automation-engine/src/engine/interpreter.ts:executeRule (waitFor case)
Temporal

The Google Pay flow uses a saved login session so you don't have to sign in every time. Google expires these sessions after weeks or months without warning. When that happens, the tool just hangs waiting for a page element that will never appear, then fails with a timeout — giving no hint that you simply need to log in again.

What to do: If Google Pay token generation starts timing out unexpectedly, run the one-time login step again to refresh the saved session before investigating anything else.

browser-automation-engine/src/gpay-token-gen.ts:gpay-token-gen
Environment

The browser automation service needs a full copy of the Chrome browser installed in exactly the right place on the machine. The server starts up and says it's ready even if Chrome isn't there. The error only appears when the first actual job comes in, making it look like a request problem rather than a setup problem.

What to do: After deploying the browser automation service to any new machine or container, send one test request immediately at startup to confirm Chrome is installed and working before relying on it for real jobs.

browser-automation-engine/src/drivers/playwrightDriver.ts:PlaywrightDriverFactory
Scale

Every time someone sends a job to the browser automation service, it starts a brand-new copy of Chrome just for that job and never reuses old ones. If many jobs arrive at once, you end up running many copies of Chrome simultaneously. Each copy uses hundreds of megabytes of memory, and there's no limit — the machine can simply run out of memory and crash.

What to do: If you expect more than a handful of simultaneous requests, put a concurrency limit (a queue or semaphore) in front of the browser engine so it only runs as many Chrome sessions as the host machine can comfortably support.

browser-automation-engine/src/engine/automationEngine.ts:AutomationEngine.run
Ordering

Automation scripts can save a value from one step (like a confirmation code) and use it in a later step. But there's no check that the step which saves the value actually runs before the step that tries to use it. If someone writes the steps in the wrong order, the later step gets an empty value or crashes — with an error message that doesn't explain the ordering problem.

What to do: When writing automation scripts that pass data between steps, always put the step that captures the value before any step that uses it, and test the script end-to-end at least once to catch ordering mistakes.

browser-automation-engine/src/engine/interpreter.ts:executeRule (extract case)
Domain

Some older payment processors send their responses in a text format that starts with a special marker in an older encoding. The system knows how to handle one kind of marker but not others. If a processor uses the older style, the response will fail to parse with an error message about bad data rather than 'wrong text encoding.'

What to do: If you add a legacy or XML-based processor and see mysterious parsing failures on otherwise valid-looking responses, check whether the processor is sending its data in an encoding other than UTF-8.

crates/common/common_utils/src/bytes_utils.rs:strip_utf8_bom
Contract

When a payment processor's firewall or content-delivery network blocks a request, it often replies with a web page saying 'access denied' instead of the expected payment response — but still uses the same HTTP success code. The system tries to interpret that web page as a payment response, fails, and tells you 'parsing failed' with no hint that the processor never even saw the request.

What to do: If you see repeated parsing errors that aren't obviously malformed data, log the raw response body temporarily to check whether you're receiving an HTML page rather than a real processor response.

crates/grpc-server/grpc-server:gRPC handler / connector-integration trait
Environment

The AI tool that writes new payment processor integrations for you assumes the AI service it uses is online, that the specific AI model it asks for still exists, and that your account can handle very large requests. None of this is checked before the job starts. If any of those things are wrong, you may wait a long time before getting an error, and any partial code that was generated might be written to disk in an incomplete state.

What to do: Before running the AI code-generation tool for the first time, verify your API key works and that the configured model name is correct by sending a small test request to the provider.

grace/src/types/config.py:LlmConfig

Open the standalone hidden-assumptions report for hyperswitch-prism →

How Data Flows Through the System

A payment call enters through one of three surfaces: a language SDK (Python/Node/Java/Rust) that speaks gRPC or FFI, the gRPC server directly, or the Rust library linked in-process. In all cases the caller provides a unified payment intent (amount in minor units, currency enum, payment method details wrapped in PII-safe Secret types, and connector credentials from a creds.json file). The system validates and converts this into the connector's proprietary request format — different serialization (JSON vs XML vs form-encoded), different field names, different amount units — fires it as an HTTP request to the processor's sandbox or production endpoint, then deserializes the raw bytes of the response, maps processor-specific status codes and error codes to canonical enums, and returns a unified PaymentResponse. No state is written anywhere; every call is independent.

  1. Receive and validate caller input — The gRPC handler (grpc-server) or FFI entry point (ffi crate) receives the caller's payment intent as a Protobuf message or FFI struct. It deserializes this into the canonical PaymentRequest domain type, checking required fields (amount > 0, valid Currency enum variant, payment method present). Connector credentials are loaded from the file referenced by CONNECTOR_AUTH_FILE_PATH (or injected via the Superposition remote config client if the superposition feature flag is enabled) and deserialized into the typed ConnectorCredentials struct. [ConnectorCredentials → PaymentRequest (canonical)] (config: aci.api_key.value, adyen.api_key.value)
  2. Route to connector implementation — The canonical PaymentRequest plus credentials are dispatched to the specific connector crate determined by the connector field in the request (e.g. 'stripe', 'adyen'). The connector registry (in composite-service or the gRPC server) maps the connector name string to the concrete Rust type that implements the connector-integration trait. This is a static dispatch — no runtime reflection; the mapping is exhaustive over all compiled-in connectors. [PaymentRequest (canonical) → PaymentRequest (canonical)]
  3. Transform canonical request to processor wire format — Inside the connector crate, the PaymentRequest is converted into the processor's proprietary request struct via TryFrom / Into implementations. This is where currency/amount unit conversion happens (MinorUnit → StringMajorUnit for processors like Stripe that want '10.00'), where card data is formatted according to the processor's schema, where processor-specific metadata fields are set, and where authentication headers (API key, HMAC, OAuth token) are injected from the ConnectorCredentials. The result is serialized to bytes: JSON for most connectors, XML for legacy processors like WorldPay XML, multipart form for some. [PaymentRequest (canonical) → ConnectorRequest (per-processor)]
  4. Dispatch HTTP request to payment processor — The serialized ConnectorRequest bytes plus headers are wrapped in a common_utils::Request struct (method, url, headers, body) and sent to the processor's endpoint via the caller-provided HTTP client (in SDKs, this is the HttpRequest/HttpResponse dataclass pair in sdk/python/src/payments/http_client.py; in the gRPC server, it is the external-services HTTP client). Latency is measured and, if Kafka is enabled, the raw request/response pair is published to the tracing-kafka topic via connector_request_kafka. [ConnectorRequest (per-processor) → HttpRequest / HttpResponse (SDK transport)]
  5. Parse processor HTTP response — The raw HTTP response bytes are stripped of any UTF-8 BOM (bytes_utils::strip_utf8_bom handles processors that include BOMs), then deserialized into the connector-specific response struct (ConnectorResponse) using serde_json or a custom XML deserializer. HTTP 4xx/5xx status codes that indicate processor-level errors are mapped to the appropriate error variant before deserialization. Response body masking (connector_response_masking) redacts sensitive fields (card numbers, CVVs) before the bytes are logged. [HttpRequest / HttpResponse (SDK transport) → ConnectorResponse (per-processor)]
  6. Map to canonical PaymentResponse — The ConnectorResponse is converted to the canonical PaymentResponse via the connector's response mapping implementation. Processor-specific status strings (e.g. Stripe's 'requires_capture', Adyen's 'AUTHORISED') are mapped to the PaymentStatus enum. Processor error codes are mapped to ConnectorError with a canonical code, human-readable message, and optional raw reason. If the response includes a redirect URL (for 3DS or bank redirect flows), it is wrapped in a RedirectForm. Amount and currency are echoed back in MinorUnit. [ConnectorResponse (per-processor) → PaymentResponse (canonical)]
  7. Return canonical response to caller — The canonical PaymentResponse is serialized back to the caller's transport format: Protobuf for the gRPC server, FFI struct for native bindings, or a language-native object for SDK callers. The gRPC server writes the response to the stream; the gRPC client SDK (rust-grpc-client, sdk/python, sdk/java) deserializes it into the SDK's payment status/error types. At this point the call is complete — no state has been written and no PII has been persisted. [PaymentResponse (canonical)]
  8. Browser automation: execute DSL script — For wallet token generation (Apple Pay via applepay-token-gen.ts, Google Pay via gpay-token-gen.ts) or integration testing, a separate flow is triggered: a RunRequest JSON with a url and a rules array is POSTed to the browser-automation-engine server (POST /run). AutomationEngine opens a Playwright session, navigates to the URL, then calls interpreter.ts's executeRule for each rule in sequence. Extracted values (e.g. the PKPaymentToken from Apple Pay's JS callback) accumulate in ctx.data keyed by the rule's 'as' field and are returned in RunResponse.data. [RunRequest / RunResponse (browser automation) → RunRequest / RunResponse (browser automation)] (config: browser_headless, slowMoMs, defaultTimeoutMs)

Data Models

The data structures that flow between stages — the contracts that hold the system together.

PaymentRequest (canonical) crates/types-traits/domain_types
Struct containing: amount: MinorUnit (integer cents), currency: Currency (ISO 4217 enum), payment_method: PaymentMethod enum (Card { number, expiry, cvv wrapped in PII Secret types } | Wallet | BankTransfer | ...), customer: Option<CustomerDetails>, metadata: Option<serde_json::Value>, connector_metadata: Option<serde_json::Value>
Created by the SDK or gRPC handler from the caller's input; passed to the connector's transform function; never persisted or logged (PII fields are masked)
ConnectorRequest (per-processor) crates/integrations/connector-integration
Connector-specific Rust struct (e.g. StripePaymentRequest { amount: i64, currency: String, payment_method_data: StripePaymentMethodData, ... }) serialized to JSON/XML/form-urlencoded bytes depending on the processor; shape differs per connector
Created inside each connector's TryFrom<PaymentRequest> implementation; serialized to bytes and sent as the HTTP body; discarded after the HTTP call returns
ConnectorResponse (per-processor) crates/integrations/connector-integration
Connector-specific struct deserialized from the processor's HTTP response bytes (e.g. StripePaymentResponse { id: String, status: StripeStatus, amount: i64, error: Option<StripeError> }); shape differs per connector
Deserialized from raw HTTP response bytes inside the connector crate; immediately converted to canonical PaymentResponse and discarded
PaymentResponse (canonical) crates/types-traits/domain_types
Struct with: status: PaymentStatus enum (Authorized | Captured | Failed | Pending | ...), connector_transaction_id: Option<String>, amount: MinorUnit, error: Option<ConnectorError { code: String, message: String, reason: Option<String> }>, redirection: Option<RedirectForm>
Produced by each connector's response mapping; returned through gRPC/FFI/SDK to the application; the only output crossing the library boundary
HttpRequest / HttpResponse (SDK transport) sdk/python/src/payments/http_client.py
HttpRequest: dataclass { url: str, method: str, headers: Optional[Dict[str,str]], body: Optional[bytes] }. HttpResponse: dataclass { status_code: int, headers: Dict[str,str], body: bytes, latency_ms: float }
Constructed by the UCS transformation inside the SDK; executed by the caller-provided HTTP client; response bytes passed back to the UCS for deserialization
RunRequest / RunResponse (browser automation) browser-automation-engine/src/types/api.ts
RunRequest: { url: string, rules: Rule[], options?: RunOptions { headless, slowMoMs, defaultTimeoutMs, navigationTimeoutMs, screenshotDir, viewport } }. RunResponse: RunSuccessResponse { success: true, data: Record<string,unknown>, finalUrl, steps: StepResult[], durationMs } | RunFailureResponse { success: false, failedStep: number, error, ... }
Parsed from JSON (HTTP body or file), executed step-by-step in Playwright, results accumulated in StepResult[] and returned as RunResponse
Rule / DSL types (browser automation) browser-automation-engine/src/types/dsl.ts
Discriminated union: GotoRule | ClickRule { selector } | FillRule { selector, value } | PressRule { selector, key } | WaitForRule { selector?, state?, urlContains? } | AssertTextRule { selector, text, match?, caseSensitive? } | ExtractRule { selector, as, attribute?, trim? } | ExtractAllRule | ScreenshotRule | EvaluateRule; all have action: RuleAction and optional timeoutMs
Parsed from the RunRequest.rules array; executed in order by interpreter.ts's executeRule switch; extracted values are accumulated in ctx.data keyed by the 'as' field
ConnectorCredentials crates/internal/connector-creds
JSON object keyed by connector name, each entry containing typed secrets (e.g. { api_key: { value: string }, merchant_account: { value: string } }); loaded from creds.json at runtime
Loaded from a file path (CONNECTOR_AUTH_FILE_PATH env var) at startup or per-request; never logged; values are wrapped in Secret<> types to prevent accidental serialization

System Behavior

How the system operates at runtime — where data accumulates, what loops, what waits, and what controls what.

Data Pools

ConnectorCredentials file (creds.json) (file-store)
JSON file containing API keys, merchant account IDs, and other secrets for every connector. Loaded at request time (or startup) and deserialized into typed Secret<> credential structs. The dummy template (creds_dummy.json) shows the expected shape for all 100+ connectors. Never logged or persisted beyond the request lifetime.
Kafka request/response event stream (queue)
When compiled with the 'kafka' feature, raw connector HTTP request/response pairs (with sensitive fields masked by connector_response_masking) are published as events to a Kafka topic. This is the primary audit/observability trail — no other persistent log of payment activity exists in the library.
Browser screenshot directory (file-store)
Directory where Playwright writes PNG screenshots when a 'screenshot' DSL rule is executed or when a step fails (for debugging). Defaults to ./screenshots relative to the process working directory. Path is configurable per RunRequest via options.screenshotDir.
WebKit browser profile (Google Pay session) (state-store)
Persistent browser profile (cookies + localStorage) saved by gpay-login.ts after a one-time Google account sign-in. Loaded by gpay-token-gen.ts on subsequent runs to skip re-authentication. Google expires this session after weeks/months, requiring a new sign-in.

Feedback Loops

Delays

Control Points

Technology Stack

Rust (runtime)
Primary implementation language for all connector integrations, type definitions, gRPC server, and FFI layer; workspace managed with Cargo
Tonic / prost (framework)
gRPC server framework (Tonic) and Protobuf code generator (prost-build) used in crates/grpc-server and crates/types-traits/grpc-api-types to define and serve the connector RPC API
UniFFI (library)
Mozilla's FFI toolkit (crates/ffi/ffi, crates/internal/uniffi-bindgen) that generates Python, Kotlin, and Swift bindings from Rust; enables non-gRPC SDK integration
serde / serde_json (serialization)
JSON serialization/deserialization throughout — connector request/response structs, configuration files, SDK types all use serde derives
Playwright (TypeScript) (testing)
Browser automation framework used by the browser-automation-engine to drive Chromium (and WebKit for Google Pay) for wallet token generation and integration testing
Fastify (TypeScript) (framework)
HTTP server framework for the browser-automation-engine's POST /run endpoint
Superposition provider (infra)
Remote config client (crates/common/common_utils/src/superposition_config.rs) that polls a Superposition service for connector credential patches and applies them at runtime; pinned to version =0.116.0
Python (LiteLLM / Firecrawl) (library)
Used by the Grace AI agent (grace/src) to generate new connector integration code: Firecrawl crawls processor API docs, LiteLLM proxies LLM calls to Qwen3-Coder or GLM models

Key Components

Explore the interactive analysis

See the full architecture map, data flow, and code patterns visualization.

Analyze on CodeSea

Related Library Repositories

Frequently Asked Questions

What is hyperswitch-prism used for?

Translates a single payment API call into any of 100+ processor-specific wire formats juspay/hyperswitch-prism is a 8-component library written in Rust. Data flows through 8 distinct pipeline stages. The codebase contains 1472 files.

How is hyperswitch-prism architected?

hyperswitch-prism is organized into 6 architecture layers: Canonical Type Layer, Connector Integration Layer, Transport / Delivery Layer, Browser Automation Engine, and 2 more. Data flows through 8 distinct pipeline stages. This layered structure keeps concerns separated and modules independent.

How does data flow through hyperswitch-prism?

Data moves through 8 stages: Receive and validate caller input → Route to connector implementation → Transform canonical request to processor wire format → Dispatch HTTP request to payment processor → Parse processor HTTP response → .... A payment call enters through one of three surfaces: a language SDK (Python/Node/Java/Rust) that speaks gRPC or FFI, the gRPC server directly, or the Rust library linked in-process. In all cases the caller provides a unified payment intent (amount in minor units, currency enum, payment method details wrapped in PII-safe Secret types, and connector credentials from a creds.json file). The system validates and converts this into the connector's proprietary request format — different serialization (JSON vs XML vs form-encoded), different field names, different amount units — fires it as an HTTP request to the processor's sandbox or production endpoint, then deserializes the raw bytes of the response, maps processor-specific status codes and error codes to canonical enums, and returns a unified PaymentResponse. No state is written anywhere; every call is independent. This pipeline design reflects a complex multi-stage processing system.

What technologies does hyperswitch-prism use?

The core stack includes Rust (Primary implementation language for all connector integrations, type definitions, gRPC server, and FFI layer; workspace managed with Cargo), Tonic / prost (gRPC server framework (Tonic) and Protobuf code generator (prost-build) used in crates/grpc-server and crates/types-traits/grpc-api-types to define and serve the connector RPC API), UniFFI (Mozilla's FFI toolkit (crates/ffi/ffi, crates/internal/uniffi-bindgen) that generates Python, Kotlin, and Swift bindings from Rust; enables non-gRPC SDK integration), serde / serde_json (JSON serialization/deserialization throughout — connector request/response structs, configuration files, SDK types all use serde derives), Playwright (TypeScript) (Browser automation framework used by the browser-automation-engine to drive Chromium (and WebKit for Google Pay) for wallet token generation and integration testing), Fastify (TypeScript) (HTTP server framework for the browser-automation-engine's POST /run endpoint), and 2 more. A focused set of dependencies that keeps the build manageable.

What system dynamics does hyperswitch-prism have?

hyperswitch-prism exhibits 4 data pools (ConnectorCredentials file (creds.json), Kafka request/response event stream), 3 feedback loops, 5 control points, 3 delays. The feedback loops handle polling and polling. These runtime behaviors shape how the system responds to load, failures, and configuration changes.

What design patterns does hyperswitch-prism use?

5 design patterns detected: Typestate-enforced amount safety, PII masking via Secret newtypes, Derive-macro config patching, DSL interpreter for browser automation, Adapter pattern per connector.

Analyzed on September 14, 2026 by CodeSea. Written by .