Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Polar Laravel Package

danestves/laravel-polar

danestves/laravel-polar brings Polar's merchant-of-record billing into Laravel, offering a clean API client, checkout helpers, webhook handling, and subscription/customer management so you can sell digital products and recurring plans without building payment infrastructure.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and pull in its config in one step:

composer require danestves/laravel-polar
php artisan polar:install   # or: vendor:publish --tag="polar-config"

Start by reading two things, in this order:

  1. config/polar.php — this tells you what the package actually expects: access token, webhook secret, sandbox/environment toggle, and the base URI. Everything else in the package is a thin resource layer over Polar's REST API, so the config is the contract.
  2. The facade / root class (typically Polar::) — it exposes resource accessors (products(), checkouts(), subscriptions(), customers(), orders(), benefits(), licenseKeys(), discounts(), …). Once you've seen the accessor list you've seen 90% of the surface area.

Minimum viable .env:

POLAR_ACCESS_TOKEN=polar_oat_...
POLAR_WEBHOOK_SECRET=whsec_...
POLAR_SANDBOX=true

First use case: a checkout redirect. This is the fastest end-to-end loop and exercises auth, HTTP, and error handling at once. Create a checkout session for a product ID from your Polar dashboard, then redirect the user to the returned URL:

Route::get('/subscribe/{product}', function (string $product) {
    $checkout = Polar::checkouts()->create([
        'products' => [$product],
        'success_url' => route('billing.success'),
        'customer_email' => auth()->user()->email,
        'external_customer_id' => (string) auth()->id(),
    ]);

    return redirect($checkout->url);
});

Use the sandbox token first. The sandbox and production orgs are entirely separate — products, customers, and webhooks do not carry over, which trips up almost everyone on day one.

Implementation Patterns

Checkout → webhook → fulfillment. Never provision entitlements from the success_url redirect. The redirect is a UX nicety the user can close, spoof, or never reach. The durable pattern is: create checkout with external_customer_id set to your user's ID, then let order.paid / subscription.active webhooks do the writing. Store polar_customer_id and polar_subscription_id on your users (or a subscriptions table) when the webhook lands.

Webhook handler as an event router. Register a single route and map Polar event types to Laravel events/jobs:

Route::post('/webhooks/polar', PolarWebhookController::class)
    ->withoutMiddleware([VerifyCsrfToken::class]);

Then in the controller, verify, persist the raw event (id + type + payload) for idempotency, and dispatch. Keep the controller thin — Polar retries, and a slow handler means duplicate work.

Filtering and pagination for list endpoints. The package's filter API accepts arrays that serialize into Polar's query syntax, and it supports nesting (e.g. an $and/$or composite containing further clauses). This is the idiomatic way to scope list calls rather than fetching everything and filtering in PHP:

$orders = Polar::orders()->filter([
    'customer_id' => $customerId,
    'created_at' => ['gte' => now()->subMonth()->toIso8601String()],
])->all();

Pin to >= 3.2.1 if you use nested clauses — earlier versions silently dropped inner clauses, which produced wrong results rather than errors. That's the worst failure mode for billing code: plausible-looking output.

Wrap it in your own service. Don't let Polar:: calls scatter across controllers. Put a BillingService (or an action per operation) between your app and the package. Two payoffs: you can cache product/price lookups, and you can swap in Http::fake() in tests without stubbing the package internals.

Sync jobs, not inline calls. Product catalogue syncs, order reconciliation, and customer updates should be queued jobs with retries and backoff. Polar is an external HTTP dependency; treat it like one.

Testing. Because the package sits on Laravel's HTTP client, Http::fake() at the Polar base URI generally works well for feature tests, and recording a handful of real sandbox responses gives you high-fidelity fixtures. Assert on your own domain state (subscription row created, feature flag flipped), not on the package's response DTOs — those are more likely to churn between releases.

Gotchas and Tips

Sandbox and production are different worlds. Separate tokens, separate webhook endpoints, separate product IDs. Make the environment explicit in config and assert on it in a boot check for staging/production. A sandbox token in production fails in confusing ways rather than loudly.

Webhook signature verification is mandatory, and the raw body matters. If you read the body through a middleware that parses JSON first, signature validation can fail. Take the raw payload ($request->getContent()) and validate against the header Polar sends. Also handle the "event already processed" case — Polar retries on non-2xx, so return 200 quickly and do the work in a job.

Nested filter hydration (fixed in 3.2.1). If you're on 3.2.0 or below, nested filter clauses were not hydrated, meaning filters returned broader result sets than requested. This is a silent correctness bug — upgrade and add a test that asserts a nested filter actually narrows the result.

List responses are paginated. ->all() on a large collection may be a single page or may paginate depending on the resource method used. Read meta/links on the response and prefer explicit pagination loops for reconciliation jobs. Rate limits are real; a naive "fetch every order" sync will hit them.

IDs are opaque strings, not integers. Don't cast Polar IDs to int, don't assume ordering, and don't use them as primary keys — use your own model plus a unique index on the Polar ID.

Money is in minor units. Amounts come back as integers in the currency's smallest unit. Format at the presentation layer only; keep integers in the database.

Timestamps are ISO-8601 UTC. Laravel's Carbon handles it fine, but be consistent about casting in your model (a datetime cast on the column) so comparisons and filters don't drift across timezones.

Extension points. The package's HTTP layer is the place to hook if you need to: custom base URI for a proxy, extra headers, or retry middleware. If a resource method is missing, call the underlying client directly rather than forking — Polar's API will keep moving and you want the package's auth/config handling.

Debugging tip. Log the outbound request path + status and the inbound webhook event id + type at info level. When a subscription "doesn't activate," the answer is almost always one of: webhook not reaching you (tunnel/URL config), signature failing, or event processed twice because your handler returned 500 after doing the write. Those three log lines disambiguate it in seconds.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names