Easypaisa is the second half of Pakistan’s mobile-wallet duopoly. If you’ve already got JazzCash on your WooCommerce checkout, adding Easypaisa is a 15–20% conversion lift for buyers who happen to carry the Telenor wallet instead of the Jazz one. If you haven’t done either yet, most Pakistani stores ship both together on day one.
This guide mirrors the JazzCash post — same structure, Easypaisa-specific details. You’ll get a plugin route (live in under an hour) and a custom-code route using the Easypay Merchant Services API. Working code, real error codes, and a sandbox-testing workflow that won’t burn your production credentials.
If you haven’t seen the JazzCash write-up yet, start there — a lot of the WooCommerce gateway scaffolding carries over one-for-one: How to Integrate JazzCash with WooCommerce.
Who this guide is for
- WooCommerce store owners in Pakistan who want Easypaisa alongside JazzCash / COD / bank transfer
- Developers with a Pakistani client asking for Easypaisa checkout
- Anyone who’s hit Easypay’s cryptic “—27—” hash error and needs the fix
Easypaisa’s two merchant products
Easypaisa for business (officially branded “Easypay” for merchant services) offers two checkout flows. You choose per transaction — the customer doesn’t see the branding difference.
- Mobile Account (MA) — Customer enters their Easypaisa mobile number. An OTP lands on their phone; they confirm and funds move from wallet to merchant. Most Pakistani DTC stores enable this as the default.
- Over-the-Counter (OTC) / Voucher — Customer gets a numeric voucher at checkout valid for 72 hours. They walk into any Easypaisa shop (there are tens of thousands, including every second kiryana store) and pay cash. Useful for unbanked buyers who still want to avoid COD’s return risk.
Unlike JazzCash, Easypaisa does not route card payments through the same integration — for cards you’ll use a separate acquirer (HBL, Meezan, or an aggregator like PayFast/2C2P).
What you’ll need before writing a line of code
- Easypaisa merchant account — apply via easypaisa.com.pk/business or your Telenor Microfinance Bank relationship manager. Approval takes 7–15 working days. You’ll submit NTN, CNIC, a business bank account, and a signed agreement.
- Credentials they issue once approved:
Store ID(sometimes called Merchant ID)Hash Key(for HMAC request signing)- Optional
Account Numberfor direct settlement configuration
- A working WordPress + WooCommerce install (WP 6.0+, WC 8.0+).
- HTTPS on production — Easypay rejects non-HTTPS
postBackURLs silently. - Sandbox credentials — usually emailed separately to your merchant-onboarding contact. Ask for them explicitly; Easypaisa doesn’t auto-issue sandbox keys.
Keep sandbox and production credentials labelled clearly. Easypay’s sandbox lives at
https://easypaystg.easypaisa.com.pk/...and production athttps://easypay.easypaisa.com.pk/.... Swapping by accident is the single biggest cause of “Invalid Store” rejections.
Option 1: The plugin route
Fast path if you just need it working.
Plugins worth trying
- Easypaisa Payment Gateway for WooCommerce — the plugin some merchants receive a link to during onboarding. MA + OTC support, sandbox toggle, callback handling. Free.
- PayFast Pakistan — the same aggregator mentioned in the JazzCash post. Wires Easypaisa + JazzCash + HBL + Meezan through one integration. Free to install; per-transaction cut added on top.
- Pakistani studio plugins — custom-built Easypaisa plugins sold for PKR 5,000–15,000. Worth it only if they ship proper IPN verification and order-status sync out of the box.
Installation (using the official plugin)
- Download the plugin ZIP from the merchant portal (or the email your onboarding contact sent).
- WP Admin → Plugins → Add New → Upload Plugin → ZIP → Install → Activate.
- WooCommerce → Settings → Payments. Toggle Easypaisa on.
- Click Manage. Fill in:
- Store ID
- Hash Key
- Account Number (if required by your merchant config)
- Post-back URL (auto-populated; leave it)
- Enable MA / Enable OTC — tick both for maximum coverage
- Mode: Sandbox for testing
- Save.
Place a test order. For the sandbox MA number, Easypaisa usually issues a test MSISDN along with the sandbox creds — use that one, not a real number. WooCommerce should mark the order Processing on success, Failed on decline.
Where plugins fall short
- OTC voucher lifecycle — most free plugins mark the order “On hold” when a voucher is issued but never auto-update it when the customer actually pays. You’ll reconcile manually or write a cron.
- No IPN verification on some of the older plugins — callbacks are trusted by URL alone, which is a security hole.
- Poor error surfacing — when Easypay returns an error code, plugins often show WooCommerce’s generic “Payment declined” with no detail in the order note.
Option 2: The custom-code route
Drop this in wp-content/plugins/asad-easypaisa/asad-easypaisa.php and activate. It’s a minimum-viable MA gateway — the same pattern works for OTC with two field changes.
Step 1: Register the gateway
<?php
/**
* Plugin Name: Asad — Easypaisa WooCommerce Gateway
* Description: Custom Easypaisa (Mobile Account) gateway for WooCommerce.
* Version: 1.0.0
*/
add_action('plugins_loaded', 'asad_ep_init_gateway', 11);
function asad_ep_init_gateway() {
if (!class_exists('WC_Payment_Gateway')) return;
class Asad_Easypaisa_Gateway extends WC_Payment_Gateway {
public function __construct() {
$this->id = 'asad_easypaisa';
$this->method_title = 'Easypaisa (Mobile Account)';
$this->method_description = 'Accept Easypaisa wallet payments in Pakistan.';
$this->has_fields = false;
$this->init_form_fields();
$this->init_settings();
$this->title = $this->get_option('title');
$this->store_id = $this->get_option('store_id');
$this->hash_key = $this->get_option('hash_key');
$this->sandbox = $this->get_option('sandbox') === 'yes';
add_action('woocommerce_update_options_payment_gateways_' . $this->id, array($this, 'process_admin_options'));
add_action('woocommerce_api_asad_easypaisa_callback', array($this, 'handle_callback'));
}
public function init_form_fields() {
$this->form_fields = array(
'enabled' => array('title' => 'Enable', 'type' => 'checkbox', 'default' => 'no'),
'title' => array('title' => 'Checkout label', 'type' => 'text', 'default' => 'Easypaisa Mobile Account'),
'store_id' => array('title' => 'Store ID', 'type' => 'text'),
'hash_key' => array('title' => 'Hash Key', 'type' => 'password'),
'sandbox' => array('title' => 'Sandbox mode', 'type' => 'checkbox', 'default' => 'yes'),
);
}
public function process_payment($order_id) {
$order = wc_get_order($order_id);
return array(
'result' => 'success',
'redirect' => add_query_arg(
$this->build_payload($order),
$this->get_checkout_url()
),
);
}
}
}
add_filter('woocommerce_payment_gateways', function ($gateways) {
$gateways[] = 'Asad_Easypaisa_Gateway';
return $gateways;
});
Step 2: Build the signed payload
Easypay signs requests with HMAC-SHA256. The string to sign is a concatenation of specific fields in a fixed order, joined by &, then hashed with your Hash Key. Get the field order wrong and you’ll see the infamous error -27.
private function build_payload($order) {
$ref = 'EP' . time() . $order->get_id();
$amount = number_format((float) $order->get_total(), 2, '.', '');
$data = array(
'storeId' => $this->store_id,
'orderRefNum' => $ref,
'transactionAmount'=> $amount,
'postBackURL' => home_url('/wc-api/asad_easypaisa_callback/'),
'mobileAccountNo' => '', // filled on Easypay-hosted form
'emailAddr' => $order->get_billing_email(),
'transactionType' => 'MA',
'tokenExpiry' => '', // default expiry, ~30 min
'bankIdentificationNumber' => '',
'encryptedHashRequest' => '',
'merchantPaymentMethod' => '',
);
$data['merchantHashedReq'] = $this->build_hash($data);
$data['asad_order_id'] = $order->get_id();
return $data;
}
private function build_hash($d) {
// Easypay documented field order (verify against the current PDF your
// merchant rep sends — it occasionally gets tweaked on version bumps).
$string = 'amount=' . $d['transactionAmount']
. '&postBackURL=' . $d['postBackURL']
. '&orderRefNum=' . $d['orderRefNum']
. '&storeId=' . $d['storeId']
. '&transactionType=' . $d['transactionType'];
return base64_encode(hash_hmac('sha256', $string, $this->hash_key, true));
}
private function get_checkout_url() {
return $this->sandbox
? 'https://easypaystg.easypaisa.com.pk/easypay/Index.jsf'
: 'https://easypay.easypaisa.com.pk/easypay/Index.jsf';
}
Step 3: Handle the callback (IPN)
Easypay POSTs the result to your postBackURL with a fresh hash. Verify it before marking the order paid — otherwise the endpoint is trivially forgeable.
public function handle_callback() {
$data = $_POST ?: $_GET;
if (empty($data['orderRefNumber']) && empty($data['orderRefNum'])) { exit; }
$ref = $data['orderRefNumber'] ?? $data['orderRefNum'];
// Rebuild the response-hash in the order Easypay documents for callbacks
$string = 'orderRefNumber=' . $ref
. '&responseCode=' . ($data['responseCode'] ?? '')
. '&responseDesc=' . ($data['responseDesc'] ?? '')
. '&storeId=' . $this->store_id;
$expected = base64_encode(hash_hmac('sha256', $string, $this->hash_key, true));
if (!hash_equals($expected, $data['merchantHashedResp'] ?? '')) {
wp_die('Easypaisa hash mismatch — possible tampering.', 403);
}
$order_id = isset($data['asad_order_id']) ? (int) $data['asad_order_id'] : 0;
$order = wc_get_order($order_id);
if (!$order) { wp_die('Order not found.', 404); }
if (($data['responseCode'] ?? '') === '0000') {
$order->payment_complete($ref);
$order->add_order_note('Easypaisa payment successful. Ref: ' . $ref);
wp_safe_redirect($this->get_return_url($order));
} else {
$order->update_status('failed', 'Easypaisa declined: ' . ($data['responseDesc'] ?? 'unknown'));
wp_safe_redirect(wc_get_checkout_url());
}
exit;
}
The OTC flow is nearly identical — swap transactionType from MA to OTC, then on callback treat responseCode = 0000 as “voucher issued” (set order status to “On hold”) and wait for a second callback when the customer actually pays at a shop.
Testing workflow
- Start in sandbox mode. Use the test MSISDN your onboarding rep issued.
- Run both flows end-to-end:
- MA success — order goes straight to Processing
- MA failure (wrong OTP) — order goes to Failed
- OTC issue + pay — order goes On hold, then Processing when the shop-side callback fires
- Flip to live mode. Make a real PKR 1 transaction against your own Easypaisa. Refund from the merchant dashboard after.
- Watch the first week of real orders. Production edge cases — customer cancels, network drops, double-click checkout — almost never show up in sandbox.
Common failures and how to fix them
| Error | Real cause | Fix |
|---|---|---|
-27 — Hash Mismatch | Field order in signing string doesn’t match Easypay’s expected order | Copy the field order exactly from the latest merchant PDF — it occasionally changes on version bumps |
-01 — Invalid Store | Sandbox creds used against live URL (or vice versa) | Match credential set to endpoint; never share a single $store_id across both |
| Customer completes OTP but order stays “Pending payment” | Postback URL blocked by security plugin or cached by CDN | Whitelist /wc-api/asad_easypaisa_callback/ in Wordfence / Cloudflare Cache Rules |
| OTC voucher issued but order never completes | Second callback (on shop-side payment) not being processed | Run a daily Status Enquiry cron against Easypay’s status API to catch missed callbacks |
| Paid orders on Easypaisa but not reflected in WooCommerce | Callback verification silently failing — probably CDN stripping POST body | Disable CDN/cache on /wc-api/; log incoming callback payloads for a day to debug |
Security — the non-negotiable checklist
- Store the Hash Key as a
wp-config.phpconstant, not in the plugin’s settings UI where it ends up in the database in plain text. - Never log full callback bodies — they contain enough to forge future callbacks if a log file leaks.
- Reject any callback over HTTP. Easypay’s production endpoints use HTTPS; anything else is suspicious.
- Verify
merchantHashedRespon every callback. A response code of0000alone is not enough — that field is trivially spoofable from curl. - Daily reconciliation cron: pull the Easypay transaction list and compare against WooCommerce orders for the previous 24 hours. Two-minute job, saves weeks of finance-team angst.
Going further
Things you’ll want within a few months of launch:
- Status Enquiry API for catching missed callbacks (especially for OTC vouchers)
- Refunds via the Easypay Refund API — partial refunds are supported
- Direct Settlement configuration so funds land in your bank daily rather than on T+2
- Recurring / subscription billing — needs a separate merchant product, not enabled by default
- Easypay Checkout Session — a newer JSON-API flow that skips the hosted redirect entirely; nicer UX but requires more merchant-side approvals
FAQ
What does Easypaisa charge merchants in Pakistan?
Standard SMB rates land around 1.5%–2.5% per transaction for MA, similar for OTC. Larger merchants negotiate lower rates. Always get the fee schedule in writing on the signed agreement — verbal quotes during onboarding have a habit of shifting by the time settlement starts.
Can I run JazzCash and Easypaisa side-by-side at checkout?
Yes — they’re entirely separate gateways and WooCommerce shows them as two distinct options. Most Pakistani stores enable both plus COD plus at least one card path. Buyers pick what’s on their phone.
Do I need a separate merchant agreement for OTC?
Usually not — OTC is enabled on the same merchant account as MA, but you’ll need to tick it on the agreement form during onboarding. If the form doesn’t mention OTC, ask for it explicitly before signing.
What happens if a customer’s phone dies mid-OTP?
Easypay expires the transaction after the token window (~30 minutes by default) and emits a responseCode != 0000 callback marking it failed. The customer’s wallet is never debited. WooCommerce marks the order Failed and they can retry.
Is Easypaisa’s API documentation publicly available?
Not publicly. You receive a PDF with your merchant credentials after approval. The PDF gets version-bumped periodically, which is why field-order changes occasionally break working integrations. Ask your merchant rep for the latest version before every major change.
Need this wired up for your store?
I build WooCommerce stores with Easypaisa, JazzCash, HBL, Meezan, and courier integrations for businesses across Pakistan — in Karachi, Lahore, Islamabad, and Rawalpindi.
If you’re already running a store and just want someone to wire up the payment + courier side properly, send me a short brief on WhatsApp — I’ll come back with a fixed PKR quote, usually the same day.
Already read the JazzCash version? Part 1: How to Integrate JazzCash with WooCommerce →