Skip to content

laraditz/xendit

Repository files navigation

Laravel Xendit

Latest Version on Packagist Total Downloads GitHub Actions

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.

Features

  • 🔥 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.)

API Coverage

Requirements

  • PHP 8.2+
  • Laravel 10.x, 11.x, or 12.x

Installation

Install the package via composer:

composer require laraditz/xendit

Publish the configuration and migrations:

php artisan vendor:publish --tag=xendit-config
php artisan vendor:publish --tag=xendit-migrations

Run the migrations:

php artisan migrate

Run the seeder:

php artisan db:seed --class=Laraditz\\Xendit\\Database\\Seeders\\DatabaseSeeder

Add your Xendit credentials to .env:

XENDIT_API_KEY=your-secret-api-key
XENDIT_WEBHOOK_SECRET=your-webhook-verification-token
XENDIT_CURRENCY=MYR

Usage

Creating Payment Request (Fluent API)

use 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);

Payment Request with Array

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,
    ],
]);

Payment Request with Specific Channels

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();

Payment Request with Channel Properties

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();

Attaching Payments to Models (Polymorphic)

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

Managing Payments

use 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,
]);

Working with Payment Tokens (Saved Payment Methods)

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

Creating Customers

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',
]);

Creating Sessions

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

Processing Refunds

use Laraditz\Xendit\Facades\Xendit;

// Create a refund
$refund = Xendit::refund()->create([
    'payment_id' => $paymentId,
    'amount' => 50000,
    'reason' => 'Customer request',
]);

Creating Payment Links

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

Querying Transactions

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',
]);

Per-Service API Versions

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.

Custom HTTP Headers (Sub-accounts & Split Rules)

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();

Querying Payments

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
}

Webhook Handling

Webhooks are automatically handled at /xendit/webhook. The package will:

  1. Verify webhook signature
  2. Log webhook to database
  3. Update payment status
  4. Dispatch Laravel events

Available Webhook 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,
    ],
];

Example Listeners

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);
    }
}

Documentation

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

Testing

composer test

Changelog

Please see CHANGELOG for more information what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email raditzfarhan@gmail.com instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.

About

Laravel package for interacting with Xendit API.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages