Search by

carllee / line-pay-offline-v4

Carl Lee

LINE Pay Offline V4 API SDK for PHP. Supports POS payment, capture, void, refund operations with Laravel support.

Package info

github.com/CarlLee1983/line-pay-offline-v4-php

pkg:composer/carllee/line-pay-offline-v4

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

v1.2.1 2025-12-12 04:52 UTC

This package is auto-updated.

Last update: 2026-10-09 10:38:03 UTC


README

CI PHP Version License

Modern, type-safe LINE Pay Offline V4 API SDK for PHP.

🌐 Language / 語言 / 言語 / ภาษา: English | 繁體中文 | 日本語 | ภาษาไทย

Features

  • ✅ PHP 8.1+ with strict types and enums
  • ✅ Laravel Integration - ServiceProvider, Facade, IoC support
  • ✅ POS/Kiosk Terminal Support - For retail and food service
  • ✅ One-Time Key Payment - Scan customer's barcode
  • ✅ Full API Coverage - Payment, capture, void, refund
  • ✅ Type-Safe Enums - Currency, PaymentStatus, etc.
  • ✅ PHPStan Level Max - Strict static analysis
  • ✅ Built on Core SDK - Shares code with Online SDK

Requirements

  • PHP 8.1 or higher
  • Composer
  • ext-json
  • ext-openssl

Installation

composer require carllee/line-pay-offline-v4

Payment Flow

sequenceDiagram
    participant U as Customer (App)
    participant P as POS/Server
    participant L as LINE Pay API
    
    U->>P: Show Barcode (OneTimeKey)
    P->>L: POST /v3/payments/oneTimeKeys/pay
    alt Success
        L-->>P: 200 OK (ReturnCode 0000)
        P->>U: Payment Success
    else Timeout / Network Error
        L-->>P: Timeout / 500 Error
        Note over P,L: CRITICAL: Must Check Status
        P->>L: GET /v3/payments/orders/{orderId}
        L-->>P: Status: COMPLETE
        P->>U: Payment Success
    end
Loading

Quick Start

"✨ Developer Experience: This SDK uses DTOs and Enums. Your IDE will provide full autocompletion for request parameters and response fields, eliminating 'magic string' typos."

<?php

use LinePay\Offline\Config\LinePayOfflineConfig;
use LinePay\Offline\LinePayOfflineClient;
use LinePay\Offline\Enums\Currency;

// Create configuration
$config = new LinePayOfflineConfig(
    channelId: getenv('LINE_PAY_CHANNEL_ID'),
    channelSecret: getenv('LINE_PAY_CHANNEL_SECRET'),
    merchantDeviceProfileId: 'POS-001',  // Your POS terminal ID
    merchantDeviceType: 'POS',           // Device type
    env: 'sandbox'                        // or 'production'
);

// Create client
$client = new LinePayOfflineClient($config);

// Request payment with customer's barcode (oneTimeKey)
$response = $client->requestPayment([
    'amount' => 100,
    'currency' => 'TWD',
    'oneTimeKey' => '12345678901245678', // From customer's LINE Pay barcode
    'orderId' => 'ORDER-' . time(),
    'packages' => [
        [
            'id' => 'PKG-001',
            'amount' => 100,
            'products' => [
                ['name' => 'Coffee', 'quantity' => 1, 'price' => 100]
            ]
        ]
    ]
]);

if ($response['returnCode'] === '0000') {
    echo "Payment successful!\n";
    echo "Transaction ID: " . $response['info']['transactionId'] . "\n";
}

Laravel Integration

The package supports Laravel Package Discovery. Just install it via composer, and the ServiceProvider and Facade will be registered automatically.

Configuration

Publish the config file:

php artisan vendor:publish --tag=linepay-offline-config

Add to your .env:

LINE_PAY_CHANNEL_ID=your-channel-id
LINE_PAY_CHANNEL_SECRET=your-channel-secret
LINE_PAY_MERCHANT_DEVICE_ID=POS-001
LINE_PAY_MERCHANT_DEVICE_TYPE=POS
LINE_PAY_ENV=sandbox
LINE_PAY_TIMEOUT=40

Using Dependency Injection

namespace App\Http\Controllers;

use LinePay\Offline\LinePayOfflineClient;
use LinePay\Offline\Enums\Currency;

class POSController extends Controller
{
    public function __construct(
        private LinePayOfflineClient $linePay
    ) {}

    public function processPayment(string $oneTimeKey)
    {
        $response = $this->linePay->requestPayment([
            'amount' => 100,
            'currency' => 'TWD',
            'oneTimeKey' => $oneTimeKey,
            'orderId' => 'ORDER-' . time(),
            'packages' => [
                ['id' => 'PKG-001', 'amount' => 100, 'products' => [
                    ['name' => 'Coffee', 'quantity' => 1, 'price' => 100]
                ]]
            ]
        ]);

        return response()->json($response);
    }
}

Using Facade

use LinePay\Offline\Laravel\LinePayOffline;

// Request payment
$response = LinePayOffline::requestPayment($request);

// Check status
$status = LinePayOffline::checkPaymentStatus($orderId);

// Refund
$response = LinePayOffline::refundPayment($orderId, 50);

API Methods

Request Payment

Request payment using customer's one-time barcode:

