A Laravel package for seamless integration with Xendit payment gateway. Built from scratch using Laravel's HTTP client, this package provides a fluent API for creating payments, managing transactions, and handling webhooks - all with database persistence and event-driven architecture.
- 🔥 Fluent API for creating payments
- 💳 Support for all payment methods (E-Wallet, Virtual Account, QR Code, Cards, OTC)
- 🗄️ Database persistence for payments, transactions, and webhooks
- 🔔 Automatic webhook handling with signature verification
- 🎯 Event-driven architecture (Payment, Refund, Token events)
- 🔗 Polymorphic relationships (attach payments to any model)
- 📦 Uses Laravel's HTTP client (no Guzzle dependency)
- 🛡️ Type-safe with PHP 8.2+ Backed Enums
- 💰 Complete refund management
- 🔑 Payment token (saved payment methods) support
- 🔗 Payment links generation
- 📊 Transaction querying and listing
- 🎫 Session management
- 🏷️ Custom HTTP headers on any API call (
for-user-id,with-split-rule, etc.)
- ✅ Payment Request - Create, get, cancel, simulate
- ✅ Payment - Get status, cancel, capture
- ✅ Payment Token - Create, get, deactivate
- ✅ Customer - Create, get, list, update
- ✅ Session - Create, get, cancel with DB persistence
- ✅ Refund - Create refunds
- ✅ Payment Link - Create and manage payment links
- ✅ Transaction - Get and list transactions
- ✅ Webhooks - All webhook events supported
- ✅ Logging - Outbound API call audit trail with retention/pruning
- PHP 8.2+
- Laravel 10.x, 11.x, or 12.x
Install the package via composer:
composer require laraditz/xenditPublish the configuration and migrations:
php artisan vendor:publish --tag=xendit-config
php artisan vendor:publish --tag=xendit-migrationsRun the migrations:
php artisan migrateRun the seeder:
php artisan db:seed --class=Laraditz\\Xendit\\Database\\Seeders\\DatabaseSeederAdd your Xendit credentials to .env:
XENDIT_API_KEY=your-secret-api-key
XENDIT_WEBHOOK_SECRET=your-webhook-verification-token
XENDIT_CURRENCY=MYRuse Laraditz\Xendit\Facades\Xendit;
$payment = Xendit::paymentRequest()
->amount(100000)
->currency('MYR')
->description('Payment for Order #123')
->ewallets('SHOPEEPAY') // Specify channel code
->successUrl('https://yourapp.com/success')
->failureUrl('https://yourapp.com/failed')
->metadata(['order_id' => 123])
->create();
// Redirect user to payment page
return redirect($payment->payment_url);use Laraditz\Xendit\Facades\Xendit;
// Using array parameter
$payment = Xendit::paymentRequest()->create([
'reference_id' => 'ORDER-123',
'amount' => 100000,
'currency' => 'MYR',
'channel_code' => 'SHOPEEPAY',
'channel_properties' => [
'success_return_url' => 'https://yourapp.com/success',
'failure_return_url' => 'https://yourapp.com/failed',
],
'description' => 'Payment for Order #123',
'metadata' => [
'order_id' => 123,
],
]);use Laraditz\Xendit\Facades\Xendit;
// E-wallet (ShopeePay)
$payment = Xendit::paymentRequest()
->amount(50000)
->ewallets('SHOPEEPAY')
->successUrl('https://yourapp.com/success')
->create();
// Virtual Account (BCA)
$payment = Xendit::paymentRequest()
->amount(50000)
->virtualAccounts('BCA')
->create();
// QR Code (QRIS)
$payment = Xendit::paymentRequest()
->amount(75000)
->qrCode('QRIS')
->create();
// Cards
$payment = Xendit::paymentRequest()
->amount(100000)
->card()
->create();
// Or use specific channel code directly
$payment = Xendit::paymentRequest()
->amount(100000)
->channelCode('GRABPAY')
->create();use Laraditz\Xendit\Facades\Xendit;
// Using channel properties for additional configuration
$payment = Xendit::paymentRequest()
->amount(250000)
->currency('MYR')
->channelCode('SHOPEEPAY')
->channelProperties('SHOPEEPAY', [
'success_return_url' => 'https://yourapp.com/success',
'failure_return_url' => 'https://yourapp.com/failed',
])
->metadata([
'order_id' => 123,
])
->create();// Attach payment to Order model
$payment = Xendit::paymentRequest()
->amount(100000)
->currency('MYR')
->ewallets('SHOPEEPAY')
->for($order)
->create();
// Attach payment to User model
$payment = Xendit::paymentRequest()
->amount(50000)
->card()
->for($user)
->create();
// Access payments from your model
$order->payments; // Get all payments for this orderuse Laraditz\Xendit\Facades\Xendit;
// Get payment status
$status = Xendit::payment()->get($paymentId);
// Cancel a payment
Xendit::payment()->cancel($paymentId);
// Capture a payment (for authorized payments)
Xendit::payment()->capture($paymentId, [
'capture_amount' => 100000,
]);use Laraditz\Xendit\Facades\Xendit;
// Create a payment token
$token = Xendit::paymentToken()->create([
'customer_id' => 'customer-123',
'type' => 'CARD',
// ... other token data
]);
// Get token status
$tokenStatus = Xendit::paymentToken()->get($tokenId);
// Deactivate a token
Xendit::paymentToken()->cancel($tokenId);use Laraditz\Xendit\Facades\Xendit;
// Create an individual customer
$customer = Xendit::customer()
->referenceId('user-001')
->givenNames('John')
->surname('Doe')
->email('john@example.com')
->mobileNumber('+60123456789')
->create();
// $customer is a persisted XenditCustomer model
echo $customer->xendit_id; // Xendit's customer ID
// Get a customer by Xendit ID
$data = Xendit::customer()->get('cust_abc123');
// List customers by reference ID
$list = Xendit::customer()->list('user-001');
// Update a customer
$updated = Xendit::customer()->update('cust_abc123', [
'email' => 'new@example.com',
]);use Laraditz\Xendit\Facades\Xendit;
// Create a PAY session (Payment Link mode)
$session = Xendit::session()
->referenceId('order-001')
->amount(100.00)
->currency('MYR')
->country('MY')
->sessionType('PAY')
->mode('PAYMENT_LINK')
->successUrl('https://yourapp.com/success')
->cancelUrl('https://yourapp.com/cancel')
->create();
// $session is a persisted XenditSession model
return redirect($session->payment_link_url);
// Get session details from Xendit API
$data = Xendit::session()->get($session->payment_session_id);
// Cancel a session
Xendit::session()->cancel($session->payment_session_id);use Laraditz\Xendit\Enums\RefundReason;
use Laraditz\Xendit\Facades\Xendit;
// Create a refund — persists a local XenditRefund record immediately
$refund = Xendit::refund()->create([
'payment_request_id' => $paymentRequestId,
'amount' => 50000,
'reason' => RefundReason::RequestedByCustomer->value,
]);
$refund->refund_id; // Xendit's refund ID
$refund->status; // RefundStatus::Pendinguse Laraditz\Xendit\Facades\Xendit;
// Create a payment link
$link = Xendit::paymentLink()->create([
'amount' => 100000,
'description' => 'Payment for Product',
'customer' => [
'email' => 'customer@example.com',
],
]);
// Get payment link
$linkDetails = Xendit::paymentLink()->get($linkId);use Laraditz\Xendit\Facades\Xendit;
// Get transaction by ID
$transaction = Xendit::transaction()->get($transactionId);
// List all transactions
$transactions = Xendit::transaction()->list([
'limit' => 20,
'after_id' => 'txn_123',
]);By default no api-version header is sent. You can configure per-service defaults in config/xendit.php, override them per call, or suppress them entirely:
// config/xendit.php — configure per-service defaults (all optional)
'api_versions' => [
'payment_request' => '2024-11-11',
'session' => '2024-05-01',
// any service key set to null suppresses the header even if the service has a default
// 'payment' => null,
],// Per-call override — takes precedence over config
Xendit::session()
->withApiVersion('2024-05-01')
->amount(100.00)
->sessionType('PAY')
->mode('PAYMENT_LINK')
->create();
// Suppress for this call only
Xendit::session()
->withoutApiVersion()
->get('ps-abc123');Available config keys: payment_request, payment, payment_token, session, customer, refund, payment_link, transaction.
Any builder supports arbitrary request headers via withHeader() / withHeaders():
// Single header
Xendit::session()->withHeader('idempotency-key', 'abc')->create();
// Multiple headers at once
Xendit::session()->withHeaders(['idempotency-key' => 'abc'])->create();
// Works on get() and cancel() too
Xendit::session()->withHeader('for-user-id', 'sub-account-id')->get('ps-abc123');SessionBuilder and PaymentRequestBuilder also expose named shortcuts that set the header and persist the value to the database for filtering and auditing:
// Session — for-user-id header + xendit_sessions.for_user_id column
$session = Xendit::session()
->forUserId('sub-account-user-id') // sets 'for-user-id' header
->withSplitRule('split-rule-id') // sets 'with-split-rule' header
->amount(100.00)
->sessionType('PAY')
->mode('PAYMENT_LINK')
->create();
// Payment Request — same pattern → xendit_payments.for_user_id / split_rule_id
$payment = Xendit::paymentRequest()
->forUserId('sub-account-user-id')
->withSplitRule('split-rule-id')
->amount(100.00)
->ewallets('SHOPEEPAY')
->create();
// Query by sub-account or split rule
XenditSession::forUserId('sub-account-user-id')->get();
XenditPayment::forUserId('sub-account-user-id')->get();
XenditSession::splitRuleId('split-rule-id')->get();
XenditPayment::splitRuleId('split-rule-id')->get();use Laraditz\Xendit\Models\XenditPayment;
use Laraditz\Xendit\Enums\PaymentStatus;
// Find payment by external ID
$payment = XenditPayment::externalId('ORDER-123')->first();
// Find paid payments
$paidPayments = XenditPayment::paid()->get();
// Find pending payments
$pendingPayments = XenditPayment::pending()->get();
// Filter by status
$payments = XenditPayment::status(PaymentStatus::Paid)->get();
// Check payment status
if ($payment->isPaid()) {
// Payment is paid
}Webhooks are automatically handled at /xendit/webhook. The package will:
- Verify webhook signature
- Log webhook to database
- Update payment status
- Dispatch Laravel events
Listen to webhook events in your EventServiceProvider:
use Laraditz\Xendit\Events\PaymentPaid;
use Laraditz\Xendit\Events\PaymentExpired;
use Laraditz\Xendit\Events\PaymentFailed;
use Laraditz\Xendit\Events\PaymentTokenCreated;
use Laraditz\Xendit\Events\PaymentTokenActivated;
use Laraditz\Xendit\Events\RefundCreated;
use Laraditz\Xendit\Events\RefundSucceeded;
use Laraditz\Xendit\Events\RefundFailed;
use Laraditz\Xendit\Events\SessionCreated;
use Laraditz\Xendit\Events\SessionCompleted;
use Laraditz\Xendit\Events\SessionExpired;
use Laraditz\Xendit\Events\SessionCanceled;
protected $listen = [
// Payment events
PaymentPaid::class => [
SendPaymentConfirmationEmail::class,
ProcessOrder::class,
],
PaymentExpired::class => [
CancelOrder::class,
],
PaymentFailed::class => [
NotifyPaymentFailure::class,
],
// Payment token events
PaymentTokenCreated::class => [
LogPaymentTokenCreation::class,
],
PaymentTokenActivated::class => [
EnableSavedPaymentMethod::class,
],
// Refund events
RefundCreated::class => [
LogRefundCreated::class,
],
RefundSucceeded::class => [
ProcessRefund::class,
],
RefundFailed::class => [
NotifyRefundFailure::class,
],
// Session events
SessionCreated::class => [
LogSessionCreated::class,
],
SessionCompleted::class => [
FulfillOrder::class,
],
SessionExpired::class => [
CancelOrder::class,
],
SessionCanceled::class => [
ReleaseReservedStock::class,
],
];Payment Event Listener:
namespace App\Listeners;
use Laraditz\Xendit\Events\PaymentPaid;
class ProcessOrder
{
public function handle(PaymentPaid $event)
{
$payment = $event->payment;
// Access related model
$order = $payment->payable; // Returns Order model
// Process the order
$order->markAsPaid();
$order->process();
}
}Refund Event Listener:
namespace App\Listeners;
use Laraditz\Xendit\Events\RefundSucceeded;
class ProcessRefund
{
public function handle(RefundSucceeded $event)
{
// $event->payload is the full webhook envelope; refund fields live under 'data'
$refundData = $event->payload['data'];
// Process refund
$order = Order::where('payment_id', $refundData['payment_request_id'])->first();
$order->markAsRefunded();
}
}Payment Token Event Listener:
namespace App\Listeners;
use Laraditz\Xendit\Events\PaymentTokenCreated;
class LogPaymentTokenCreation
{
public function handle(PaymentTokenCreated $event)
{
$tokenData = $event->payload;
// Store token reference or log
Log::info('Payment token created', $tokenData);
}
}For detailed documentation on each service, please refer to:
- Payment Request - Complete guide to creating payment requests with fluent builder
- Payment - Managing payment status, cancellation, and capture
- Payment Token - Saving and managing customer payment methods
- Customer - Creating and managing Xendit customers with DB persistence
- Session - Creating secure payment sessions for checkout flows
- Refund - Processing full and partial refunds
- Payment Link - Generating shareable payment links for invoices
- Transaction - Querying and listing all transactions
- Webhooks - Handling webhook events and notifications
- Logging - Outbound API call audit trail with retention/pruning
composer testPlease see CHANGELOG for more information what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security related issues, please email raditzfarhan@gmail.com instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.