Framework-agnostic Khalti SDK for modern ePayment integrations in PHP.
- Modern API shape:
payments(),verification(),legacyPayments(),transactions() - ePayment KPG-2 create/status flow with strict backend verification
- Polling helper:
waitForCompletion() - Idempotency-friendly verification model for safe order fulfillment
- Retry policy for transient failures (
429,5xx, transport) - Typed models and value objects (
MoneyPaisa,OrderVerificationResult) - Framework agnostic core with pluggable transport
- PHP
8.2+ ext-json
composer require sudiptpa/khalti-sdk-phpuse Khalti\Config\ClientConfig;
use Khalti\Khalti;
$khalti = Khalti::client(new ClientConfig(
secretKey: $_ENV['KHALTI_SECRET_KEY'],
));use Khalti\Model\EpaymentInitiateRequest;
$request = new EpaymentInitiateRequest(
returnUrl: 'https://example.com/payments/khalti/return',
websiteUrl: 'https://example.com',
amount: 1000,
purchaseOrderId: 'ORD-1001',
purchaseOrderName: 'Pro Subscription'
);
$session = $khalti->payments()->create($request);
$status = $khalti->payments()->status($session->pidx);Khalti ePayment does not provide a dedicated payment webhook for this checkout flow.
You must manually verify payment on your backend before order fulfillment.
Never trust return query params alone.
use Khalti\ValueObject\MoneyPaisa;
use Khalti\Verification\VerificationContext;
$returnPayload = $khalti->verification()->parseReturnQuery($_GET);
$result = $khalti->verification()->verify(
payload: $returnPayload,
context: new VerificationContext(
orderId: 'ORD-1001',
pidx: $returnPayload->pidx,
expectedAmount: MoneyPaisa::of(1000),
receivedAtUnix: time(),
)
);
if (! $result->fulfillable) {
// pending/failed/refunded/duplicate
return;
}
// fulfill onceUse IdempotencyStoreInterface in your app:
$idempotencyStore = new App\Payments\Khalti\RedisIdempotencyStore($redis);
$result = $khalti->verification()->verify(
payload: $payload,
context: new VerificationContext(
orderId: $orderId,
pidx: $payload->pidx,
expectedAmount: MoneyPaisa::of($expectedAmount),
),
idempotencyStore: $idempotencyStore,
);If the same payment return is received again, status becomes duplicate and fulfillment is blocked.
$status = $khalti->payments()->waitForCompletion(
pidx: $session->pidx,
timeoutSeconds: 30,
intervalSeconds: 2,
);$config = new ClientConfig(
secretKey: $_ENV['KHALTI_SECRET_KEY'],
maxRetries: 2,
retryBackoffMs: 200,
retryMaxBackoffMs: 1200,
retryHttpStatusCodes: [429, 500, 502, 503, 504],
);$list = $khalti->transactions()->all(page: 1, pageSize: 20);
$detail = $khalti->transactions()->find('txn_idx');
foreach ($list->records as $row) {
echo $row->idx . ' => ' . $row->state . PHP_EOL;
}$verify = $khalti->legacyPayments()->verify($token, 1000);
$status = $khalti->legacyPayments()->status($token, 1000);RequestNormalizerInterfaceResponseNormalizerInterfaceIdempotencyStoreInterfaceMismatchCounterInterfaceClockInterface
These are optional and do not add runtime dependencies.
Laravel Transport Example:
use Illuminate\Support\Facades\Http;
use Khalti\Exception\TransportException;
use Khalti\Http\HttpRequest;
use Khalti\Http\HttpResponse;
use Khalti\Transport\TransportInterface;
use Throwable;
final class LaravelTransport implements TransportInterface
{
public function send(HttpRequest $request, int $timeoutSeconds): HttpResponse
{
try {
$response = Http::timeout($timeoutSeconds)
->withHeaders($request->headers)
->send($request->method, $request->url, ['body' => $request->body]);
} catch (Throwable $e) {
throw new TransportException('Laravel HTTP transport failed.', 0, $e);
}
$headers = [];
foreach ($response->headers() as $name => $values) {
$headers[strtolower($name)] = implode(', ', $values);
}
return new HttpResponse($response->status(), $response->body(), $headers);
}
}AuthenticationException(401/403): wrong secret key or wrong environment key.- Check sandbox vs production key mismatch.
- Stored order amount in paisa differs from Khalti lookup amount.
- Tax/fee math done at UI but not stored in backend order snapshot.
- Verifying wrong order against wrong
pidx.
- Parse query with
parseReturnQuery(). - Verify with
VerificationContext(orderId,pidx,expectedAmount). - Enforce idempotency before fulfillment.
- Fulfill only when
OrderVerificationResult::isPaid()andfulfillable === true.
composer test
composer stan
composer lintFramework-agnostic Khalti PHP SDK for ePayment create/status verification, idempotent backend confirmation, and production-safe order fulfillment.
See ARCHITECTURE.md.
MIT