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
- 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\Facades\Xendit;
// Create a refund
$refund = Xendit::refund()->create([
'payment_id' => $paymentId,
'amount' => 50000,
'reason' => 'Customer request',
]);use 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\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 => [
LogRefundRequest::class,
],
RefundSucceeded::class => [
ProcessRefund::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)
{
$refundData = $event->payload;
// Process refund
$order = Order::where('payment_id', $refundData['payment_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
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.