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

v1.0 2026-01-30 10:17 UTC

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/token and /qris/notify endpoints
  • โœ… 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.