vrajroham/laravel-bitpay
A Laravel wrapper for the BitPay payment gateway that lets your application accept Bitcoin and other cryptocurrencies. Includes a clean facade and configuration for creating invoices, generating payment URLs, and handling crypto transactions within your Laravel app.
vrajroham/laravel-bitpay is a thin Laravel wrapper around BitPay's PHP SDK (REST API). You get a service provider, a facade, a publishable config file, and a handful of convenience methods for invoices, payouts, refunds and currency rates. v6.0.0 is a major bump with no published changelog beyond the tag, so treat it as a breaking release until proven otherwise.
Minimal steps:
composer require vrajroham/laravel-bitpay:^6.0
php artisan vendor:publish --provider="Vrajroham\LaravelBitpay\LaravelBitpayServiceProvider"
Then look at, in this order:
composer.json constraints inside vendor/vrajroham/laravel-bitpay — this is the fastest way to learn what v6 actually changed (PHP version, illuminate/* ranges, bitpay/sdk version).config/bitpay.php — network (testnet vs mainnet), token, private key path/password. Every real problem you hit in the first day is a config problem.src/LaravelBitpay.php (or the facade's accessor) — the actual method list, since the README often lags a major release.v5.3.0...v6.0.0 — with empty release notes, the diff is your changelog.Environment variables to set before your first call (names may differ slightly in v6 — confirm against the published config):
BITPAY_NETWORK=testnet
BITPAY_TOKEN=your-pairing-token
BITPAY_PRIVATE_KEY_PATH=/secure/path/bitpay.pem
BITPAY_PRIVATE_KEY_PASSWORD=
Generate the key pair and token with BitPay's setup tooling, not by hand. Private keys must live outside the repo and outside public/.
First use case: create an invoice, persist it locally, and redirect the customer to BitPay's hosted checkout. Don't start with payouts or refunds — they require a merchant token with elevated permissions and are far harder to test.
Wrap the facade, don't scatter it. The package is a static facade, which makes it awkward to mock and easy to call from controllers. Put it behind one small service:
final class BitPayGateway
{
public function createInvoice(Order $order): Invoice
{
return BitPay::createInvoice([
'price' => $order->total, // string or int, never a float
'currency' => $order->currency,
'orderId' => (string) $order->id,
'notificationUrl' => route('webhooks.bitpay'),
'redirectUrl' => route('orders.thanks', $order),
'posData' => json_encode(['order_id' => $order->id]),
]);
}
}
Persistence pattern. Store a local bitpay_invoices row keyed by the BitPay invoice id, with status, orderId, and the raw JSON payload. Every webhook then becomes an upsert against a known record rather than a trust exercise. Keep the raw payload — when status transitions look wrong, the payload is the only source of truth you'll have.
Webhook handling. Register the route outside web middleware or add it to ValidateCsrfToken::$except:
Route::post('/webhooks/bitpay', BitPayWebhookController::class)
->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
BitPay posts a data blob with an embedded signature. Pass it through the SDK's signature verification before touching the database, respond 2xx fast, and do the real work in a queued job. BitPay retries on non-2xx — so the handler must be idempotent. Use updateOrCreate on the invoice id and guard status transitions (a paid event arriving twice should not double-credit a wallet).
Never trust the redirect alone. Users can close the tab; the redirectUrl is a UX nicety. Fulfilment must key off the webhook (or a reconciliation poll on getInvoice($id) for anything older than a few minutes).
Pricing with currency rates. For crypto-denominated pricing, use the package's rate call and cache it. Rates move; a per-request call will both slow checkout and burn your API quota. Cache::remember('bitpay:rates:'.$currency, 60, ...) is usually the right granularity.
Testing. Bind a fake implementation of your BitPayGateway interface in tests and assert on the payload you would have sent. For the package itself, BitPay::shouldReceive(...) works if the facade has been registered, but a hand-rolled fake is less brittle across a major version.
Money types. Invoice amounts and crypto amounts come back as strings and are frequently in satoshi-scale integers. Cast to string or int, never (float). Do conversion math with bcmath and explicit precision — this is the single most common source of cents-off bugs.
v5.3.0...v6.0.0 yourself and check: facade class/namespace, config key names, whether the underlying BitPay SDK major changed, and the minimum PHP/Laravel version. A major tag on a wrapper package almost always means "SDK major bump + config renames"..env or config/bitpay.php, run php artisan config:clear. A stale cached token against testnet looks identical to a bad key.config('bitpay.network') in a boot-time check or a health command.chmod 600, and never commit it. In containerised setups, a relative path in config will resolve differently than locally — prefer an absolute path from env.notificationUrl silently degrades you to polling-only for that invoice. Validate the route exists (php artisan route:list | grep bitpay) before shipping.paid event to your endpoint. Verify, then act. Log verification failures at warning level with the invoice id.new → paid → confirmed → complete, plus terminal expired/invalid/exception) and ignore regressions rather than throwing.Http::retry() semantics or the SDK's built-in retry, but cap backoff — checkout latency is user-visible. Wrap invoice creation in a short timeout and show a real error rather than a spinner.^6.0 only after your own smoke test passes (create invoice → pay in testnet → receive webhook → verify status), and record in your PR why you bumped.How can I help you explore Laravel packages today?