coretrekas / vipps
PHP SDK for Vipps MobilePay APIs
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/coretrekas/vipps
Requires
- php: ^8.1
- ext-json: *
- guzzlehttp/guzzle: ^7.8
- psr/http-client: ^1.0
- psr/http-message: ^1.0|^2.0
- psr/log: ^3.0
Requires (Dev)
- laravel/pint: ^1.13
- mockery/mockery: ^1.6
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.5
- symfony/var-dumper: ^6.4|^7.0
README
A comprehensive, production-ready PHP SDK for Vipps MobilePay APIs, providing easy integration with:
- Checkout API v3 - Complete checkout sessions for payments and subscriptions
- Recurring Payments API v3 - Recurring payment agreements and charges
- Login API v1 - OAuth 2.0 / OpenID Connect authentication
Features
- ✅ Full API Coverage - Complete support for Checkout API v3, Recurring Payments API v3, and Login API v1
- ✅ Automatic Token Management - Access tokens cached and refreshed automatically
- ✅ Fluent Builders - Easy-to-use builder interfaces for sessions, agreements, and authorization URLs
- ✅ Type-Safe - PHP 8.1+ with strict types and full type hints
- ✅ Error Handling - Comprehensive exception handling with detailed error information
- ✅ PSR Compliant - PSR-3 (Logger), PSR-4 (Autoloading), PSR-12 (Code Style), PSR-18 (HTTP Client)
- ✅ Well Tested - 89 unit tests with 187 assertions, 100% pass rate
- ✅ Code Quality - PHPStan level 5, Laravel Pint for code style
- ✅ Environment Support - Separate test and production configurations
- ✅ Production Ready - Comprehensive documentation and examples
Requirements
- PHP 8.1 or higher
- ext-json
Installation
Install via Composer:
composer require coretrekas/vipps
Note: This package requires PHP 8.1 or higher.
Package Structure
The SDK is organized under the Coretrek\Vipps
namespace:
Coretrek\Vipps\
├── VippsClient # Main SDK client
├── Checkout\
│ ├── CheckoutApi # Checkout API methods
│ └── SessionBuilder # Fluent builder for sessions
├── Recurring\
│ ├── RecurringApi # Recurring API methods
│ └── AgreementBuilder # Fluent builder for agreements
├── Login\
│ ├── LoginApi # Login API methods
│ └── AuthorizationUrlBuilder # Fluent builder for OAuth URLs
└── Exceptions\
└── VippsException # Exception handling
All classes use the Coretrek\Vipps
namespace prefix.
Quick Start
Initialize the Client
use Coretrek\Vipps\VippsClient; $client = new VippsClient( clientId: 'your-client-id', clientSecret: 'your-client-secret', subscriptionKey: 'your-subscription-key', merchantSerialNumber: 'your-msn', testMode: true // Set to false for production );
Create a Payment Session (Checkout API)
// Simple payment session $session = $client->checkout()->createPaymentSession( reference: 'order-12345', amount: 10000, // Amount in minor units (100.00 NOK) currency: 'NOK', options: [ 'paymentDescription' => 'Order #12345', 'merchantInfo' => [ 'callbackUrl' => 'https://example.com/vipps/callback', 'returnUrl' => 'https://example.com/order/12345/complete', 'termsAndConditionsUrl' => 'https://example.com/terms', ], ] ); // Redirect user to checkout header('Location: ' . $session['checkoutFrontendUrl']);
Create a Payment Session with Builder (Recommended)
$session = $client->checkout() ->buildPaymentSession() ->reference('order-12345') ->transaction(10000, 'NOK', 'order-12345', 'Order #12345') ->merchantInfo( callbackUrl: 'https://example.com/vipps/callback', returnUrl: 'https://example.com/order/12345/complete', termsAndConditionsUrl: 'https://example.com/terms', callbackAuthorizationToken: 'your-secret-token' ) ->prefillCustomer([ 'firstName' => 'John', 'lastName' => 'Doe', 'email' => 'john@example.com', 'phoneNumber' => '+4712345678', ]) ->customerInteraction('CUSTOMER_NOT_PRESENT') ->elements('Full') ->countries(['NO', 'SE', 'DK']) ->idempotencyKey('unique-key-' . time()) ->systemInfo('my-ecommerce', '1.0.0', 'vipps-plugin', '2.0.0') ->create(); echo "Checkout URL: " . $session['checkoutFrontendUrl'];
Get Session Information
$sessionInfo = $client->checkout()->getSession('order-12345'); echo "Session State: " . $sessionInfo['sessionState']; echo "Payment Method: " . $sessionInfo['paymentMethod'];
Create a Recurring Agreement
$agreement = $client->recurring() ->buildAgreement() ->legacyPricing(2500, 'NOK') // 25.00 NOK per interval ->interval('MONTH', 1) ->product('Premium Subscription', 'Access to premium features') ->merchantUrls( redirectUrl: 'https://example.com/subscription/complete', agreementUrl: 'https://example.com/my-subscriptions' ) ->phoneNumber('4712345678') ->initialCharge(100, 'NOK', 'Activation fee', 'DIRECT_CAPTURE') ->idempotencyKey('agreement-' . time()) ->create(); // Redirect user to accept agreement header('Location: ' . $agreement['vippsConfirmationUrl']);
List Agreements
$agreements = $client->recurring()->listAgreements([ 'status' => 'ACTIVE', 'pageNumber' => 1, 'pageSize' => 50, ]); foreach ($agreements as $agreement) { echo "Agreement ID: " . $agreement['id'] . "\n"; echo "Product: " . $agreement['productName'] . "\n"; echo "Status: " . $agreement['status'] . "\n"; }
Create a Charge
$charge = $client->recurring()->createCharge( agreementId: 'agr_5kSeqz', chargeData: [ 'amount' => 2500, 'transactionType' => 'DIRECT_CAPTURE', 'description' => 'Monthly subscription - January 2024', 'due' => '2024-01-01', 'retryDays' => 5, 'type' => 'RECURRING', ], headers: ['Idempotency-Key' => 'charge-jan-2024'] ); echo "Charge ID: " . $charge['chargeId'];
Capture a Reserved Charge
$client->recurring()->captureCharge( agreementId: 'agr_5kSeqz', chargeId: 'chr_123', captureData: [ 'amount' => 2500, 'description' => 'Capture for January', ] );
Refund a Charge
$client->recurring()->refundCharge( agreementId: 'agr_5kSeqz', chargeId: 'chr_123', refundData: [ 'amount' => 2500, 'description' => 'Customer requested refund', ] );
Advanced Usage
Custom HTTP Client
You can provide your own PSR-18 compatible HTTP client:
use GuzzleHttp\Client; $httpClient = new Client([ 'timeout' => 60, 'verify' => true, ]); $client = new VippsClient( clientId: 'your-client-id', clientSecret: 'your-client-secret', subscriptionKey: 'your-subscription-key', merchantSerialNumber: 'your-msn', testMode: true, options: ['http_client' => $httpClient] );
Custom Logger
Integrate with your PSR-3 compatible logger:
use Monolog\Logger; use Monolog\Handler\StreamHandler; $logger = new Logger('vipps'); $logger->pushHandler(new StreamHandler('path/to/vipps.log', Logger::DEBUG)); $client = new VippsClient( clientId: 'your-client-id', clientSecret: 'your-client-secret', subscriptionKey: 'your-subscription-key', merchantSerialNumber: 'your-msn', testMode: true, options: ['logger' => $logger] );
Error Handling
use Coretrek\Vipps\Exceptions\VippsException; try { $session = $client->checkout()->createPaymentSession( reference: 'order-12345', amount: 10000, currency: 'NOK' ); } catch (VippsException $e) { echo "Error: " . $e->getMessage() . "\n"; echo "Status Code: " . $e->getCode() . "\n"; // Check error type if ($e->isValidationError()) { echo "Validation error occurred\n"; print_r($e->getErrorDetails()); } if ($e->isAuthenticationError()) { echo "Authentication failed - check your credentials\n"; } if ($e->isNotFoundError()) { echo "Resource not found\n"; } }
Checkout API Examples
Payment with Logistics Options
$session = $client->checkout() ->buildPaymentSession() ->reference('order-12345') ->transaction(10000, 'NOK', 'order-12345', 'Order with shipping') ->merchantInfo( 'https://example.com/callback', 'https://example.com/return', 'https://example.com/terms' ) ->logistics([ 'fixedOptions' => [ [ 'brand' => 'POSTEN', 'amount' => ['value' => 300, 'currency' => 'NOK'], 'id' => 'posten-home', 'priority' => 1, 'isDefault' => true, 'description' => 'Home delivery', ], [ 'brand' => 'POSTEN', 'amount' => ['value' => 200, 'currency' => 'NOK'], 'type' => 'PICKUP_POINT', 'id' => 'posten-pickup', 'priority' => 2, 'isDefault' => false, 'description' => 'Pickup point', ], ], ]) ->create();
Subscription Session
$session = $client->checkout() ->buildSubscriptionSession() ->reference('sub-12345') ->transaction(100, 'NOK', 'sub-12345', 'Initial charge') ->subscription([ 'productName' => 'Premium Membership', 'amount' => ['value' => 2500, 'currency' => 'NOK'], 'interval' => ['unit' => 'MONTH', 'count' => 1], 'merchantAgreementUrl' => 'https://example.com/my-subscriptions', 'productDescription' => 'Monthly premium membership', ]) ->merchantInfo( 'https://example.com/callback', 'https://example.com/return', 'https://example.com/terms' ) ->create();
Recurring API Examples
Agreement with Campaign
// Price campaign - reduced price until a date $agreement = $client->recurring() ->buildAgreement() ->legacyPricing(3900, 'NOK') ->interval('MONTH', 1) ->product('News Subscription') ->merchantUrls('https://example.com/redirect', 'https://example.com/manage') ->phoneNumber('4712345678') ->priceCampaign(100, '2024-12-31T23:59:59Z') // 1 NOK until end of year ->create(); // Period campaign - fixed price for a period $agreement = $client->recurring() ->buildAgreement() ->legacyPricing(3900, 'NOK') ->interval('MONTH', 1) ->product('News Subscription') ->merchantUrls('https://example.com/redirect', 'https://example.com/manage') ->phoneNumber('4712345678') ->periodCampaign(100, 'WEEK', 4) // 1 NOK for 4 weeks ->initialCharge(100, 'NOK', 'Campaign activation', 'DIRECT_CAPTURE') ->create();
Variable Amount Agreement
$agreement = $client->recurring() ->buildAgreement() ->variablePricing(5000, 'NOK') // User can be charged up to 50 NOK ->interval('MONTH', 1) ->product('Usage-based Service') ->merchantUrls('https://example.com/redirect', 'https://example.com/manage') ->phoneNumber('4712345678') ->create();
Create Multiple Charges Asynchronously
$charges = [ [ 'agreementId' => 'agr_123', 'amount' => 2500, 'transactionType' => 'DIRECT_CAPTURE', 'description' => 'January charge', 'due' => '2024-01-01', 'retryDays' => 5, 'type' => 'RECURRING', ], [ 'agreementId' => 'agr_456', 'amount' => 2500, 'transactionType' => 'DIRECT_CAPTURE', 'description' => 'January charge', 'due' => '2024-01-01', 'retryDays' => 5, 'type' => 'RECURRING', ], ]; $result = $client->recurring()->createChargesAsync($charges);
Login API Examples
OAuth 2.0 / OpenID Connect Flow
use Coretrek\Vipps\Login\AuthorizationUrlBuilder; // Generate secure random values $state = AuthorizationUrlBuilder::generateState(); $nonce = AuthorizationUrlBuilder::generateNonce(); $codeVerifier = AuthorizationUrlBuilder::generateCodeVerifier(); // Build authorization URL $authUrl = $client->login() ->buildAuthorizationUrl() ->clientId('your-client-id') ->redirectUri('https://example.com/vipps/callback') ->scope(['openid', 'name', 'email', 'phoneNumber', 'address']) ->state($state) ->nonce($nonce) ->pkce($codeVerifier, 'S256') ->build(); // Store state, nonce, and code_verifier in session $_SESSION['oauth_state'] = $state; $_SESSION['oauth_nonce'] = $nonce; $_SESSION['oauth_code_verifier'] = $codeVerifier; // Redirect user to Vipps login header('Location: ' . $authUrl);
Handle OAuth Callback
// In your callback handler $code = $_GET['code']; $returnedState = $_GET['state']; // Verify state to prevent CSRF if ($returnedState !== $_SESSION['oauth_state']) { throw new Exception('Invalid state'); } // Exchange code for tokens $tokens = $client->login()->exchangeCodeForTokens( code: $code, redirectUri: 'https://example.com/vipps/callback', options: [ 'code_verifier' => $_SESSION['oauth_code_verifier'], ] ); // Get user information $userInfo = $client->login()->getUserInfo($tokens['access_token']); echo "Welcome, " . $userInfo['name']; echo "Email: " . $userInfo['email']; echo "Phone: " . $userInfo['phone_number'];
CIBA Flow (Merchant-Initiated Login)
// Check if user exists $userExists = $client->login()->checkUserExists('4712345678'); if ($userExists['exists']) { // Initiate authentication $auth = $client->login()->initiateCibaAuth( loginHint: '4712345678', options: [ 'scope' => 'openid name email', 'bindingMessage' => 'Login to Example App', 'requested_expiry' => 300, ] ); // Poll for token (with proper interval) $interval = $auth['interval']; $authReqId = $auth['auth_req_id']; while (true) { sleep($interval); try { $tokens = $client->login()->pollCibaToken($authReqId); // User has authenticated break; } catch (VippsException $e) { // Still waiting for user to approve continue; } } $userInfo = $client->login()->getUserInfo($tokens['access_token']); }
Get OpenID Configuration
// Get OpenID Connect discovery document $config = $client->login()->getOpenIdConfiguration(); echo "Issuer: " . $config['issuer']; echo "Authorization Endpoint: " . $config['authorization_endpoint']; echo "Supported Scopes: " . implode(', ', $config['scopes_supported']); // Get JWKS for token verification $jwks = $client->login()->getJwks();
Testing
Running Tests
# Run all tests composer test # Run only unit tests composer test:unit # Run only integration tests composer test:integration # Run with coverage report composer test:coverage
Code Quality
# Run static analysis with PHPStan (level 5) composer phpstan # Check code style with Laravel Pint composer pint:test # Fix code style issues automatically composer pint
Integration Tests
Integration tests require valid Vipps test environment credentials. Set these environment variables:
export VIPPS_CLIENT_ID="your-test-client-id" export VIPPS_CLIENT_SECRET="your-test-client-secret" export VIPPS_SUBSCRIPTION_KEY="your-test-subscription-key" export VIPPS_MERCHANT_SERIAL_NUMBER="your-test-msn" # Run integration tests composer test:integration
Note: Integration tests are skipped by default if environment variables are not set.
API Documentation
For detailed API documentation, visit:
Support
- Issues: GitHub Issues
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
Development Setup
# Clone the repository git clone https://github.com/coretrekas/vipps.git cd vipps # Install dependencies composer install # Run tests composer test # Check code quality composer phpstan composer pint:test
License
This SDK is licensed under the MIT License. See the LICENSE file for details.
Changelog
See CHANGELOG.md for version history.