PHPackages                             letbar/loketqris-sdk - 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. letbar/loketqris-sdk

ActiveLibrary

letbar/loketqris-sdk
====================

PHP SDK for LoketQRIS API integration for Laravel

v1.0(3mo ago)037MITPHPPHP ^8.2

Since Jan 30Pushed 3mo agoCompare

[ Source](https://github.com/letbar/loketqris-sdk)[ Packagist](https://packagist.org/packages/letbar/loketqris-sdk)[ RSS](/packages/letbar-loketqris-sdk/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (6)Versions (2)Used By (0)

LoketQRIS SDK
=============

[](#loketqris-sdk)

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
--------

[](#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
------------

[](#requirements)

- PHP 8.2+
- Laravel 11.x or 12.x

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

[](#installation)

### Via Composer

[](#via-composer)

Install loketqris-sdk:

```
composer require letbar/loketqris-sdk
```

### Publish Configuration

[](#publish-configuration)

```
php artisan vendor:publish --tag=loketqris-sdk-config
```

Configuration
-------------

[](#configuration)

### Environment Variables

[](#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-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
-----

[](#usage)

### Generate QRIS

[](#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

[](#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

[](#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

[](#using-the-client-directly)

```
use Letbar\LoketQrisSdk\LoketQrisClient;

$client = app(LoketQrisClient::class);

$response = $client->generate($request, $credential);
```

Events
------

[](#events)

The SDK dispatches events throughout the API lifecycle, allowing you to hook in for logging, monitoring, or custom business logic.

### Available Events

[](#available-events)

EventDescriptionProperties`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

[](#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

[](#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
----------------

[](#webhook-handling)

The SDK automatically registers webhook routes for receiving callbacks from LoketQRIS.

### Default Routes

[](#default-routes)

MethodPathDescriptionPOST`/b2b/token`Token exchange endpointPOST`/qris/notify`Payment notification endpoint### Customizing Routes

[](#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

[](#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

[](#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
------------------

[](#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
--------------------

[](#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
--------------

[](#dtos-reference)

### CredentialData

[](#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)

```
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

[](#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)

```
QueryQrisRequest::make(string $originalPartnerReferenceNo)

$request->originalPartnerReferenceNo;
$request->toArray();
```

### QueryQrisResponse

[](#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)

```
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
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for details.

###  Health Score

38

—

LowBetter than 85% of packages

Maintenance81

Actively maintained with recent releases

Popularity11

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

100d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7b9b8bc66ebb3e58073e2fc1eefc63cc206f3d2602702d2879fe07458cd16063?d=identicon)[addeeandra](/maintainers/addeeandra)

---

Top Contributors

[![addeeandra](https://avatars.githubusercontent.com/u/22489224?v=4)](https://github.com/addeeandra "addeeandra (8 commits)")

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/letbar-loketqris-sdk/health.svg)

```
[![Health](https://phpackages.com/badges/letbar-loketqris-sdk/health.svg)](https://phpackages.com/packages/letbar-loketqris-sdk)
```

###  Alternatives

[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k12.1M99](/packages/laravel-pulse)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9682.1M97](/packages/roots-acorn)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

255.2k](/packages/aedart-athenaeum)[spatie/laravel-export

Create a static site bundle from a Laravel app

646127.9k5](/packages/spatie-laravel-export)[jerome/filterable

Streamline dynamic Eloquent query filtering with seamless API request integration and advanced caching strategies.

19226.1k](/packages/jerome-filterable)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

44643.1k1](/packages/pressbooks-pressbooks)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
