Universal payment gateway infrastructure and TypeScript SDK for Pakistan.
Unifying mobile wallets into a single, type-safe, developer-first API.
Explore Packages • Quickstart • Error Diagnostics • Repository Structure
Integrating digital payment gateways in Pakistan traditionally requires managing disparate HMAC-SHA256 signature algorithms, custom string concatenation rules, non-standard timestamp formats, paisa conversions, and inconsistent banking error response codes across providers.
RawaPay abstracts this complexity behind a modular monorepo architecture and a unified, Stripe-inspired API for JazzCash and EasyPaisa Mobile Wallets.
Note
Currently, RawaPay supports direct single-charge Mobile Wallet deductions (USSD push prompt and mobile PIN authorization). Recurring billing and card schemes will be supported in upcoming iterations.
The core engineering work of RawaPay lives inside the packages/ directory. The codebase is decoupled into focused, standalone libraries designed for maximum flexibility:
packages/
├── core/ # @rawapay/core - Primitives, types, schemas, errors & validation
├── jazzcash/ # @rawapay/jazzcash - JazzCash MWALLET driver & HMAC-SHA256 engine
├── easypaisa/ # @rawapay/easypaisa - EasyPaisa MA driver & REST authentication
└── rawapay/ # rawapay - Unified umbrella SDK bundling all packages
| Package | Role | Purpose |
|---|---|---|
rawapay |
Umbrella SDK | The complete package developers install. Exposes the unified RawaPay client, dynamic driver routing, and re-exports all drivers and core utilities. |
@rawapay/core |
Core Engine | Zero-dependency foundational layer: Safe Result pattern (Result<T, E>), normalized Payment entity, RawaPayError, server guards, and Pakistani format validators. |
@rawapay/jazzcash |
Provider Driver | JazzCash Mobile Wallet (MWALLET) driver. Handles payload ordering, HMAC-SHA256 hashing, USSD dispatch, inquiry APIs, and webhook signature verification. |
@rawapay/easypaisa |
Provider Driver | EasyPaisa Mobile Account (MA) driver. Handles REST endpoint communication, Basic Auth credentials generation, status inquiry, and error code translation. |
Tip
Most developers will only need the all-in-one rawapay package. If you are building a microservice dedicated solely to JazzCash or EasyPaisa, you can optionally install @rawapay/jazzcash or @rawapay/easypaisa alongside @rawapay/core for a minimal dependency footprint.
- Unified Client Interface: Initialize once and charge any supported wallet without writing nested
if/elseorswitchstatements across your codebase. - Safe Result Pattern: No unexpected uncaught exceptions. Operations return a type-narrowed
{ data, error }object inspired by Go and Rust. - Normalized Error Matrix: Translates proprietary gateway error codes (e.g. JazzCash
119and EasyPaisa0013) into standard codes likeINSUFFICIENT_FUNDS,ACCOUNT_NOT_FOUND, andLIMIT_EXCEEDED. - Intelligent Retry Diagnostics:
error.isRetryableexplicitly identifies transient failures (such as customer USSD timeouts or bank gateway 502s) versus terminal rejections (wrong PIN, invalid credentials). - Server-Only Security Guard: Built-in runtime checks prevent accidental imports into client bundles, protecting merchant passwords, store IDs, and integrity salts from leaking to browsers.
- Built-in Format Validation: Lightweight validators for Pakistani phone numbers (
03xxxxxxxxx), CNIC suffixes, and paisa conversion with zero external validation dependencies.
Install the unified SDK:
# Using bun
bun add rawapay
# Using npm
npm install rawapay
# Using pnpm
pnpm add rawapayStore your gateway credentials in your server environment:
# JazzCash (Sandbox or Production)
JAZZCASH_MERCHANT_ID="MC12345"
JAZZCASH_PASSWORD="your_api_password"
JAZZCASH_INTEGRITY_SALT="your_integrity_salt"
# EasyPaisa (Sandbox or Production)
EASYPAISA_STORE_ID="12345"
EASYPAISA_USERNAME="your_username"
EASYPAISA_PASSWORD="your_password"Instantiate RawaPay once in a server-side module:
import { RawaPay } from "rawapay";
export const pay = new RawaPay({
options: {
environment: "sandbox", // "sandbox" | "production" (defaults to "sandbox")
timeoutMs: 30000,
},
providers: {
jazzcash: {
merchantId: process.env.JAZZCASH_MERCHANT_ID!,
password: process.env.JAZZCASH_PASSWORD!,
integritySalt: process.env.JAZZCASH_INTEGRITY_SALT!,
},
easypaisa: {
storeId: process.env.EASYPAISA_STORE_ID!,
username: process.env.EASYPAISA_USERNAME,
password: process.env.EASYPAISA_PASSWORD,
},
},
});JazzCash requires the customer's mobile number and the last 6 digits of their CNIC (or full 13-digit CNIC) to initiate the USSD authorization prompt on their mobile device:
const { data, error } = await pay.payment.create({
provider: "jazzcash",
amount: 2500, // Amount in PKR
phone: "03001234567",
cnic: "123456", // Last 6 digits or full 13-digit CNIC
description: "Order #1042",
});
if (error) {
console.error(`Payment failed [${error.code}]:`, error.message);
return;
}
// Initial direct wallet creation returns status: "pending"
// while awaiting customer MPIN entry
console.log("Transaction ID:", data.id);
console.log("Status:", data.status); // "pending"EasyPaisa requires only the customer's mobile number:
const { data, error } = await pay.payment.create({
provider: "easypaisa",
amount: 3200, // Amount in PKR
phone: "03451234567",
description: "Order #1043",
});
if (error) {
console.error(`Payment failed [${error.code}]:`, error.message);
return;
}
console.log("Transaction ID:", data.id);
console.log("Status:", data.status); // "succeeded"Query transaction settlement across providers with a single call:
const { data, error } = await pay.payment.status({
provider: "jazzcash", // or "easypaisa"
paymentId: "T20260926101234123456",
});
if (data?.status === "succeeded") {
// Safe to mark order as paid
}Protect your server endpoints against forged callbacks. JazzCash HMAC-SHA256 signatures are verified using constant-time comparisons:
export async function handleJazzCashWebhook(req: Request) {
const body = await req.json();
// Validate cryptographic signature against integritySalt
const isValid = pay.verifyCallback("jazzcash", body);
if (!isValid) {
return new Response("Unauthorized signature", { status: 400 });
}
if (body.pp_ResponseCode === "000") {
// Transaction authorized by customer
await fulfillOrder(body.pp_TxnRefNo);
}
return new Response("OK");
}Important
Standard EasyPaisa Mobile Account callbacks do not carry symmetric HMAC signatures. To confirm transaction finality with EasyPaisa, perform a server-to-server inquiry using pay.payment.status({ provider: "easypaisa", paymentId }) or restrict callback access to EasyPaisa's IP ranges.
The top-level developer package. Coordinates configured providers through an internal driver registry.
- Client Class:
RawaPay - Dynamic Routing: Dispatches
createandstatuscalls to the appropriate driver usingoptions.provider. - Direct Driver Access: Access underlying driver instances using
pay.getDriver<JazzCashDriver>("jazzcash"). - Re-exports: Automatically exposes all types, errors, driver classes, crypto utilities, and validators from
@rawapay/core,@rawapay/jazzcash, and@rawapay/easypaisa.
The foundation for all RawaPay drivers and client operations. Contains no external dependencies.
- Safe Result Primitives:
Result<T, E>success(data)andfailure(error)
- Normalized Data Models:
Payment: Standard transaction entity returned by all drivers (id,provider,status,amount,currency,responseCode,responseMessage,retrievalRefNo,raw,createdAt).PaymentStatus:"succeeded" | "pending" | "processing" | "failed" | "cancelled" | "expired".
- Error Engine:
RawaPayError: Custom error class with enumerablemessage, normalizedRawaPayErrorCode, HTTPstatusCode,rawCode,rawResponse, andisRetryable.- Serializer:
error.toJSON()prevents serialization loss in JSON APIs and Next.js Server Actions.
- Security:
assertServerEnv(context): Guarantees that code executing sensitive operations runs solely in server runtimes (Node.js, Bun, Edge).
- Pakistani Format Validators & Normalizers:
phoneSchema: Validates and normalizes03xxxxxxxxx,+923xxxxxxxxx,923xxxxxxxxx, and3xxxxxxxxxinto a clean 11-digit string.cnicSchema: Validates and extracts the 6-digit suffix required for mobile wallet authorization from either a 6-digit string or a full 13-digit CNIC (42101-1234567-1).positiveAmountSchema: Enforces valid positive transaction limits up to PKR 10,000,000.- Utility helpers:
formatToPaisa(),formatDateTime()(guarantees Asia/Karachi PKT UTC+5 timestamp synchronization), andgenerateTxnRefNo().
Driver implementation for JazzCash API v2.0.
- Driver Class:
JazzCashDriver - Supported Flow: Direct Mobile Wallet (
MWALLET) transactions viaDoMWalletTransaction. - Status Inquiry: Transaction status verification via
PaymentInquiry. - Cryptographic Engine (
crypto.ts):- Alphabetical key sorting and parameter concatenation with
pp_IntegritySalt. - Uppercase hexadecimal HMAC-SHA256 signature calculation (
calculateSecureHash). - Constant-time verification (
verifySecureHash) viacrypto.timingSafeEqualto safeguard against timing attacks.
- Alphabetical key sorting and parameter concatenation with
- Error Mapping (
errors.ts): Maps 3-digitpp_ResponseCodevalues (101,115,116,118,119,120,122,157,999, etc.) into standardizedRawaPayErrorcodes.
Driver implementation for EasyPaisa REST API v4.
- Driver Class:
EasyPaisaDriver - Supported Flow: Direct Mobile Account (
MA) transactions viainitiate-ma-transaction. - Status Inquiry: Direct reconciliation via
inquire-transaction. - Header Authentication: Manages Base64 credential token formatting across
CredentialsandAuthorization: Basicheaders. - Error Mapping (
errors.ts): Maps 4-digitresponseCodevalues (0001,0002,0006,0007,0013,0014,0017, etc.) into standardizedRawaPayErrorcodes.
RawaPay maps distinct gateway error responses into a single standardized error matrix:
{
"name": "RawaPayError",
"code": "INSUFFICIENT_FUNDS",
"message": "Customer mobile account has insufficient balance to complete this transaction. (119)",
"provider": "jazzcash",
"statusCode": 402,
"rawCode": "119",
"isRetryable": false,
"rawResponse": {
"pp_ResponseCode": "119",
"pp_ResponseMessage": "Customer mobile account has insufficient balance"
}
}Using error.isRetryable, background jobs and API consumers can immediately determine whether an operation is eligible for retry:
const { data, error } = await pay.payment.create(params);
if (error) {
if (error.isRetryable) {
// Customer timed out on USSD prompt (122) or gateway dropped connection (502)
// Safe to queue for retry or invite the customer to request a new prompt
await scheduleRetry();
} else {
// Terminal failure: wrong PIN (120), invalid CNIC, or insufficient funds (119/0013)
await markOrderFailed(error.message);
}
}rawapay/
├── apps/
│ ├── docs/ # Full documentation website (Next.js, Fumadocs)
│ └── web/ # Interactive playground & demo checkout application (Next.js, TailwindCSS)
├── packages/
│ ├── config/ # Shared tsconfig and lint rules (@rawapay/config)
│ ├── core/ # Foundational primitives, error engine, validators (@rawapay/core)
│ ├── easypaisa/ # EasyPaisa REST v4 driver (@rawapay/easypaisa)
│ ├── jazzcash/ # JazzCash API v2.0 driver & crypto engine (@rawapay/jazzcash)
│ └── rawapay/ # Unified public SDK client (rawapay)
├── package.json # Monorepo workspaces definition
└── turbo.json # Turborepo task pipeline configuration
apps/docs: Documentation portal detailing guides, API references, error tables, and provider specifications.apps/web: Demonstration app and sandbox testing playground for inspecting live checkout flows.
RawaPay uses Bun and Turborepo to manage workspace builds and package dependencies.
# Clone the repository
git clone https://github.com/alihamas/rawapay.git
cd rawapay
# Install monorepo dependencies
bun install| Script | Description |
|---|---|
bun run dev |
Starts all applications (docs and web) in development mode |
bun run dev:web |
Starts only the playground web application |
bun run build |
Builds all packages and applications via Turborepo |
bun run check-types |
Executes TypeScript type-checks across all workspaces |
bun run check |
Runs Oxlint and Oxfmt for linting and code formatting |