glennraya/xendivel
Xendivel is a Laravel wrapper for Xendit that simplifies accepting payments and sending payouts. It offers a fluent API for invoices, virtual accounts, e-wallets, retail outlets, QR codes, disbursements, and webhook handling, with straightforward configuration and Laravel-friendly integration.
Xendivel wraps Xendit's payment APIs for Laravel. As of v3.2.0, QR Code payments are a first-class payment type alongside card, e-wallet, and OTC — built on Xendit's Payments API (POST /payment_requests), which is enabled by default on your account (no channel activation needed).
Minimal setup after upgrading:
Republish config and assets to pull in the new QR keys, event/listener stubs, and updated checkout templates:
php artisan vendor:publish --tag=xendivel --force
Review the diff first — --force overwrites any local edits to config or templates.
Register the webhook listener in app/Providers/EventServiceProvider.php:
QrPaymentEvents::class => [QrPaymentWebhookListener::class],
Add the dashboard webhook in Xendit under the Payment callback type (payment.succeeded), pointing at qr_webhook_url (default /xendit/qr/webhook). The default /xendit/* CSRF exclusion already covers this path.
First use case — create a dynamic QR for a fixed amount and render it:
$request = Xendivel::createQrCode([
'amount' => 150.00,
'type' => 'DYNAMIC', // ONE_TIME_USE
]);
$qrString = $request['payment_method']['qr_code']['channel_properties']['qr_string'];
From there, getQrCode($id) polls status (PENDING → SUCCEEDED), and QrPaymentWebhookListener handles asynchronous confirmation.
Look first at the split test files — XendivelQrCodePaymentsTest is the fastest reference for exact request shapes, overrides, and validation rules.
Dynamic vs static QR. DYNAMIC maps to ONE_TIME_USE (fixed amount baked in); STATIC maps to MULTIPLE_USE (payer enters the amount). Choose based on whether the amount is known at creation time — dynamic for checkout totals, static for reusable point-of-sale / donation-style codes.
Webhook-first, poll as fallback. Treat QrPaymentWebhookListener as the source of truth for payment confirmation (data.status === 'SUCCEEDED') and use getQrCode($id) only for explicit "check status now" UI actions or reconciliation jobs. Polling every few seconds in a request cycle is a good way to get rate-limited.
Per-request overrides. qr_channel_code (default QRPH) and qr_currency (default PHP) are config defaults but are overridable per request — useful in multi-region apps or when testing against non-default channels:
Xendivel::createQrCode([
'amount' => 99.00,
'type' => 'DYNAMIC',
'currency' => 'IDR',
'channel' => 'QRIS',
]);
Test-mode simulation. simulateQrPayment($id, $amount) flips a QR to paid without a real bank app. It accepts either the QR reference or a pr-… payment request id (auto-resolved) — so in tests you can chain createQrCode() straight into simulateQrPayment() using whichever identifier you already have.
Checkout integration. The bundled Blade/JSX/TSX templates now have three isolated tabs (card / e-wallet / QR) that no longer leak fields across tabs. If you've built a custom checkout UI, mirror that separation — render QR generation lazily when the tab is opened, not on page load, so you don't create abandoned payment requests for users who never pick QR.
Listener wiring. The publishable listener stub gives you the happy path; extend it rather than replacing it, and dispatch your own domain event (e.g. OrderPaid) from inside it so business logic stays decoupled from the Xendit payload shape.
qr_string path is deep. It lives at payment_method.qr_code.channel_properties.qr_string. Use Arr::get() / null-safe access instead of chained array indexing — a static QR or a failed create can leave intermediate keys missing and throw an undefined-index error.DYNAMIC = one-time-use / fixed amount; STATIC = multi-use / payer-entered amount. Get this backwards and your static code silently requires an amount it shouldn't.simulateQrPayment is test-mode only. Guard it behind app()->isLocal() / a config check — never leave a route that can flip real payments to SUCCEEDED reachable in production.QrPaymentEvents is only dispatched if you add the mapping to EventServiceProvider. Silent no-op is the failure mode — check the registration first when debugging "webhook received but nothing happened."payment.succeeded), not a legacy QR-specific callback type — the listener expects the unified Payments payload (data.status).--force republish overwrites local edits. If you've customised the config or checkout templates, diff before forcing. The React templates in particular lose your publishable key on republish — re-add it, then npm run build.status is PENDING → SUCCEEDED, but expired/failed states exist. Don't treat "not yet SUCCEEDED" as success in a polling loop — bound the loop and surface expiry.createQrCode on timeout, you may create duplicate payment requests. Generate your own order reference and reconcile against getQrCode before retrying.qr_channel_code and qr_currency default to QRPH / PHP — non-PHP deployments must set these or override per request, otherwise you'll create valid-looking QR codes for the wrong market.How can I help you explore Laravel packages today?