$response = $client->requestPayment([
    'amount' => 100,
    'currency' => 'TWD',
    'oneTimeKey' => '12345678901245678',
    'orderId' => 'ORDER-001',
    'options' => [
        'payment' => [
            'capture' => true  // Capture immediately (default)
        ],
        'extra' => [
            'branchId' => 'BRANCH-001',
            'branchName' => 'Main Store'
        ]
    ]
]);

Check Payment Status

Check payment status (use after timeout):

$status = $client->checkPaymentStatus('ORDER-001');
// Returns: COMPLETE, FAIL, or REFUND
echo "Status: " . $status['info']['status'];

Capture Payment

Capture an authorized payment:

$response = $client->capturePayment(
    orderId: 'ORDER-001',
    amount: 100,
    currency: Currency::TWD
);

Void Authorization

Cancel an authorization before capture:

$response = $client->voidAuthorization('ORDER-001');

Refund Payment

Refund a completed payment:

// Full refund
$response = $client->refundPayment('ORDER-001');

// Partial refund
$response = $client->refundPayment('ORDER-001', 50);

Query Authorizations

Query authorization information:

$auths = $client->queryAuthorizations(orderId: 'ORDER-001');
// or
$auths = $client->queryAuthorizations(transactionId: '1234567890123456789');

Retrieve Payment Details

Get detailed payment information:

$details = $client->retrievePaymentDetails(orderId: 'ORDER-001');

Error Handling

use LinePay\Core\Errors\LinePayError;
use LinePay\Core\Errors\LinePayTimeoutError;
use LinePay\Core\Errors\LinePayConfigError;

try {
    $response = $client->requestPayment($request);
    
} catch (LinePayTimeoutError $e) {
    // Timeout occurred - ALWAYS check payment status
    $status = $client->checkPaymentStatus($orderId);
    
    if ($status['info']['status'] === 'COMPLETE') {
        // Payment was successful despite timeout
        handleSuccessfulPayment($status);
    } else {
        // Payment failed or still processing
        handleFailedPayment($status);
    }
    
} catch (LinePayError $e) {
    // API error
    echo "Error Code: " . $e->getReturnCode() . "\n";
    echo "Error Message: " . $e->getReturnMessage() . "\n";
    
} catch (LinePayConfigError $e) {
    // Configuration error
    echo "Config Error: " . $e->getMessage() . "\n";
}

Common Pitfalls & Troubleshooting

⚠️ Critical: Handling Timeouts (Read Timeout)

LINE Pay Offline API interactions involve real-world network latency (connecting to POS, waiting for user confirmation).

The Problem: You might receive a ConnectTimeout or ReadTimeout (cURL error 28) if the response takes longer than your configured timeout settings.

The Solution: NEVER assume the payment failed just because of a timeout. The transaction might have succeeded on LINE Pay's server while your server gave up waiting.

  1. Catch the Error: Always catch LinePayTimeoutError.
  2. Double Check: Immediately call checkPaymentStatus($orderId).
  3. Reconcile: If checkPaymentStatus returns COMPLETE, treat the order as paid.
try {
    $response = $client->requestPayment(...);
} catch (LinePayTimeoutError $e) {
    // 1. Log the timeout
    // 2. Check actual status from LINE Pay
    $status = $client->checkPaymentStatus($orderId);
    
    if ($status['info']['status'] === 'COMPLETE') {
        // Handle as success
    }
}

🚫 OneTimeKey Reuse (Error 1172)

The oneTimeKey (barcode) provided by the customer is single-use only and expires quickly (usually 5 minutes).

  • Do not try to reuse a barcode for a retry if the first request failed with a logic error.
  • Do not use a hardcoded barcode for testing; you must regenerate it from the LINE App each time.

💰 Amount Mismatch (Error 1106)

When calling capturePayment(), the amount must match the authorized amount (unless you are performing a partial capture, if allowed).

  • Ensure your database stores the exact authorized amount.
  • Floating point precision errors can occur; consider storing amounts as integers (e.g., cents) or using bcmath.

Configuration Options

Option Type Required Description
channelId string ✅ LINE Pay Channel ID
channelSecret string ✅ LINE Pay Channel Secret
merchantDeviceProfileId string ✅ Your POS/terminal device ID
merchantDeviceType string ❌ Device type (default: "POS")
env string ❌ Environment: "sandbox" or "production"
timeout int ❌ Request timeout in seconds (default: 20)

Best Practices

1. Handle Timeouts Properly

As detailed in the Common Pitfalls & Troubleshooting section, never treat a timeout as a failure. Always verify the transaction status using checkPaymentStatus().

2. Verify Payment Amounts

Always verify the payment amount in the response:

$response = $client->requestPayment($request);
$totalPaid = array_sum(array_column($response['info']['payInfo'], 'amount'));

if ($totalPaid !== $requestedAmount) {
    error_log("Amount mismatch: requested $requestedAmount, paid $totalPaid");
}

3. Use Environment Variables

Never hardcode credentials:

$config = new LinePayOfflineConfig(
    channelId: getenv('LINE_PAY_CHANNEL_ID'),
    channelSecret: getenv('LINE_PAY_CHANNEL_SECRET'),
    merchantDeviceProfileId: getenv('MERCHANT_DEVICE_ID')
);

Testing

# Run tests
composer test

# Run static analysis
composer analyze

# Check code style
composer lint

Related Packages

License

MIT License - see LICENSE for details.

Resources