Skip to content
Learning Machine

Kapital Bank payments in PHP and Laravel: kapitalbank-checkout

Kapital Bank payments in PHP and Laravel: kapitalbank-checkout

An open PHP package for the Kapital Bank / Birbank Checkout API: create payments, check status, refund, verify webhooks, with Laravel support.

Accepting card payments through Kapital Bank means talking to the Kapital Bank / Birbank Checkout API: getting an OAuth2 token, creating a payment, sending the customer to the bank's page, checking the result, handling refunds and verifying the bank's notifications. We packaged all of this into an open PHP library, learning-machine/kapitalbank-checkout. This article shows how to accept a Kapital Bank payment in PHP and Laravel with it, and which details deserve attention.

What the package does

  • OAuth2 token with caching through any PSR-16 cache, so a token is not requested for every call.
  • Creating a payment and redirecting the customer to the bank's payment page.
  • Payment status: Pending, Succeeded, Canceled or Unknown.
  • Full and partial refunds.
  • Webhook verification: an HMAC-SHA256 signature, base64-encoded, in the X-Signature header.
  • A framework-agnostic core on the PSR-18, PSR-17 and PSR-16 standards, plus an optional, auto-discovered Laravel integration.

It needs PHP 8.1 or newer and is released under the MIT licence.

Installation

The package is on Packagist, so one Composer command installs it:

composer require learning-machine/kapitalbank-checkout

Creating a payment in plain PHP

Create a client with your credentials. The example uses the pre-production environment, https://precheckout.kapitalbank.az/api; in production the base URL is https://checkout.kapitalbank.az/api.

use LearningMachine\KapitalBank\Checkout\Amount;
use LearningMachine\KapitalBank\Checkout\CheckoutClient;

$client = CheckoutClient::fromConfig([
    'base_url' => 'https://precheckout.kapitalbank.az/api',
    'client_id' => getenv('KAPITALBANK_CLIENT_ID'),
    'client_secret' => getenv('KAPITALBANK_CLIENT_SECRET'),
    'merchant_id' => getenv('KAPITALBANK_MERCHANT_ID'),
    'terminal_id' => getenv('KAPITALBANK_TERMINAL_ID'),
], cache: $psr16Cache);

$payment = $client->createPayment(
    Amount::fromDecimal('12.50'),
    'Order #123',
    'https://shop.example/payment/return',
    $idempotencyKey
);

// store $payment->id and $payment->idempotencyKey with the order
header('Location: ' . $payment->redirectUrl());

Amounts are value objects: Amount::fromDecimal('12.50') or Amount::fromMinor(1250) for the same sum in minor units. The currency is AZN by default. Passing the decimal amount as a string avoids floating-point surprises.

The idempotency key is a unique value you generate for this payment attempt and keep with the order. It is what makes a safe retry possible, as described below.

Checking the status

When the customer comes back to your return URL, do not trust the redirect alone: ask the bank.

$p = $client->getPayment($paymentId);
$p->status; // Status::Pending, Status::Succeeded, Status::Canceled or Status::Unknown

Only Succeeded means the money has moved. For Unknown, the original string from the bank is available in $p->rawStatus.

Refunds

$refund = $client->refund($paymentId, Amount::fromDecimal('2.50'), $refundRowUuid);

The amount you pass decides how much goes back: the whole sum or a part of it. The third argument is the UUID of the refund record in your own database. One detail worth knowing: the public API reference documents only the refund object, not the request. The body the live API accepts is {"id": "<payment uuid>", "amount": 2.5}, and that is what the package sends; $refund->requestBody shows it if you want to log it.

Webhooks

The bank notifies your server about payment events. Verify the signature before you trust anything in the body:

use LearningMachine\KapitalBank\Checkout\Webhook\WebhookParser;

$parser = new WebhookParser(getenv('KAPITALBANK_WEBHOOK_SECRET'));
$event = $parser->parse($rawBody, $request->headers->get('X-Signature'));

if ($event->isSucceeded()) {
    // credit the order idempotently
}
// always answer HTTP 200, or the bank re-delivers

Pass the raw request body, exactly as received; a re-encoded JSON string will not match the signature. If the signature is wrong, parse() throws an InvalidWebhook exception. For valid events, answer HTTP 200 in every case, including canceled payments, and make crediting idempotent, because the same event can arrive more than once.

Errors that matter for money

use LearningMachine\KapitalBank\Checkout\Exception\AuthenticationFailed;
use LearningMachine\KapitalBank\Checkout\Exception\BankRejected;
use LearningMachine\KapitalBank\Checkout\Exception\OutcomeUnknown;

try {
    $payment = $client->createPayment($amount, $description, $returnUrl, $idempotencyKey);
} catch (BankRejected $e) {
    // the bank answered with an error; quote $e->error->traceId to the bank
    // $e->isServerError() is true for 5xx answers
} catch (OutcomeUnknown $e) {
    // timeout or dropped connection: the bank MAY have processed the request
} catch (AuthenticationFailed $e) {
    // the token request failed; nothing money-related was sent
}

OutcomeUnknown is the case to design for. The request may or may not have reached the bank, so never create a new payment blindly. Either retry with the same idempotency key, or check the payment with getPayment(). BankRejected carries a traceId that the bank's support can use to find your request.

Laravel integration

In Laravel the service provider is discovered automatically. Publish the config:

php artisan vendor:publish --tag=kapitalbank-config

and fill in .env:

KAPITALBANK_BASE_URL=https://precheckout.kapitalbank.az/api
KAPITALBANK_CLIENT_ID=
KAPITALBANK_CLIENT_SECRET=
KAPITALBANK_MERCHANT_ID=
KAPITALBANK_TERMINAL_ID=
KAPITALBANK_WEBHOOK_SECRET=

After that, inject CheckoutClient or WebhookParser wherever you need them; both are registered as singletons. In a webhook controller, read the raw body with $request->getContent() and remember to exclude the webhook route from CSRF protection. The package also works with Laravel's Http::fake(), so your payment flow can be covered by tests without calls to the bank.

Use it, or let us integrate it

The package is open source on GitHub; issues and pull requests are welcome. More about it is on the project page and in our open-source section. If you would like help integrating payments into your site or system, describe your task.

  • PHP
  • Laravel
  • Payments
  • Open Source

Contact us

Send your project or idea to us to get quotation and estimation. We are glad to any of your ideas!

Rasul Rza 75, Baku, Azerbaijan
[email protected]

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

By sending you agree to the privacy policy.