PHPackages                             arbory/omnipay-swedbank-banklink - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. arbory/omnipay-swedbank-banklink

ActiveLibrary[Payment Processing](/categories/payments)

arbory/omnipay-swedbank-banklink
================================

Swedbank Payment Initiation API driver for Omnipay payment processing library

4.0.1(1mo ago)57.7k5MITPHPPHP ^8.1CI failing

Since Aug 21Pushed 1mo ago2 watchersCompare

[ Source](https://github.com/arbory/omnipay-swedbank-banklink)[ Packagist](https://packagist.org/packages/arbory/omnipay-swedbank-banklink)[ Docs](https://github.com/arbory/omnipay-swedbank-banklink)[ RSS](/packages/arbory-omnipay-swedbank-banklink/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (10)Dependencies (12)Versions (26)Used By (0)

Omnipay: Swedbank Payment Initiation API V3
===========================================

[](#omnipay-swedbank-payment-initiation-api-v3)

**Swedbank Payment Initiation API V3 driver for the Omnipay PHP payment processing library**

[Omnipay](https://github.com/thephpleague/omnipay) is a framework agnostic, multi-gateway payment processing library for PHP. This package implements Swedbank Payment Initiation API V3 support for Omnipay.

Installation
------------

[](#installation)

```
composer require arbory/omnipay-swedbank-banklink
```

Or add it to `composer.json` manually:

```
{
    "require": {
        "arbory/omnipay-swedbank-banklink": "^4.0"
    }
}
```

---

Certificate Setup
-----------------

[](#certificate-setup)

### How Keys Work

[](#how-keys-work)

Swedbank V3 uses **asymmetric RSA cryptography**. There are two separate key pairs:

KeyWho generates/provides itWhat to do with it**Merchant private key****You generate it**Keep secret on your server, never share**Merchant public key****You generate it** (from private key)Upload to Swedbank merchant portal**Bank certificate**Provided by SwedbankDownload and use to verify bank responses> ℹ️ **The merchant private key is always generated by you** — Swedbank never provides it. The playground at `pi-playground.swedbank.com` is for testing API flows only, not for issuing keys.

---

### Step 1 — Generate Your Merchant Key Pair

[](#step-1--generate-your-merchant-key-pair)

Run these commands once per environment:

```
mkdir -p payment_certificates/swedbank_prod

# Generate 4096-bit RSA private key
openssl genrsa -out payment_certificates/swedbank_prod/merchant_private.key 4096

# Extract the public key from it
openssl rsa -in payment_certificates/swedbank_prod/merchant_private.key \
  -pubout -out payment_certificates/swedbank_prod/merchant_public.key

# Secure the private key
chmod 600 payment_certificates/swedbank_prod/merchant_private.key

# Verify the private key is valid
openssl rsa -in payment_certificates/swedbank_prod/merchant_private.key -noout -check
# Expected output: RSA key ok
```

### Step 2 — Upload Your Public Key to Swedbank

[](#step-2--upload-your-public-key-to-swedbank)

Log in to the Swedbank merchant portal and upload the contents of `merchant_public.key` (the `-----BEGIN PUBLIC KEY-----` block) to your merchant account settings. Swedbank will associate it with your Merchant ID.

> For sandbox testing, upload to the sandbox portal. For production, upload to the production portal.

### Step 3 — Download the Bank Certificate

[](#step-3--download-the-bank-certificate)

Swedbank's public certificate is used to **verify signatures on bank responses**.

```
# Production bank certificate
curl -o payment_certificates/swedbank_prod/bankCertificate_009.txt \
  https://pi.swedbank.com/public/resources/bank-certificates/009

# Sandbox bank certificate (for testing)
mkdir -p payment_certificates/swedbank_sandbox
curl -o payment_certificates/swedbank_sandbox/bankCertificate_009.txt \
  https://pi-playground.swedbank.com/public/resources/bank-certificates/009

# Check certificate details and expiry
openssl x509 -in payment_certificates/swedbank_prod/bankCertificate_009.txt -noout -text | grep -E "Subject:|Not After"
```

The production bank certificate (as of 2024) is issued to **Banklink Host** by Swedbank G3 Issuing CA, valid until **2027-08-06**.

### Certificate Format

[](#certificate-format)

All files are in standard **PEM format**:

```
-----BEGIN PRIVATE KEY-----   ← merchant_private.key
-----BEGIN PUBLIC KEY-----    ← merchant_public.key
-----BEGIN CERTIFICATE-----   ← bankCertificate_009.txt (X.509 certificate)

```

### Important Notes

[](#important-notes)

⚠️ **Never share or commit your merchant private key.** It must only exist on your server.

- The **merchant public key** is what you register with Swedbank — not the private key
- Use the correct environment certificates: sandbox keys with sandbox, production keys with production
- The bank certificate expires periodically — check `Not After` date and re-download when needed

Add to `.gitignore`:

```
payment_certificates/
storage/certificates/

```

---

Environment &amp; Laravel Configuration
---------------------------------------

[](#environment--laravel-configuration)

### `.env`

[](#env)

```
SWEDBANK_MERCHANT_ID=SANDBOX_RSA
SWEDBANK_TEST_MODE=true
SWEDBANK_COUNTRY=LV
SWEDBANK_PRIVATE_KEY_PATH=/path/to/payment_certificates/swedbank_sandbox/merchant_private.key
SWEDBANK_BANK_PUBLIC_KEY_PATH=/path/to/payment_certificates/swedbank_sandbox/bankCertificate_009.txt
SWEDBANK_ALGORITHM=RS512
SWEDBANK_DEBUG_LOGGING=false
SWEDBANK_RETURN_URL=https://yourdomain.com/payments/complete-purchase/swedbank-banklink
SWEDBANK_GATEWAY_URL=https://pi-playground.swedbank.com/sandbox/
```

For production:

```
SWEDBANK_MERCHANT_ID=YOUR_PRODUCTION_MERCHANT_ID
SWEDBANK_TEST_MODE=false
SWEDBANK_COUNTRY=LV
SWEDBANK_PRIVATE_KEY_PATH=/path/to/payment_certificates/swedbank_prod/merchant_private.key
SWEDBANK_BANK_PUBLIC_KEY_PATH=/path/to/payment_certificates/swedbank_prod/bankCertificate_009.txt
SWEDBANK_ALGORITHM=RS512
SWEDBANK_DEBUG_LOGGING=false
SWEDBANK_RETURN_URL=https://yourdomain.com/payments/complete-purchase/swedbank-banklink
SWEDBANK_GATEWAY_URL=https://pi.swedbank.com
```

### `config/laravel-omnipay.php`

[](#configlaravel-omnipayphp)

```
'swedbank-banklink' => [
    'driver' => 'SwedbankBanklink',
    'options' => [
        'merchantId'        => env('SWEDBANK_MERCHANT_ID'),
        'country'           => env('SWEDBANK_COUNTRY', 'LV'),
        'privateKeyPath'    => env('SWEDBANK_PRIVATE_KEY_PATH'),
        'bankPublicKeyPath' => env('SWEDBANK_BANK_PUBLIC_KEY_PATH'),
        'algorithm'         => env('SWEDBANK_ALGORITHM', 'RS512'),
        'testMode'          => env('SWEDBANK_TEST_MODE', true),
        'returnUrl'         => env('SWEDBANK_RETURN_URL'),
        'baseUrl'           => env('SWEDBANK_GATEWAY_URL'),
        'debugLogging'      => env('SWEDBANK_DEBUG_LOGGING', false),
    ],
],
```

---

Usage
-----

[](#usage)

### Step 1 — Initialise the Gateway

[](#step-1--initialise-the-gateway)

```
use Omnipay\Omnipay;

$gateway = Omnipay::create('SwedbankBanklink');
$gateway->initialize([
    'merchantId'        => env('SWEDBANK_MERCHANT_ID'),
    'country'           => env('SWEDBANK_COUNTRY', 'LV'),
    'privateKeyPath'    => env('SWEDBANK_PRIVATE_KEY_PATH'),
    'bankPublicKeyPath' => env('SWEDBANK_BANK_PUBLIC_KEY_PATH'),
    'algorithm'         => env('SWEDBANK_ALGORITHM', 'RS512'),
    'testMode'          => env('SWEDBANK_TEST_MODE', true),
    'baseUrl'           => env('SWEDBANK_GATEWAY_URL'),
    'debugLogging'      => env('SWEDBANK_DEBUG_LOGGING', false),
]);

// Alternative: pass raw key content instead of file paths
// $gateway->initialize([
//     'merchantId'    => '...',
//     'country'       => 'LV',
//     'privateKey'    => file_get_contents('/path/to/private.key'),
//     'bankPublicKey' => file_get_contents('/path/to/bank-certificate.pem'),
//     'testMode'      => true,
//     'debugLogging'  => false,
// ]);
```

> When using the `laravel-omnipay` package the gateway is resolved from `config/laravel-omnipay.php` automatically — you do not need to call `initialize()` manually.

---

Provider Resolution
-------------------

[](#provider-resolution)

The `provider` parameter passed to `purchase()` must be a valid bank BIC code (e.g. `HABALT22`). In applications where the user selects a bank via a `payment_type` slug (stored in a `BankLink` model or similar), a resolver maps that slug to the correct BIC at runtime.

### `ProviderResolver`

[](#providerresolver)

`ProviderResolver` is a static utility class in this package that handles the mapping:

ScenarioBehaviour`payment_type` is emptyReturns `ProviderResolver::DEFAULT_BIC` (`HABALV22`)Custom resolver registered, returns a BICUses the resolved BICCustom resolver registered, returns `null`Falls back to the raw `payment_type` valueNo resolver registeredReturns the raw `payment_type` value directly### Registering a Custom Resolver (Laravel)

[](#registering-a-custom-resolver-laravel)

Register the resolver once during application boot in `AppServiceProvider`:

```
// app/Providers/AppServiceProvider.php

use App\Models\BankLink;
use Omnipay\SwedbankBanklink\Utils\ProviderResolver;

public function boot(): void
{
    // ...

    ProviderResolver::setResolver(function (string $paymentType): ?string {
        $bankLink = BankLink::where('payment_type', $paymentType)->first();
        return $bankLink?->bic;
    });
}
```

This looks up the `BankLink` record matching the order's `payment_type` and returns its `bic` column value. If no record is found, `null` is returned and `ProviderResolver` falls back to using the raw `payment_type` string.

> The resolver is called automatically by `SwedbankBanklinkHandler::getProvider()` when building purchase arguments — no manual call is needed in your controllers.

---

### Step 2 — Get Available Payment Providers

[](#step-2--get-available-payment-providers)

```
$response = $gateway->getProviders()->send();

if ($response->isSuccessful()) {
    $providers = $response->getEnabledProviders();
    // Each provider has a BIC code, e.g. 'HABALT22' (Swedbank Latvia)
}
```

### Step 3 — Initiate a Payment

[](#step-3--initiate-a-payment)

Store the returned transaction ID in your session/database before redirecting the customer.

```
$response = $gateway->purchase([
    'amount'          => '99.99',
    'currency'        => 'EUR',                 // Only EUR supported
    'locale'          => 'lv',                  // en, et, lv, lt, or ru
    'description'     => 'Order #12345',        // Max 140 chars (unstructured)
    // 'reference'    => 'RF18539007547034',    // ISO11649 structured reference (alternative)
    'provider'        => 'HABALT22',            // BIC from getProviders()
    'returnUrl'       => 'https://yourdomain.com/payment/return',
    'notificationUrl' => 'https://yourdomain.com/payment/webhook',
])->send();

if ($response->isRedirect()) {
    $transactionId = $response->getTransactionReference(); // Save this!
    session(['swedbank_transaction_id' => $transactionId]);

    return $response->redirect(); // Redirect customer to bank
}
```

### Step 4 — Handle the Return URL

[](#step-4--handle-the-return-url)

When the customer is redirected back, **always call `fetchTransaction`** — the redirect itself carries no payment confirmation.

```
// PaymentReturnController.php
$transactionId = session('swedbank_transaction_id');

$response = $gateway->fetchTransaction([
    'transactionReference' => $transactionId,
])->send();

if ($response->isSuccessful()) {
    // EXECUTED or SETTLED — payment confirmed, safe to fulfill the order
    $details = $response->getPaymentDetails();
    /*
     * $details keys: transactionId, status, amount, currency, paymentType,
     *   debtor, debtorAccount, debtorBic, creditor, creditorAccount, creditorBic,
     *   reference, referenceType, description, endToEndIdentification,
     *   createdAt, statusUpdatedAt, statusCheckedAt, errorDetails, errorLabels, ...
     */
} elseif ($response->isPending()) {
    // Still processing — show a "payment is being processed" page
    // Rely on the webhook notification (Step 5) for the final status update
} elseif ($response->isCancelled()) {
    // Customer cancelled at the bank
} elseif ($response->isFailed()) {
    $reason = $response->getRejectionReason(); // from errorDetails / errorLabels
}
```

---

Status Polling
--------------

[](#status-polling)

### Payment Status Reference

[](#payment-status-reference)

StatusGroupDescription`NOT_INITIATED`⏳ PendingTransaction registered but not yet started`INITIAL`⏳ PendingUser initiated the payment`STARTED`⏳ PendingPayment initiation started`IN_PROGRESS`⏳ PendingAwaiting final status from bank`IN_AUTHENTICATION`⏳ PendingAwaiting user authentication`IN_CONFIRMATION`⏳ PendingAwaiting user confirmation`IN_DOUBLE_SIGNING`⏳ PendingAwaiting second signer`UNKNOWN`⏳ PendingTemporary unknown state`EXECUTED`✅ SuccessPayment successfully initiated`SETTLED`✅ SuccessPayment settled (only with Swedbank settlement account)`ABANDONED`❌ FailedNot initiated within 1 hour`FAILED`❌ FailedPayment initiation failed`CANCELLED_BY_USER`❌ FailedUser cancelled at the bank`EXPIRED`❌ FailedNo final status within expected timeframe### Background Job Polling

[](#background-job-polling)

For payments still `isPending()` after the return, poll in a background job with exponential backoff:

```
// App\Jobs\PollSwedbankPayment.php (simplified)
public function handle(): void
{
    $response = $this->gateway->fetchTransaction([
        'transactionReference' => $this->transactionId,
    ])->send();

    if ($response->isSuccessful()) {
        // Mark order as paid
    } elseif ($response->isFailed() || $response->isCancelled()) {
        // Mark order as failed
    } elseif ($response->isPending()) {
        // Re-dispatch with increasing delay
        self::dispatch($this->gateway, $this->transactionId)
            ->delay(now()->addSeconds(15));
    }
}
```

> ⚠️ **In production**, never use a blocking `sleep()` loop. Use queued jobs with delays and a maximum timeout (e.g., 10–15 minutes total).

---

Bank Webhook Notifications
--------------------------

[](#bank-webhook-notifications)

Swedbank sends a **server-to-server HTTP POST** to your `notificationUrl` when a payment reaches a final status. This is the most reliable confirmation method — it arrives even if the customer closes their browser before being redirected back.

### What Swedbank Sends

[](#what-swedbank-sends)

The notification body is a signed JSON payload (`StatusResponseV3`), identical to the `fetchTransaction` response. The signature is in the `x-jws-signature` HTTP header.

```
POST /payment/webhook HTTP/1.1
Content-Type: application/json
x-jws-signature: eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJSUzUxMiIsImlhdCI6...

{
    "transactionId": "abc123...",
    "status": "EXECUTED",
    "amount": "99.99",
    "currency": "EUR",
    "debtorBic": "HABALT22",
    ...
}

```

### Route

[](#route)

```
// routes/web.php or routes/api.php
Route::post('/payment/webhook', [PaymentWebhookController::class, 'handle'])
    ->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);
```

### Controller

[](#controller)

```
