sudiptpa/omnipay-esewa

Framework-agnostic eSewa ePay v2 payment SDK for PHP.

Maintainers

Package info

github.com/sudiptpa/esewa-sdk-php

pkg:composer/sudiptpa/omnipay-esewa

Statistics

Installs: 3 872

Dependents: 0

Suggesters: 0

Stars: 6

Open Issues: 0

v3.0.1 2026-03-13 10:39 UTC

This package is auto-updated.

Last update: 2026-03-13 10:46:38 UTC


README

Framework-agnostic, zero-dependency PHP SDK for eSewa ePay v2, with an optional Omnipay v3 bridge.

CI Latest Release Packagist Version Packagist Downloads PHP Version License

Public API

This package exposes two public namespaces:

  • Sujip\\Esewa
  • Omnipay\\Esewa

Highlights

  • checkout, callback, and transaction flows built around request and result models
  • typed value objects for amount, transaction UUID, product code, and reference ID
  • toArray() and fromArray() where they are useful
  • callback verification with explicit states: verified, invalid_signature, replayed
  • zero-dependency core with built-in CurlTransport
  • configurable retry policy and clock handling
  • replay protection backed by filesystem or PDO storage
  • optional PSR-18 transport support
  • optional Omnipay v3 bridge
  • PHP 8.2 to 8.5

Installation

composer require sudiptpa/omnipay-esewa

Optional PSR-18 usage:

composer require symfony/http-client nyholm/psr7

Documentation

Quick Start

<?php

declare(strict_types=1);

use Sujip\Esewa\Esewa;
use Sujip\Esewa\Domain\Checkout\CheckoutRequest;

$client = Esewa::make(
    merchantCode: 'EPAYTEST',
    secretKey: $_ENV['ESEWA_SECRET_KEY'],
    environment: 'uat',
);

$intent = $client->checkout()->createIntent(new CheckoutRequest(
    amount: '100',
    taxAmount: '0',
    serviceCharge: '0',
    deliveryCharge: '0',
    transactionUuid: 'TXN-1001',
    productCode: 'EPAYTEST',
    successUrl: 'https://merchant.example.com/esewa/success',
    failureUrl: 'https://merchant.example.com/esewa/failure',
));

The request constructor accepts strings for convenience. Internally, those values are normalized into value objects. If you want stricter typing in your own code, build the value objects directly:

use Sujip\Esewa\Domain\Checkout\CheckoutRequest;
use Sujip\Esewa\ValueObject\Amount;
use Sujip\Esewa\ValueObject\ProductCode;
use Sujip\Esewa\ValueObject\TransactionUuid;

$request = new CheckoutRequest(
    amount: Amount::fromString('100'),
    taxAmount: Amount::fromString('0'),
    serviceCharge: Amount::fromString('0'),
    deliveryCharge: Amount::fromString('0'),
    transactionUuid: TransactionUuid::fromString('TXN-1001'),
    productCode: ProductCode::fromString('EPAYTEST'),
    successUrl: 'https://merchant.example.com/esewa/success',
    failureUrl: 'https://merchant.example.com/esewa/failure',
);

Working With Models

The core models can be converted to arrays. That is mainly useful when you are crossing controller boundaries, queueing work, or saving fixtures for tests.

$payload = $request->toArray();
$restored = CheckoutRequest::fromArray($payload);

The client stays small. Most integrations only need three modules:

  • $client->checkout()
  • $client->callbacks()
  • $client->transactions()

Callback Verification

use Sujip\Esewa\Domain\Verification\CallbackPayload;
use Sujip\Esewa\Domain\Verification\VerificationExpectation;

$payload = CallbackPayload::fromArray([
    'data' => $_GET['data'] ?? '',
    'signature' => $_GET['signature'] ?? '',
]);

$result = $client->callbacks()->verifyCallback(
    $payload,
    new VerificationExpectation(
        totalAmount: '100.00',
        transactionUuid: 'TXN-1001',
        productCode: 'EPAYTEST',
    )
);

if ($result->state->value === 'replayed') {
    http_response_code(409);
    exit('Replay detected');
}

if (!$result->isSuccessful()) {
    http_response_code(400);
    exit('Invalid callback');
}

Do not treat the success redirect alone as proof of payment. Verify the callback on your backend and keep a status check as a fallback when something looks off.

Transaction Status

use Sujip\Esewa\Domain\Transaction\TransactionStatusRequest;

$status = $client->transactions()->fetchStatus(new TransactionStatusRequest(
    transactionUuid: 'TXN-1001',
    totalAmount: '100.00',
    productCode: 'EPAYTEST',
));

if ($status->isSuccessful()) {
    // mark paid
}

Production Notes

For live traffic, turn on replay protection and use persistent storage:

use Sujip\Esewa\Config\ClientOptions;
use Sujip\Esewa\Esewa;
use Sujip\Esewa\Infrastructure\Idempotency\FilesystemIdempotencyStore;

$client = Esewa::make(
    merchantCode: 'EPAYTEST',
    secretKey: $_ENV['ESEWA_SECRET_KEY'],
    environment: 'uat',
    options: new ClientOptions(
        preventCallbackReplay: true,
        idempotencyStore: new FilesystemIdempotencyStore(__DIR__ . '/storage/esewa-idempotency'),
    ),
);

Retry behavior is also configurable:

use Sujip\Esewa\Config\ClientOptions;
use Sujip\Esewa\Support\FixedDelayRetryPolicy;

$options = new ClientOptions(
    retryPolicy: new FixedDelayRetryPolicy(
        maxRetries: 3,
        delayUs: 250000,
    ),
);

Omnipay Bridge

use Omnipay\Esewa\SecureGateway;

$gateway = new SecureGateway();
$gateway->setMerchantCode('EPAYTEST');
$gateway->setSecretKey($_ENV['ESEWA_SECRET_KEY']);
$gateway->setProductCode('EPAYTEST');
$gateway->setTestMode(true);
$gateway->setReturnUrl('https://merchant.example.com/esewa/success');
$gateway->setFailureUrl('https://merchant.example.com/esewa/failure');

Supported bridge flows:

  • purchase()
  • completePurchase()
  • verifyPayment()

Development

composer test
composer stan
composer rector:check