xepeng / oauth-php
Xepeng OAuth & Integration PHP SDK
Requires
- php: ^7.4 || ^8.0
- ext-json: *
- guzzlehttp/guzzle: ^7.0
README
Library PHP untuk integrasi dengan layanan Xepeng. SDK ini terdiri dari dua komponen utama:
- OAuth SDK - Autentikasi pengguna menggunakan OAuth 2.0 dengan PKCE
- Integration SDK - Integrasi API untuk mengelola pesanan dan tautan pembayaran
Persyaratan Sistem
| Komponen | Minimum |
|---|---|
| PHP | 7.4 atau 8.0+ |
| Ekstensi JSON | Required |
| Guzzle HTTP | 7.0+ |
Instalasi
Via Composer
composer require xepeng/oauth-php
Manual Installation
Jika tidak menggunakan Composer, unduh source dan load secara manual:
require_once 'path/to/src/autoload.php'; // atau include satu per satu semua file di src/
Quick Start - OAuth
Contoh minimal untuk integrasi OAuth dalam 10 baris:
use Xepeng\OAuth\Client; session_start(); $client = new Client([ 'client_id' => 'your-client-id', 'client_secret' => 'your-client-secret', 'redirect_uri' => 'https://your-app.com/callback', ]); // Redirect ke halaman login Xepeng if (!isset($_GET['code'])) { header('Location: ' . $client->getAuthorizationUrl()); exit; } // Handle callback dan dapatkan token $tokens = $client->handleCallback(); echo "Access Token: " . $tokens['access_token'];
Bagian 1: OAuth SDK
1.1 Konfigurasi
Parameter Konfigurasi
| Parameter | Tipe | Required | Default | Deskripsi |
|---|---|---|---|---|
client_id |
string | Ya | - | Client ID dari dashboard Xepeng |
client_secret |
string | Ya | - | Client Secret dari dashboard Xepeng |
redirect_uri |
string | Ya | - | URL callback setelah autentikasi |
env |
string | Tidak | development |
development atau production |
scopes |
array | Tidak | ['profile', 'email'] |
Scope OAuth yang diminta |
base_url |
string | Tidak | auto | Override base URL untuk OAuth |
api_base_url |
string | Tidak | auto | Override API base URL |
storage_adapter |
StorageInterface | Tidak | SessionStorage | Adapter storage kustom |
auto_refresh |
bool | Tidak | true |
Auto-refresh token sebelum expired |
refresh_buffer |
int | Tidak | 300 |
Detik sebelum expired untuk refresh |
Environment URLs
// Development 'base_url' => 'https://staging-app.xepeng.com' 'api_base_url' => 'https://staging-api.xepeng.com' // Production 'base_url' => 'https://app.xepeng.com' 'api_base_url' => 'https://api.xepeng.com'
1.2 Alur OAuth + PKCE
SDK ini menggunakan OAuth 2.0 PKCE (Proof Key for Code Exchange) untuk keamanan tambahan. Berikut step-by-step alur autentikasi:
┌─────────────────────────────────────────────────────────────────────────────┐
│ ALUR OAUTH + PKCE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ [1] GENERATE RANDOM STATE & PKCE │
│ ├── Generate random state (43 chars) │
│ ├── Generate code_verifier (64 chars) │
│ └── Generate code_challenge (SHA256 + base64url encode) │
│ │
│ [2] REDIRECT KE XEPENG │
│ GET https://app.xepeng.com/oauth/authorize │
│ ?client_id=xxx │
│ &redirect_uri=xxx │
│ &response_type=code │
│ &scope=profile+email │
│ &state=xxx │
│ &code_challenge=xxx │
│ &code_challenge_method=S256 │
│ │
│ [3] USER LOGIN DI XEPENG │
│ └── User authorize aplikasi │
│ │
│ [4] REDIRECT BACK TO CALLBACK │
│ GET https://your-app.com/callback?code=xxx&state=xxx │
│ │
│ [5] EXCHANGE CODE FOR TOKEN │
│ POST /oauth/token │
│ { │
│ grant_type: 'authorization_code', │
│ code: 'xxx', │
│ redirect_uri: 'xxx', │
│ client_id: 'xxx', │
│ client_secret: 'xxx', │
│ code_verifier: 'xxx' ← PKCE verifier untuk verifikasi │
│ } │
│ │
│ [6] RESPONSE │
│ { │
│ access_token: 'xxx', │
│ refresh_token: 'xxx', │
│ expires_in: 3600, │
│ token_type: 'Bearer' │
│ } │
│ │
│ [7] STORE TOKENS │
│ └── Tokens disimpan di storage (session/redis/db) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Mengapa PKCE?
PKCE menambahkan lapisan keamanan dengan:
- Code Verifier - Secret yang hanya diketahui client
- Code Challenge - Hash dari verifier yang dikirim ke server
- Mencegah interception -即使有人拦截了code,也无法兑换token tanpa verifier
1.3 Penggunaan Vanilla PHP
Contoh Lengkap
<?php require 'vendor/autoload.php'; use Xepeng\OAuth\Client; use Xepeng\OAuth\Exceptions\OAuthException; session_start(); $client = new Client([ 'client_id' => 'your-client-id', 'client_secret' => 'your-client-secret', 'redirect_uri' => 'https://your-app.com/callback', 'env' => 'production', 'scopes' => ['profile', 'email'], ]); // Step 1: Cek apakah ada code di callback if (!isset($_GET['code'])) { // Redirect ke halaman login Xepeng $authUrl = $client->getAuthorizationUrl(); header('Location: ' . $authUrl); exit; } // Step 2: Handle callback try { $tokenResponse = $client->handleCallback(); // Response token: // { // "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", // "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...", // "expires_in": 3600, // "token_type": "Bearer" // } echo "Login berhasil!\n"; echo "Access Token: " . $tokenResponse['access_token'] . "\n"; echo "Expires In: " . $tokenResponse['expires_in'] . " detik\n"; // Optional: Ambil info user // $user = $client->getUserInfo(); // echo "User: " . $user['email']; } catch (OAuthException $e) { echo "Error: " . $e->getMessage() . "\n"; echo "Error Code: " . $e->getError() . "\n"; }
Contoh: Cek Status Autentikasi
// Cek apakah user sudah login if ($client->isAuthenticated()) { echo "User sudah login"; // Ambil access token (auto-refresh jika perlu) $token = $client->getAccessToken(); echo "Token: " . $token; } else { echo "User belum login, redirect ke login..."; header('Location: ' . $client->getAuthorizationUrl()); }
Contoh: Logout
// Revoke token di server dan clear local storage $client->revokeTokens(); // Atau hanya clear local storage (tanpa revoke) $client->logout();
1.4 Integrasi Laravel
Step 1: Konfigurasi Environment
Tambahkan kredensial ke file .env:
XEPENG_CLIENT_ID=your_client_id XEPENG_CLIENT_SECRET=your_client_secret XEPENG_REDIRECT_URI=https://your-app.com/callback/xepeng XEPENG_ENV=development
Step 2: Buat Config File
Buat file config/xepeng.php:
<?php return [ 'client_id' => env('XEPENG_CLIENT_ID'), 'client_secret' => env('XEPENG_CLIENT_SECRET'), 'redirect_uri' => env('XEPENG_REDIRECT_URI'), 'env' => env('XEPENG_ENV', 'development'), 'scopes' => ['profile', 'email'], ];
Step 3: Buat Session Storage Adapter
Buat service app/Services/LaravelSessionStorage.php:
<?php namespace App\Services; use Xepeng\OAuth\Storage\StorageInterface; use Illuminate\Support\Facades\Session; class LaravelSessionStorage implements StorageInterface { private string $prefix; public function __construct(string $prefix = 'xepeng_oauth_') { $this->prefix = $prefix; } public function set($key, $value): void { Session::put($this->prefix . $key, $value); } public function get($key) { return Session::get($this->prefix . $key); } public function remove($key): void { Session::forget($this->prefix . $key); } public function clear(): void { Session::forget($this->prefix . 'tokens'); Session::forget($this->prefix . 'oauth_state'); } public function has($key): bool { return Session::has($this->prefix . $key); } }
Step 4: Buat Controller
Buat controller app/Http/Controllers/XepengOAuthController.php:
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Xepeng\OAuth\Client; use App\Services\LaravelSessionStorage; class XepengOAuthController extends Controller { private Client $client; public function __construct() { $this->client = new Client([ 'client_id' => config('xepeng.client_id'), 'client_secret' => config('xepeng.client_secret'), 'redirect_uri' => config('xepeng.redirect_uri'), 'env' => config('xepeng.env'), 'scopes' => config('xepeng.scopes'), 'storage_adapter' => new LaravelSessionStorage(), ]); } public function redirect() { return redirect()->away($this->client->getAuthorizationUrl()); } public function callback(Request $request) { try { $tokenResponse = $this->client->handleCallback($request->all()); return response()->json([ 'message' => 'Authentication successful', 'tokens' => $tokenResponse ]); } catch (\Exception $e) { return response()->json([ 'error' => $e->getMessage(), 'code' => method_exists($e, 'getError') ? $e->getError() : 'unknown' ], 400); } } public function logout() { $this->client->revokeTokens(); return response()->json(['message' => 'Logged out successfully']); } public function userInfo() { try { $user = $this->client->getUserInfo(); return response()->json($user); } catch (\Exception $e) { return response()->json(['error' => $e->getMessage()], 401); } } }
Step 5: Tambah Routes
Tambahkan routes di routes/web.php:
use App\Http\Controllers\XepengOAuthController; Route::get('/login/xepeng', [XepengOAuthController::class, 'redirect']); Route::get('/callback/xepeng', [XepengOAuthController::class, 'callback']); Route::post('/logout/xepeng', [XepengOAuthController::class, 'logout']); Route::get('/user/xepeng', [XepengOAuthController::class, 'userInfo']);
Step 6: Register Service Provider (Optional)
Untuk penggunaan yang lebih bersih, buat Service Provider:
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use Xepeng\OAuth\Client; use App\Services\LaravelSessionStorage; class XepengOAuthServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton(Client::class, function ($app) { return new Client([ 'client_id' => config('xepeng.client_id'), 'client_secret' => config('xepeng.client_secret'), 'redirect_uri' => config('xepeng.redirect_uri'), 'env' => config('xepeng.env'), 'scopes' => config('xepeng.scopes'), 'storage_adapter' => new LaravelSessionStorage(), ]); }); } public function boot(): void { // } }
1.5 Custom Storage
Jika kamu tidak ingin menggunakan session atau ingin menggunakan storage lain (database, Redis, file), implementasikan interface StorageInterface:
<?php namespace App\Storage; use Xepeng\OAuth\Storage\StorageInterface; class DatabaseStorage implements StorageInterface { private string $prefix; private $db; public function __construct($db, string $prefix = 'xepeng_oauth_') { $this->db = $db; $this->prefix = $prefix; } public function set($key, $value): void { $this->db->table('oauth_storage') ->updateOrInsert( ['key' => $this->prefix . $key], ['value' => json_encode($value)] ); } public function get($key) { $row = $this->db->table('oauth_storage') ->where('key', $this->prefix . $key) ->first(); return $row ? json_decode($row->value, true) : null; } public function remove($key): void { $this->db->table('oauth_storage') ->where('key', $this->prefix . $key) ->delete(); } public function clear(): void { $this->db->table('oauth_storage') ->where('key', 'like', $this->prefix . '%') ->delete(); } public function has($key): bool { return $this->db->table('oauth_storage') ->where('key', $this->prefix . $key) ->exists(); } }
Penggunaan Custom Storage
use Xepeng\OAuth\Client; $client = new Client([ 'client_id' => 'your-client-id', 'client_secret' => 'your-client-secret', 'redirect_uri' => 'https://your-app.com/callback', 'storage_adapter' => new DatabaseStorage($db), ]);
1.6 Referensi API - OAuth SDK
Method Reference
| Method | Parameter | Return | Deskripsi |
|---|---|---|---|
__construct() |
array $config |
- | Inisialisasi client |
getAuthorizationUrl() |
- | string |
Generate URL untuk redirect ke login Xepeng |
handleCallback() |
array|null $queryParams |
array |
Proses callback dari Xepeng, return tokens |
exchangeCodeForToken() |
string $code, string $codeVerifier |
array |
Tukar authorization code dengan token |
refreshAccessToken() |
- | array |
Refresh access token menggunakan refresh token |
getUserInfo() |
- | array |
Ambil informasi user yang login |
revokeTokens() |
- | void |
Revoke token di server dan clear local |
logout() |
- | void |
Clear local storage tokens |
isAuthenticated() |
- | bool |
Cek apakah user sudah login dan token valid |
getTokens() |
- | array|null |
Ambil semua tokens dari storage |
getAccessToken() |
- | string |
Ambil access token (auto-refresh jika perlu) |
Exception Reference
use Xepeng\OAuth\Exceptions\OAuthException; // OAuthException methods $e->getMessage(); // Pesan error $e->getError(); // Error code string $e->getCode(); // HTTP status code
Error Codes
| Error Code | Deskripsi |
|---|---|
invalid_callback |
Callback tidak memiliki code atau state |
invalid_state |
State PKCE tidak cocok atau expired |
no_refresh_token |
Tidak ada refresh token untuk di-refresh |
not_authenticated |
User belum login |
userinfo_failed |
Gagal mengambil info user |
token_error |
Error saat request token |
Bagian 2: Integration SDK
2.1 Constructor Parameters
Integration SDK adalah client untuk API Xepeng Payment Gateway yang tidak memerlukan OAuth. Cocok untuk backend-to-backend integration.
use Xepeng\OAuth\Integration\XepengIntegrationClient; $client = new XepengIntegrationClient( string $clientId, // Required: Client ID dari dashboard Xepeng string $clientSecret, // Required: Client Secret dari dashboard Xepeng bool $isProduction = false, // Optional: true = production, false = staging ?string $baseUrl = null, // Optional: Override base URL ?GuzzleClient $httpClient = null // Optional: Custom Guzzle instance );
Parameter Detail
| Parameter | Tipe | Required | Default | Deskripsi |
|---|---|---|---|---|
$clientId |
string | Ya | - | Client ID dari dashboard Xepeng |
$clientSecret |
string | Ya | - | Client Secret dari dashboard Xepeng |
$isProduction |
bool | Tidak | false |
true untuk production URL |
$baseUrl |
string | Tidak | null (auto) | Override base URL jika perlu |
$httpClient |
GuzzleClient | Tidak | null | Custom Guzzle instance |
Base URLs
// Staging (development) 'https://staging-api.xepeng.com' // Production 'https://api.xepeng.com'
2.2 Mekanisme Signature HMAC-SHA256
Setiap request ke Integration API diamankan menggunakan HMAC-SHA256 signature. SDK menangani pembuatan signature secara otomatis.
Format String yang Di-sign
METHOD + PATH + TIMESTAMP + BODY
Contoh Signature Generation
// Contoh untuk POST /openapi/orders $method = 'POST'; $path = '/openapi/orders'; $timestamp = time(); // Unix timestamp $body = '{"items":[{"amount":50000,"product_name":"Kemeja Flanel"}]}'; // Payload yang di-hash $payload = strtoupper($method) . $path . (string)$timestamp . $body; // Generate signature $signature = hash_hmac('sha256', $payload, $clientSecret); // Result: 'a1b2c3d4e5f6...'
Headers yang Dikirim
Setiap request otomatis menyertakan header berikut:
| Header | Contoh | Deskripsi |
|---|---|---|
X-Client-ID |
client_xxx |
Client ID Anda |
X-Timestamp |
1715251200 |
Unix timestamp request |
X-Signature |
a1b2c3... |
HMAC-SHA256 signature |
Content-Type |
application/json |
Request content type |
Accept |
application/json |
Response content type |
Kenapa HMAC Signature?
- Integritas Data - Me确保消息在传输过程中未被篡改
- Autentikasi - Hanya client dengan secret yang valid bisa membuat signature
- Replay Protection - Timestamp memastikan request tidak bisa di-replay
2.3 Alur Pembayaran (PENTING!)
Ini adalah bagian penting yang harus dipahami. Payment link tidak bisa dibuat secara langsung - необходимо mengikuti urutan langkah berikut:
┌─────────────────────────────────────────────────────────────────────────────┐
│ ALUR PEMBUATAN PEMBAYARAN │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ STEP 1: BUAT ORDER │
│ ══════════════════════════ │
│ POST /openapi/orders │
│ │ │
│ │ { │
│ │ "items": [ │
│ │ { │
│ │ "amount": 50000, │
│ │ "product_name": "Kemeja Flanel" │
│ │ } │
│ │ ] │
│ │ } │
│ │ │
│ ↓ │
│ Response: { "data": { "uid": "ord_xxx" } } │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ PENJELASAN: │ │
│ │ - Order adalah container untuk item-item yang akan dibayar │ │
│ │ - Setiap order memiliki unique ID (uid) │ │
│ │ - Status default order adalah 'pending' │ │
│ │ - Payment link hanya bisa di-generate untuk order dengan status 'active' │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ STEP 2: UPDATE ORDER STATUS KE 'active' │
│ ═══════════════════════════════════════════════ │
│ PUT /openapi/orders/{order_uid} │
│ │ │
│ │ { │
│ │ "status": "active", │
│ │ "items": [...] // item yang sama atau berbeda │
│ │ } │
│ │ │
│ ↓ │
│ Response: { "data": { "uid": "ord_xxx", "status": "active" } } │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ PENJELASAN: │ │
│ │ - Setelah order dibuat, status masih 'pending' │ │
│ │ - Order harus di-update ke status 'active' sebelum bisa generate payment │ │
│ │ - Kenapa perlu langkah ini? │ │
│ │ 1. Memberikan kesempatan untuk modify items sebelum payment │ │
│ │ 2. Konfirmasi akhir dari merchant sebelum payment link dibuat │ │
│ │ 3. Beberapa kasus butuh approval sebelum payment │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ STEP 3: GENERATE PAYMENT LINK │
│ ════════════════════════════════ │
│ POST /openapi/payment-links/generate │
│ │ │
│ │ { │
│ │ "order_uid": "ord_xxx", │
│ │ "expired_at": "2024-05-20T18:00:00Z", │
│ │ "callback_url": "https://your-app.com/api/notification", │
│ │ "success_url": "https://your-app.com/success", │
│ │ "cancel_url": "https://your-app.com/cancel" │
│ │ } │
│ │ │
│ ↓ │
│ Response: { "data": { "payment_url": "https://pay.xepeng.com/xxx" } } │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ PENJELASAN: │ │
│ │ - Payment link bind ke order_uid tertentu │ │
│ │ - Order harus berstatus 'active' untuk bisa generate payment link │ │
│ │ - Payment link memiliki expired_at (waktu kadaluarsa) │ │
│ │ - Callback URL untuk notifikasi payment status │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ STEP 4: REDIRECT USER KE HALAMAN PEMBAYARAN │
│ ══════════════════════════════════════════════════ │
│ │
│ header('Location: ' . $paymentUrl); │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Contoh Lengkap Full Flow
<?php require 'vendor/autoload.php'; use Xepeng\OAuth\Integration\XepengIntegrationClient; $client = new XepengIntegrationClient( 'YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET', false // false = staging, true = production ); // ======================================== // STEP 1: Buat Order // ======================================== $items = [ [ 'amount' => 50000, 'notes' => 'Pembelian Kemeja', 'product_description' => 'Kemeja Flanel Ukuran L', 'product_name' => 'Kemeja Flanel', ], [ 'amount' => 25000, 'notes' => 'Ongkos kirim', 'product_description' => 'Pengiriman via JNE', 'product_name' => 'Ongkos Kirim', ], ]; $orderResponse = $client->orders()->create($items); // Response: // { // "success": true, // "message": "Order created successfully", // "data": { // "uid": "ord_a1b2c3d4e5f6", // "status": "pending", // "items": [...], // "created_at": "2024-05-15T10:30:00Z" // } // } $orderUid = $orderResponse['data']['uid']; echo "Order dibuat dengan UID: $orderUid\n"; // ======================================== // STEP 2: Update Order ke Active // ======================================== $updateResponse = $client->orders()->update($orderUid, $items, 'active'); // Response: // { // "success": true, // "message": "Order updated successfully", // "data": { // "uid": "ord_a1b2c3d4e5f6", // "status": "active", // "items": [...], // "updated_at": "2024-05-15T10:35:00Z" // } // } echo "Order diupdate ke status: active\n"; // ======================================== // STEP 3: Generate Payment Link // ======================================== $options = [ 'expired_at' => date('c', strtotime('+2 hours')), 'callback_url' => 'https://your-app.com/api/xepeng/notification', 'success_url' => 'https://your-app.com/payment/success', 'cancel_url' => 'https://your-app.com/payment/cancel', ]; $paymentResponse = $client->paymentLinks()->generate($orderUid, $options); // Response: // { // "success": true, // "message": "Payment link generated successfully", // "data": { // "uid": "pl_a1b2c3d4e5f6", // "order_uid": "ord_a1b2c3d4e5f6", // "payment_url": "https://staging-pay.xepeng.com/pay/abc123xyz", // "amount": 75000, // "status": "pending", // "expired_at": "2024-05-15T12:35:00Z", // "created_at": "2024-05-15T10:35:00Z" // } // } $paymentUrl = $paymentResponse['data']['payment_url']; echo "Payment Link: $paymentUrl\n"; // ======================================== // STEP 4: Redirect User // ======================================== header('Location: ' . $paymentUrl); exit;
2.4 Manajemen Order
Order adalah container untuk item-item yang akan dibayar. Setiap order memiliki status dan dapat di-update.
Method: create()
Membuat order baru dengan item-item.
Request:
$items = [ [ 'amount' => 50000, 'notes' => 'Deskripsi item', 'product_description' => 'Kemeja Flanel Ukuran L - Merah', 'product_name' => 'Kemeja Flanel', ], [ 'amount' => 15000, 'notes' => 'Item tambahan', 'product_description' => 'Dust Bag', 'product_name' => 'Tas', ], ]; $response = $client->orders()->create($items);
Response Sukses:
{
"success": true,
"message": "Order created successfully",
"data": {
"uid": "ord_7f8g9h0i1j2k",
"status": "pending",
"total_amount": 65000,
"items": [
{
"amount": 50000,
"notes": "Deskripsi item",
"product_description": "Kemeja Flanel Ukuran L - Merah",
"product_name": "Kemeja Flanel"
},
{
"amount": 15000,
"notes": "Item tambahan",
"product_description": "Dust Bag",
"product_name": "Tas"
}
],
"created_at": "2024-05-15T10:30:00Z",
"updated_at": "2024-05-15T10:30:00Z"
}
}
Response Error:
{
"success": false,
"message": "Order items cannot be empty.",
"error_code": "VALIDATION_ERROR"
}
Validasi Input:
| Field | Required | Rules |
|---|---|---|
amount |
Ya | Positive integer (> 0) |
product_name |
Ya | Non-empty string |
notes |
Tidak | String |
product_description |
Tidak | String |
Method: get()
Mengambil detail order berdasarkan UID.
Request:
$orderUid = 'ord_7f8g9h0i1j2k'; $response = $client->orders()->get($orderUid);
Response Sukses:
{
"success": true,
"data": {
"uid": "ord_7f8g9h0i1j2k",
"status": "active",
"total_amount": 65000,
"items": [
{
"amount": 50000,
"notes": "Deskripsi item",
"product_description": "Kemeja Flanel Ukuran L - Merah",
"product_name": "Kemeja Flanel"
},
{
"amount": 15000,
"notes": "Item tambahan",
"product_description": "Dust Bag",
"product_name": "Tas"
}
],
"created_at": "2024-05-15T10:30:00Z",
"updated_at": "2024-05-15T10:35:00Z"
}
}
Response Error:
{
"success": false,
"message": "Order not found",
"error_code": "NOT_FOUND"
}
Method: update()
Mengupdate order (status dan/atau items). Wajib dipanggil untuk mengubah status dari pending ke active sebelum bisa generate payment link.
Request:
$orderUid = 'ord_7f8g9h0i1j2k'; // Update status menjadi active $response = $client->orders()->update($orderUid, $items, 'active'); // Update items tanpa ubah status // $response = $client->orders()->update($orderUid, $newItems, 'pending'); // Update items dan ubah status sekaligus // $response = $client->orders()->update($orderUid, $newItems, 'active');
Response Sukses:
{
"success": true,
"message": "Order updated successfully",
"data": {
"uid": "ord_7f8g9h0i1j2k",
"status": "active",
"total_amount": 65000,
"items": [
{
"amount": 50000,
"notes": "Deskripsi item",
"product_description": "Kemeja Flanel Ukuran L - Merah",
"product_name": "Kemeja Flanel"
},
{
"amount": 15000,
"notes": "Item tambahan",
"product_description": "Dust Bag",
"product_name": "Tas"
}
],
"created_at": "2024-05-15T10:30:00Z",
"updated_at": "2024-05-15T10:35:00Z"
}
}
Response Error (Order tidak ditemukan):
{
"success": false,
"message": "Order not found",
"error_code": "NOT_FOUND"
}
Response Error (UID kosong):
{
"success": false,
"message": "Order UID is required for update.",
"error_code": "VALIDATION_ERROR"
}
Status Order:
| Status | Deskripsi |
|---|---|
pending |
Order baru dibuat, belum aktif |
active |
Order aktif, bisa generate payment link |
inactive |
Order tidak aktif |
Method: list()
Mengambil daftar order dengan pagination.
Request:
$page = 1; $limit = 10; $response = $client->orders()->list($page, $limit);
Response Sukses:
{
"success": true,
"data": {
"orders": [
{
"uid": "ord_7f8g9h0i1j2k",
"status": "active",
"total_amount": 65000,
"items": [...],
"created_at": "2024-05-15T10:30:00Z"
},
{
"uid": "ord_6e7f8g9h0i1j",
"status": "pending",
"total_amount": 120000,
"items": [...],
"created_at": "2024-05-14T15:20:00Z"
}
],
"pagination": {
"current_page": 1,
"total_pages": 5,
"total_items": 47,
"limit": 10
}
}
}
2.5 Manajemen Payment Link
Payment link adalah tautan pembayaran yang di-bind ke order tertentu. User akan diarahkan ke halaman pembayaran Xepeng melalui link ini.
Method: generate()
Membuat payment link untuk order tertentu.
Request:
$orderUid = 'ord_7f8g9h0i1j2k'; $options = [ 'expired_at' => date('c', strtotime('+2 hours')), // Format ISO8601 'callback_url' => 'https://your-app.com/api/xepeng/notification', 'success_url' => 'https://your-app.com/payment/success?order_id=xxx', 'cancel_url' => 'https://your-app.com/payment/cancel?order_id=xxx', ]; $response = $client->paymentLinks()->generate($orderUid, $options);
Options Reference:
| Option | Tipe | Required | Deskripsi |
|---|---|---|---|
expired_at |
string (ISO8601) | Tidak | Waktu kadaluarsa payment link. Format: 2024-05-15T18:00:00Z |
callback_url |
string (URL) | Tidak | URL untuk menerima notification payment |
success_url |
string (URL) | Tidak | URL redirect setelah payment berhasil |
cancel_url |
string (URL) | Tidak | URL redirect jika user cancel payment |
Response Sukses:
{
"success": true,
"message": "Payment link generated successfully",
"data": {
"uid": "pl_m3n4o5p6q7r",
"order_uid": "ord_7f8g9h0i1j2k",
"payment_url": "https://staging-pay.xepeng.com/pay/s8t9u0v1w2x3y4z",
"amount": 65000,
"status": "pending",
"expired_at": "2024-05-15T12:30:00Z",
"callback_url": "https://your-app.com/api/xepeng/notification",
"success_url": "https://your-app.com/payment/success?order_id=ord_7f8g9h0i1j2k",
"cancel_url": "https://your-app.com/payment/cancel?order_id=ord_7f8g9h0i1j2k",
"created_at": "2024-05-15T10:35:00Z"
}
}
Response Error (Order UID kosong):
{
"success": false,
"message": "Order UID is required to generate payment link.",
"error_code": "VALIDATION_ERROR"
}
Response Error (Order tidak aktif):
{
"success": false,
"message": "Order must be active to generate payment link.",
"error_code": "INVALID_ORDER_STATUS"
}
Method: get()
Mengambil detail payment link berdasarkan UID.
Request:
$paymentLinkUid = 'pl_m3n4o5p6q7r'; $response = $client->paymentLinks()->get($paymentLinkUid);
Response Sukses:
{
"success": true,
"data": {
"uid": "pl_m3n4o5p6q7r",
"order_uid": "ord_7f8g9h0i1j2k",
"payment_url": "https://staging-pay.xepeng.com/pay/s8t9u0v1w2x3y4z",
"amount": 65000,
"status": "pending",
"expired_at": "2024-05-15T12:30:00Z",
"callback_url": "https://your-app.com/api/xepeng/notification",
"success_url": "https://your-app.com/payment/success?order_id=ord_7f8g9h0i1j2k",
"cancel_url": "https://your-app.com/payment/cancel?order_id=ord_7f8g9h0i1j2k",
"created_at": "2024-05-15T10:35:00Z",
"paid_at": null,
"payment_method": null
}
}
Status Payment Link:
| Status | Deskripsi |
|---|---|
pending |
Menunggu pembayaran |
paid |
Sudah dibayar |
expired |
Kadaluarsa |
cancelled |
Dibatalkan |
Method: list()
Mengambil daftar payment link.
Request:
$response = $client->paymentLinks()->list();
Response Sukses:
{
"success": true,
"data": {
"payment_links": [
{
"uid": "pl_m3n4o5p6q7r",
"order_uid": "ord_7f8g9h0i1j2k",
"payment_url": "https://staging-pay.xepeng.com/pay/s8t9u0v1w2x3y4z",
"amount": 65000,
"status": "pending",
"expired_at": "2024-05-15T12:30:00Z",
"created_at": "2024-05-15T10:35:00Z"
},
{
"uid": "pl_k1l2m3n4o5p",
"order_uid": "ord_j0i1h2g3f4e",
"payment_url": "https://staging-pay.xepeng.com/pay/d5e6f7g8h9i0",
"amount": 120000,
"status": "paid",
"expired_at": "2024-05-14T18:00:00Z",
"created_at": "2024-05-14T14:00:00Z",
"paid_at": "2024-05-14T15:30:00Z",
"payment_method": "bank_transfer"
}
],
"pagination": {
"current_page": 1,
"total_pages": 3,
"total_items": 25,
"limit": 10
}
}
}
Method: inactivate()
Membatalkan (menonaktifkan) payment link.
Request:
$paymentLinkUid = 'pl_m3n4o5p6q7r'; $response = $client->paymentLinks()->inactivate($paymentLinkUid);
Response Sukses:
{
"success": true,
"message": "Payment link inactivated successfully",
"data": {
"uid": "pl_m3n4o5p6q7r",
"order_uid": "ord_7f8g9h0i1j2k",
"status": "inactive",
"updated_at": "2024-05-15T11:00:00Z"
}
}
Response Error:
{
"success": false,
"message": "Payment link not found",
"error_code": "NOT_FOUND"
}
2.6 Integrasi Laravel
Konfigurasi
Tambahkan konfigurasi di config/services.php:
'xepeng' => [ 'client_id' => env('XEPENG_CLIENT_ID'), 'client_secret' => env('XEPENG_CLIENT_SECRET'), 'is_production' => env('XEPENG_PRODUCTION', false), ],
Tambahkan di .env:
XEPENG_CLIENT_ID=your_client_id XEPENG_CLIENT_SECRET=your_client_secret XEPENG_PRODUCTION=false
Service Class
Buat service class untuk mengelola client:
<?php namespace App\Services; use Xepeng\OAuth\Integration\XepengIntegrationClient; class XepengService { private ?XepengIntegrationClient $client = null; public function client(): XepengIntegrationClient { if ($this->client === null) { $this->client = new XepengIntegrationClient( config('services.xepeng.client_id'), config('services.xepeng.client_secret'), config('services.xepeng.is_production') ); } return $this->client; } public function createOrder(array $items): array { return $this->client()->orders()->create($items); } public function activateOrder(string $orderUid, array $items): array { return $this->client()->orders()->update($orderUid, $items, 'active'); } public function generatePaymentLink(string $orderUid, array $options = []): array { return $this->client()->paymentLinks()->generate($orderUid, $options); } public function getPaymentStatus(string $paymentLinkUid): array { return $this->client()->paymentLinks()->get($paymentLinkUid); } }
Controller Contoh
<?php namespace App\Http\Controllers; use App\Services\XepengService; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; class PaymentController extends Controller { private XepengService $xepeng; public function __construct(XepengService $xepeng) { $this->xepeng = $xepeng; } public function create(Request $request): JsonResponse { $request->validate([ 'items' => 'required|array|min:1', 'items.*.amount' => 'required|numeric|min:1', 'items.*.product_name' => 'required|string', ]); // Step 1: Buat Order $orderResponse = $this->xepeng->createOrder($request->items); $orderUid = $orderResponse['data']['uid']; // Step 2: Update ke Active $this->xepeng->activateOrder($orderUid, $request->items); // Step 3: Generate Payment Link $paymentResponse = $this->xepeng->generatePaymentLink($orderUid, [ 'expired_at' => now()->addHours(2)->toIso8601String(), 'callback_url' => url('/api/xepeng/webhook'), 'success_url' => url('/payment/success'), 'cancel_url' => url('/payment/cancel'), ]); return response()->json([ 'success' => true, 'payment_url' => $paymentResponse['data']['payment_url'], 'order_uid' => $orderUid, ]); } public function webhook(Request $request): JsonResponse { // Handle payment notification // $request->all() berisi data payment status return response()->json(['received' => true]); } }
Route Webhook
Route::post('/api/xepeng/webhook', [PaymentController::class, 'webhook']);
3. Error Handling
OAuth SDK Exceptions
use Xepeng\OAuth\Exceptions\OAuthException; try { $client = new Client([...]); $tokens = $client->handleCallback(); } catch (OAuthException $e) { // Handle OAuth errors $message = $e->getMessage(); // "Invalid state parameter" $error = $e->getError(); // "invalid_state" $code = $e->getCode(); // HTTP status code }
Integration SDK Exceptions
use Xepeng\OAuth\Integration\Exceptions\XepengException; try { $client = new XepengIntegrationClient(...); $response = $client->orders()->create($items); } catch (XepengException $e) { // Handle integration errors $message = $e->getMessage(); // "Order items cannot be empty." $code = $e->getCode(); // HTTP status code atau 0 }
Common Error Scenarios
| Skenario | Error | Solusi |
|---|---|---|
| Order items kosong | Order items cannot be empty. |
Pastikan array items tidak kosong |
| Amount <= 0 | Item at index X must have a positive amount. |
Gunakan amount > 0 |
| Product name kosong | Item at index X must have a product_name. |
Pastikan setiap item punya product_name |
| Order UID kosong | Order UID is required for update. |
Berikan order_uid yang valid |
| Order belum active | Order must be active to generate payment link. |
Panggil update() dengan status 'active' |
| PKCE state mismatch | PKCE state mismatch or expired. |
State tidak cocok atau sudah expired, mulai ulang flow |
| Missing code/state | Missing code or state in callback |
Cek parameter callback dari Xepeng |
4. FAQ (Pertanyaan yang Sering Diajukan)
Q: Apa perbedaan OAuth SDK dan Integration SDK?
A:
- OAuth SDK - Digunakan untuk autentikasi end-user (user login dengan akun Xepeng). Menggunakan OAuth 2.0 dengan PKCE.
- Integration SDK - Digunakan untuk backend-to-backend integration. Tidak memerlukan login user, langsung pakai client credentials (client_id + client_secret) dengan signature HMAC.
Q: Kapan harus pakai OAuth SDK?
A: Gunakan OAuth SDK ketika kamu ingin:
- User login menggunakan akun Xepeng
- Akses data user (profile, email, dll)
- User melakukan autentikasi, bukan hanya payment
Q: Kapan harus pakai Integration SDK?
A: Gunakan Integration SDK ketika kamu:
- Backend system yang perlu memproses payment
- Tidak memerlukan user login
- Sudah punya sistem autentikasi sendiri
- Contoh: e-commerce, membership site, dll
Q: Kenapa harus buat order duluan?
A: Order adalah representasi dari transaksi yang akan dibayar. Payment link di-bind ke order tertentu. Ini memastikan:
- Tracking transaksi yang jelas
- Items sudah didefinisikan sebelum payment
- Amount sudah fix sebelum payment link dibuat
Q: Kenapa harus update order ke 'active'?
A: Status pending → active memberikan kesempatan untuk:
- Modify items sebelum finalize
- Konfirmasi akhir dari merchant
- Approval workflow jika diperlukan
- Beberapa sistem butuh validasi sebelum payment
Q: Berapa lama payment link aktif?
A: Default tidak ada expiry. Gunakan option expired_at untuk membatasi waktu validity. Contoh: 2 jam setelah generated.
Q: Apakah bisa generate ulang payment link untuk order yang sama?
A: Ya, bisa. Order dengan status active bisa generate multiple payment links.
Q: Apakah bisa ubah items setelah order dibuat?
A: Ya, gunakan method update() untuk ubah items dan/atau status.
Q: Error "PKCE state mismatch" - bagaimana cara fix?
A: Error ini terjadi karena:
- State tidak cocok - kemungkinan ada lebih dari satu callback request
- State expired - state sudah kadaluarsa (> 10 menit)
Solusi: Mulai ulang OAuth flow dari awal (redirect ke getAuthorizationUrl lagi).
Q: Apakah SDK ini thread-safe?
A: Tidak ada mekanisme locking internal. Jika menggunakan multi-thread/concurrent, pastikan masing-masing thread punya storage terpisah atau gunakan database storage dengan proper locking.
5. Lisensi
MIT License - Xepeng 2024
6. Dukungan
Untuk pertanyaan dan laporan bug:
- Email: hello@xepeng.com
- Website: https://xepeng.com