letbar / loketqris-sdk
PHP SDK for LoketQRIS API integration for Laravel
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/letbar/loketqris-sdk
Requires
- php: ^8.2
- illuminate/cache: ^11.0|^12.0
- illuminate/contracts: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0|^4.0
This package is not auto-updated.
Last update: 2026-01-31 08:27:15 UTC
README
A PHP SDK for integrating with LoketQRIS payment gateway APIs. Built for Laravel applications with support for multi-tenant architectures and event-driven workflows.
Features
- ๐ Secure Authentication - HMAC-SHA256 signature generation with automatic timestamp handling
- ๐ข Multi-tenant Support - Pass credentials per-request or use config-based defaults
- ๐ก Event-driven Architecture - Hook into API lifecycle via Laravel events
- ๐ Webhook Handling - Built-in controllers for
/b2b/tokenand/qris/notifyendpoints - โ Type-safe DTOs - Request/response objects with helper methods
- ๐งช Fully Tested - Comprehensive test suite with 72 tests
Requirements
- PHP 8.2+
- Laravel 11.x or 12.x
Installation
Via Composer
Install loketqris-sdk:
composer require letbar/loketqris-sdk
Publish Configuration
php artisan vendor:publish --tag=loketqris-sdk-config
Configuration
Environment Variables
LOKETQRIS_BASE_URL=https://api.loketqris.com LOKETQRIS_KODE_LOKET=LK000001 LOKETQRIS_API_KEY=your-api-key LOKETQRIS_CLIENT_SECRET=your-client-secret LOKETQRIS_TOKEN_TTL=300
Config File
// config/loketqris-sdk.php return [ // LoketQRIS API base URL 'base_url' => env('LOKETQRIS_BASE_URL'), // Default credentials (optional for multi-tenant apps) 'credentials' => [ 'kode_loket' => env('LOKETQRIS_KODE_LOKET'), 'api_key' => env('LOKETQRIS_API_KEY'), 'client_secret' => env('LOKETQRIS_CLIENT_SECRET'), ], // Webhook routes configuration 'routes' => [ 'enabled' => true, // Set false to handle routes yourself 'prefix' => '', // Route prefix 'token_path' => '/b2b/token', 'notify_path' => '/qris/notify', 'middleware' => ['api'], ], // Access token TTL in seconds 'token_ttl' => env('LOKETQRIS_TOKEN_TTL', 300), // Response codes mapping 'response_codes' => [ 'success' => ['2004700', '2005100'], 'pending' => ['2005101'], ], ];
Usage
Generate QRIS
use Letbar\LoketQrisSdk\Facades\LoketQris; use Letbar\LoketQrisSdk\DTOs\GenerateQrisRequest; // Using config credentials $request = GenerateQrisRequest::make( partnerReferenceNo: 'INV-2026-001', amount: 50000, validTime: 900 // seconds (optional, default: 900) ); $response = LoketQris::generate($request); if ($response->isSuccessful()) { $qrContent = $response->getQrContent(); $referenceNo = $response->getPartnerReferenceNo(); } // Access raw response $rawData = $response->toArray(); $customField = $response->get('customField', 'default');
Query QRIS Transaction
use Letbar\LoketQrisSdk\Facades\LoketQris; use Letbar\LoketQrisSdk\DTOs\QueryQrisRequest; $request = QueryQrisRequest::make('INV-2026-001'); $response = LoketQris::query($request); if ($response->isSuccessful()) { if ($response->isPaid()) { $paidTime = $response->getPaidTime(); $amount = $response->getAmountValue(); $currency = $response->getAmountCurrency(); // Additional info $issuerId = $response->getAdditionalInfoValue('issuerID'); $paymentRef = $response->getAdditionalInfoValue('paymentReferenceNo'); } if ($response->isPending()) { // Transaction still pending } }
Multi-tenant Usage
For multi-tenant applications, pass credentials per-request:
use Letbar\LoketQrisSdk\DTOs\CredentialData; use Letbar\LoketQrisSdk\DTOs\GenerateQrisRequest; use Letbar\LoketQrisSdk\Facades\LoketQris; // Get tenant credentials from your database $tenant = Credential::find($tenantId); $credential = CredentialData::make( kodeLoket: $tenant->kode_loket, apiKey: $tenant->api_key, clientSecret: $tenant->client_secret ); // Or from array $credential = CredentialData::fromArray([ 'kode_loket' => $tenant->kode_loket, 'api_key' => $tenant->api_key, 'client_secret' => $tenant->client_secret, ]); $request = GenerateQrisRequest::make('INV-001', 50000); // Pass credential as second argument $response = LoketQris::generate($request, $credential);
Using the Client Directly
use Letbar\LoketQrisSdk\LoketQrisClient; $client = app(LoketQrisClient::class); $response = $client->generate($request, $credential);
Events
The SDK dispatches events throughout the API lifecycle, allowing you to hook in for logging, monitoring, or custom business logic.
Available Events
| Event | Description | Properties |
|---|---|---|
QrisGenerating |
Before generate API call | $request, $credential |
QrisGenerated |
After successful generate | $request, $response, $credential |
QrisQuerying |
Before query API call | $request, $credential |
QrisQueried |
After successful query | $request, $response, $credential |
TokenRequested |
Webhook: token requested | $request, $kodeLoket, $apiKey |
TokenIssued |
Webhook: token issued | $token, $kodeLoket, $apiKey, $expiresIn |
NotificationReceived |
Webhook: payment notification | $payload, $request |
Listening to Events
// app/Providers/EventServiceProvider.php use Letbar\LoketQrisSdk\Events\QrisGenerated; use Letbar\LoketQrisSdk\Events\NotificationReceived; protected $listen = [ QrisGenerated::class => [ LogQrisGenerated::class, ], NotificationReceived::class => [ ProcessPaymentNotification::class, ], ];
Example Listeners
// app/Listeners/LogQrisGenerated.php namespace App\Listeners; use Illuminate\Support\Facades\Log; use Letbar\LoketQrisSdk\Events\QrisGenerated; class LogQrisGenerated { public function handle(QrisGenerated $event): void { Log::info('QRIS generated', [ 'reference' => $event->request->partnerReferenceNo, 'amount' => $event->request->amount, 'qr_content' => $event->response->getQrContent(), 'kode_loket' => $event->credential->kodeLoket, ]); } }
// app/Listeners/ProcessPaymentNotification.php namespace App\Listeners; use App\Models\Transaction; use Letbar\LoketQrisSdk\Events\NotificationReceived; class ProcessPaymentNotification { public function handle(NotificationReceived $event): void { $payload = $event->payload; if (!$payload->isSuccessful()) { return; } $transaction = Transaction::where( 'reference_no', $payload->getOriginalPartnerReferenceNo() )->first(); if ($transaction) { $transaction->update([ 'status' => 'paid', 'paid_at' => $payload->getPaymentDate(), 'payment_reference' => $payload->getPaymentReferenceNo(), ]); } } }
Webhook Handling
The SDK automatically registers webhook routes for receiving callbacks from LoketQRIS.
Default Routes
| Method | Path | Description |
|---|---|---|
| POST | /b2b/token |
Token exchange endpoint |
| POST | /qris/notify |
Payment notification endpoint |
Customizing Routes
// config/loketqris-sdk.php 'routes' => [ 'enabled' => true, 'prefix' => 'api/v1', // Routes: /api/v1/b2b/token 'token_path' => '/auth/token', // Custom path 'notify_path' => '/webhooks/qris', 'middleware' => ['api', 'throttle:60,1'], ],
Disabling SDK Routes
If you need full control over routing:
// config/loketqris-sdk.php 'routes' => [ 'enabled' => false, ],
Then define your own routes using the SDK controllers:
// routes/api.php use Letbar\LoketQrisSdk\Http\Controllers\TokenController; use Letbar\LoketQrisSdk\Http\Controllers\NotificationController; use Letbar\LoketQrisSdk\Http\Middleware\VerifyLoketQrisSignature; use Letbar\LoketQrisSdk\Http\Middleware\VerifyBearerToken; Route::post('/custom/token', [TokenController::class, 'store']) ->middleware(VerifyLoketQrisSignature::class); Route::post('/custom/notify', [NotificationController::class, 'store']) ->middleware(VerifyBearerToken::class);
Multi-tenant Webhook Support
For multi-tenant apps, implement the CredentialResolver contract to resolve credentials for incoming webhooks:
// app/Services/TenantCredentialResolver.php namespace App\Services; use App\Models\Credential; use Letbar\LoketQrisSdk\Contracts\CredentialResolver; class TenantCredentialResolver implements CredentialResolver { public function resolveClientSecret(string $kodeLoket, string $apiKey): ?string { $credential = Credential::query() ->where('kode_loket', $kodeLoket) ->where('api_key', $apiKey) ->first(); return $credential?->client_secret; } }
Register the resolver in your service provider:
// app/Providers/AppServiceProvider.php use App\Services\TenantCredentialResolver; use Letbar\LoketQrisSdk\Contracts\CredentialResolver; public function register(): void { $this->app->bind(CredentialResolver::class, TenantCredentialResolver::class); }
Exception Handling
The SDK throws specific exceptions for different error scenarios:
use Letbar\LoketQrisSdk\Exceptions\ApiRequestException; use Letbar\LoketQrisSdk\Exceptions\ConfigurationException; use Letbar\LoketQrisSdk\Exceptions\InvalidCredentialException; use Letbar\LoketQrisSdk\Exceptions\InvalidSignatureException; try { $response = LoketQris::generate($request); } catch (ConfigurationException $e) { // Missing base_url or credentials } catch (InvalidCredentialException $e) { // Empty or invalid credential fields } catch (ApiRequestException $e) { // API returned an error $httpStatus = $e->getHttpStatusCode(); $responseCode = $e->getResponseCode(); $responseBody = $e->getResponseBody(); }
Signature Generation
The SDK handles signature generation automatically, but you can also use it directly:
use Carbon\Carbon; use Letbar\LoketQrisSdk\SignatureGenerator; use Letbar\LoketQrisSdk\DTOs\CredentialData; // Generate signature $result = SignatureGenerator::generate( kodeLoket: 'LK000001', clientSecret: 'your-secret', timestamp: Carbon::now() // optional ); $timestamp = $result['timestamp']; // ISO 8601 format $signature = $result['signature']; // Base64 encoded // Or from credential $credential = CredentialData::make('LK000001', 'api-key', 'secret'); $result = SignatureGenerator::fromCredential($credential); // Verify signature $isValid = SignatureGenerator::verify( kodeLoket: 'LK000001', clientSecret: 'your-secret', timestamp: '2026-01-28T12:00:00+07:00', signature: 'base64-signature' );
DTOs Reference
CredentialData
CredentialData::make(string $kodeLoket, string $apiKey, string $clientSecret) CredentialData::fromArray(array $data) CredentialData::fromConfig() $credential->kodeLoket; $credential->apiKey; $credential->clientSecret; $credential->toArray(); $credential->validate(); // throws InvalidCredentialException
GenerateQrisRequest
GenerateQrisRequest::make( string $partnerReferenceNo, string|float|int $amount, string|int $validTime = '900' ) $request->partnerReferenceNo; $request->amount; // Formatted as "10000.00" $request->validTime; $request->toArray();
GenerateQrisResponse
$response->isSuccessful(): bool $response->getResponseCode(): ?string $response->getResponseMessage(): ?string $response->getPartnerReferenceNo(): ?string $response->getQrContent(): ?string $response->toArray(): array $response->get(string $key, mixed $default = null): mixed
QueryQrisRequest
QueryQrisRequest::make(string $originalPartnerReferenceNo) $request->originalPartnerReferenceNo; $request->toArray();
QueryQrisResponse
$response->isSuccessful(): bool $response->isPending(): bool $response->isPaid(): bool $response->getResponseCode(): ?string $response->getResponseMessage(): ?string $response->getOriginalPartnerReferenceNo(): ?string $response->getServiceCode(): ?string $response->getTransactionStatusDesc(): ?string $response->getLatestTransactionStatus(): ?string $response->getPaidTime(): ?string $response->getAmountValue(): ?string $response->getAmountCurrency(): ?string $response->getAdditionalInfo(): array $response->getAdditionalInfoValue(string $key, mixed $default = null): mixed $response->toArray(): array $response->get(string $key, mixed $default = null): mixed
NotificationPayload
NotificationPayload::fromArray(array $data) $payload->isSuccessful(): bool $payload->getOriginalReferenceNo(): ?string $payload->getOriginalPartnerReferenceNo(): ?string $payload->getLatestTransactionStatus(): ?string $payload->getTransactionStatusDesc(): ?string $payload->getAmountValue(): ?string $payload->getAmountCurrency(): ?string $payload->getExternalStoreId(): ?string $payload->getAdditionalInfo(): array $payload->getCallbackUrl(): ?string $payload->getIssuerId(): ?string $payload->getMerchantId(): ?string $payload->getPaymentDate(): ?string $payload->getRetrievalReferenceNo(): ?string $payload->getPaymentReferenceNo(): ?string $payload->toArray(): array $payload->get(string $key, mixed $default = null): mixed
License
MIT License. See LICENSE for details.