jinseokoh/bootpay
A Laravel wrapper for Bootpay, the Korean payment gateway, providing framework-friendly configuration and PHP bindings for integrating Bootpay payment APIs into Laravel applications. Useful for payment request, verification, and receipt handling flows.
Confirm the constraints before touching code. This package's last release (1.2.0, March 2022) claims Laravel 8/9 support only. Check what you actually resolve:
composer show jinseokoh/bootpay
php artisan about | grep -i laravel
If you're on Laravel 10+, do not assume it works — read the composer.json illuminate/* constraints in vendor/jinseokoh/bootpay first. You may need "illuminate/support": "*" to be relaxed, or a fork.
Install and publish.
composer require jinseokoh/bootpay
php artisan vendor:publish --provider="Jinseokoh\Bootpay\BootpayServiceProvider"
Where to look first, in order:
config/bootpay.php — the config surface is the whole integration. Note every key: application id, private key, mode/sandbox flag, and any REST base URL.vendor/jinseokoh/bootpay/src/ — it is a thin wrapper; you can read the entire thing in ten minutes. Do that rather than trusting the README.config/app.php / the provider's register() — that tells you the call style.Configure credentials — two separate pairs.
# .env
BOOTPAY_APPLICATION_ID=67xxxxxxxxxxxx
BOOTPAY_PRIVATE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
BOOTPAY_MODE=development # or production
Bootpay issues different application ids for sandbox and production. The private key is a server secret — it must never appear in a Blade template, a JS bundle, or a client-side config object.
First use case — verify a receipt after checkout.
The typical flow: your frontend calls the Bootpay JS SDK and opens the payment window; the client returns a receipt_id; your Laravel backend then confirms with Bootpay that the payment is real, succeeded, and for the right amount.
use Jinseokoh\Bootpay\Facades\Bootpay; // verify exact namespace in src/
Route::post('/checkout/confirm', function (Request $request) {
$receipt = Bootpay::verify($request->receipt_id); // name varies — read src/
$order = Order::where('merchant_uid', $request->order_id)->firstOrFail();
abort_unless(
$receipt['status'] === 2 && // map the status enum explicitly
(int) $receipt['price'] === $order->amount, // never trust the client amount
422
);
$order->markPaid($receipt['receipt_id']);
});
Smoke test before writing app code:
php artisan tinker
>>> app(Jinseokoh\Bootpay\Bootpay::class); // or resolve the facade root
If that throws a binding error, auto-discovery failed or the namespace in the docs is stale — fix that before anything else.
Wrap the package. Don't call it from controllers.
The single most important pattern given this package's age and support status: hide it behind your own interface so you can swap it for raw Guzzle later.
interface PaymentGateway {
public function verify(string $receiptId): Receipt;
public function cancel(string $receiptId, string $reason, ?int $amount = null): void;
}
final class BootpayGateway implements PaymentGateway { /* thin adapter */ }
Bind it in a service provider. Every future surprise (Laravel 11 incompatibility, Bootpay API v3) becomes a one-class change.
Pattern: server-side confirmation as the source of truth.
Bootpay.requestPayment(...) → resolves with receipt_id.{ receipt_id, order_id } to your API.status, price, order_id/merchant_uid, and item_name against your own records.Do the same check in your return_url/redirect handler and in your webhook handler. Treat the client redirect as untrusted UX, not as proof of payment.
Pattern: idempotent cancellation/refund.
Cancels are the operation that goes wrong in production. Model them as a queued job with an idempotency key equal to your own refund record id:
final class CancelPayment implements ShouldQueue
{
public function handle(PaymentGateway $gateway): void
{
if ($this->refund->isSettled()) return; // idempotent guard
$gateway->cancel($this->refund->receipt_id, $this->refund->reason, $this->refund->amount);
$this->refund->markSettled();
}
}
Retry with backoff; a network timeout does not mean the cancel failed. Re-query the receipt before retrying a full cancel, or you'll double-refund.
Pattern: billing keys / recurring charges.
For subscription products, the client-side flow returns a billing_key, not a receipt. Persist the billing key against the customer, then charge server-side on your schedule. Treat the billing key as a secret of the same sensitivity as a card token — encrypt it at rest and never put it in logs or API responses.
Pattern: event/state-machine integration.
Emit domain events (PaymentConfirmed, PaymentCancelled, RefundSettled) from your adapter, not from the package layer. Keeps order state, email, and accounting listeners decoupled from Bootpay's response shape.
Testing: fake the gateway, and separately test the adapter.
Unit tests should bind a FakePaymentGateway — you never want CI calling Bootpay. For the adapter itself, use Http::fake() and assert on the outbound request body, plus a set of recorded Bootpay JSON fixtures for success/cancel/error responses. Bootpay's sandbox is useful for a manual smoke test but is a poor fit for automated CI.
Amount handling. Store amounts as bigInteger KRW — no decimals, no floats anywhere in the chain. Cast to int when comparing receipt prices; JSON may hand you a string.
The package is only half the integration. Bootpay is a payment window product: you need their JS SDK on the frontend plus this server-side wrapper. Teams routinely install this and then discover there is no way to open a payment dialog from it. Budget frontend work explicitly.
Abandoned-ish dependency — plan for it. Last release 2022, 4 stars, zero dependents. No CI matrix, no support policy, no security process. Mitigations: pin the exact version in composer.lock, wrap it behind your own interface (above), and be ready to vendor or fork the ~few-hundred lines of HTTP code. If you're planning a Laravel 10/11 upgrade, add "replace bootpay package" to that ticket now, not later.
Status codes are magic integers. Bootpay returns numeric receipt statuses and a code where 0 is success. Do not sprinkle === 2 and === 0 through controllers. Define an enum/constants once, with a default branch that throws on unknown codes — Bootpay has added statuses over time and a silent fallthrough on an unmapped status is a payment-integrity bug.
Access-token lifetime and caching. The wrapper typically fetches an access token per call, or expects you to. If you see repeated token requests or occasional auth failures on long-running queues, cache the token yourself with a short TTL and invalidate on 401. Never cache it across a mode switch.
Sandbox vs production drift. Different application ids, different keys, and sometimes different behaviour for cancellation and billing keys. Make the mode an explicit env var validated at boot (fail fast if the production mode is paired with a sandbox-looking key, or vice versa), and log which mode handled each transaction.
Logging and debugging. Log the raw request and response payloads (redacting the private key, billing key, and card fields) around every gateway call. When Bootpay returns a non-zero code, its message is usually Korean and terse — keep a lookup of the codes you encounter in your runbook. Http::fake() + a dd() of the response array is faster than tinker for diagnosing shape mismatches.
Never trust the client amount — again. This is the classic Bootpay integration failure: the frontend picks the price, the attacker edits it, and your backend confirms on receipt_id alone. Always re-compare the receipt's price and merchant_uid against your own order row before fulfilment, even if you "know" your frontend is correct.
Webhook verification. Bootpay's webhook hits your server asynchronously and can be spoofed. The safe pattern is to ignore the webhook body's claims and re-fetch the receipt by id from the API before acting. Also make webhook handling idempotent — retries are normal.
Timezone and currency locale. Korean payment flows often involve cash receipt / tax invoice fields and KST timestamps. Keep your DB in UTC, format only at the edges, and don't pass Carbon objects directly into the request builder — serialize to the string format Bootpay expects.
Extension points are thin, so use Laravel's. The realistic levers are: publish the config, rebind the client class in the container, and swap the facade root. If the package hardcodes its HTTP client, the pragmatic extension is to stop calling the package for that endpoint and use a small Http-based service of your own — mixing is fine, since a shared access token is the only real coupling.
Cancellation has two modes. Full cancel and partial cancel are different request shapes with different resulting receipt states. Test both in the sandbox and record what your DB should hold after each.
How can I help you explore Laravel packages today?