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

Recurly Laravel Package

moovly/recurly

A Laravel/PHP package for integrating Recurly subscription billing into your application. It provides convenient access to Recurly API functionality for managing accounts, subscriptions, plans, invoices, and related billing data within your Laravel projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Before writing a line of application code, open vendor/moovly/recurly/composer.json and diff the package against recurly/recurly-client-php. This is a repackaged/forked Recurly SDK, not a first-party Laravel abstraction, and the description oversells the framework integration. Confirm the PHP constraint, the Guzzle major, and the Recurly API version the client sends in its Accept header. That ten-minute audit tells you whether you are adopting a maintained dependency or inheriting source code.

Installation is trivial:

composer require moovly/recurly

Expect no service provider, no config file to publish, no facade. You write the Laravel glue. The minimum viable setup is a config file and a container binding:

// config/recurly.php
return [
    'api_key'    => env('RECURLY_API_KEY'),
    'subdomain'  => env('RECURLY_SUBDOMAIN'),
    'environment'=> env('RECURLY_ENV', 'sandbox'), // or 'production'
    'timeout'    => 10,
];
// app/Providers/RecurlyServiceProvider.php
public function register(): void
{
    $this->mergeConfigFrom(__DIR__.'/../../config/recurly.php', 'recurly');

    $this->app->singleton(RecurlyClientInterface::class, function ($app) {
        $client = new \Recurly\Client(
            $app['config']->get('recurly.api_key'),
            // region/timeouts as the fork's constructor requires
        );
        return new RecurlyClientAdapter($client);
    });
}

Where to look first: the package's src/ for a single Client class wrapping Recurly's REST endpoints, and the Recurly API docs for object shapes (Account, Plan, Subscription, Invoice, Transaction). The package is unlikely to add domain logic, so the mental model is "thin HTTP client."

First use case: list plans from Recurly to render a pricing page, then create an account with a subscription:

$plans = $billing->plans(); // returns array of plan objects

$account = $billing->createAccount([
    'code'       => 'acct_'.$user->id,
    'email'      => $user->email,
    'first_name' => $user->first_name,
    'last_name'  => $user->last_name,
], idempotencyKey: 'acct_'.$user->id);

$subscription = $billing->createSubscription([
    'plan_code'     => $request->plan_code,
    'account_code'  => $account->getCode(),
    'currency'      => 'USD',
], idempotencyKey: (string) Str::uuid());

If you need card capture, do not build it here — redirect to Recurly Hosted Pages and let the subscription attach to the account afterwards.

Implementation Patterns

Wrap the SDK behind your own interface. This is the single most important pattern; it makes the supplier decision reversible and keeps the rest of the app ignorant that this package exists. Only one class in the codebase should import package namespaces:

interface BillingService
{
    public function plans(): array;
    public function account(string $code): AccountDto;
    public function createSubscription(CreateSubscriptionInput $in): SubscriptionDto;
    public function invoices(string $accountCode): array;
}

RecurlyBillingService implements it; controllers, jobs, and Livewire/Inertia components depend on BillingService only. When (not if) you swap to the official SDK, the change is one file.

Webhooks: raw body in, signature check, queue, dedupe. Exclude the route from CSRF and read the raw body before Laravel touches it:

// routes/web.php
Route::post('/webhooks/recurly', [RecurlyWebhookController::class, 'handle'])
    ->withoutMiddleware(VerifyCsrfToken::class);

// app/Http/Controllers/RecurlyWebhookController.php
public function handle(Request $request)
{
    $payload = $request->getContent();                 // raw body
    $signature = $request->header('Recurly-Signature');

    if (! $this->verifier->verify($payload, $signature)) {
        abort(401);
    }

    ProcessRecurlyWebhook::dispatch($payload, $signature)
        ->onQueue('billing');

    return response('', 200);
}

In ProcessRecurlyWebhook, dedupe on the Recurly event UUID (persist processed IDs in a webhook_events table) and dispatch a domain event only once. Never do heavy work in the HTTP handler — Recurly will time out and retry, giving you duplicate deliveries.

Cache stable reference data. listPlans is called from your pricing page; cache it in Redis with a reasonable TTL and bust on webhook events for plan changes. Avoid hitting Recurly on every page view.

Idempotency on writes. Recurly accepts an idempotency header per request; wrap mutating adapter methods so retries of createSubscription don't double-charge. Generate the key once at the caller and pass it through.

Handle 429s with backoff. Put retry policy in the adapter, not in callers — exponential backoff with jitter, capped retries. A queued job with $tries = 5 and backoff() is the right home for retryable Recurly errors.

Testing pattern: unit tests use a FakeBillingService (the BillingService interface makes this trivial); integration tests exercise the adapter against recorded Recurly fixtures (VCR-style). That contract suite is what catches silent divergence from upstream.

Hosted Pages flow: for self-serve upgrades and payment method updates, generate a Recurly hosted page URL and redirect. No server-side SDK call needed for the card capture itself — the SDK is only for the account/subscription objects behind it.

Gotchas and Tips

  • It's a fork; pin it exactly. Use a constraint like "moovly/recurly": "1.2.3" — never ^. Also commit a lock file and verify the Composer integrity hash. No floating constraints on code in the payment path.
  • No vendor:publish. There is no config stub to publish; write config/recurly.php yourself and use mergeConfigFrom in your provider. Don't expect php artisan recurly:... commands — none exist.
  • No auto-discovery. If the package declares a service provider, check it's discoverable and doesn't register conflicting bindings. If not (more likely), register yours manually.
  • API version header is a footgun. The SDK sends a version like application/vnd.recurly.v2021-02-25. If the fork pins a stale version, Recurly's deprecation schedule will eventually break you with a request rejection or silent behavior change. Write the pinned version down and watch Recurly's changelog.
  • Webhook signature verification must be explicit. Read the vendor implementation and confirm it matches Recurly's documented HMAC scheme; a fork that "simplified" verification is a release blocker. Write a test that a forged signature returns 401.
  • Dedupe webhooks. Recurly retries on non-2xx. Store event IDs and short-circuit replays. Idempotent handlers, always.
  • Money is integer minor units. Never use floats for amounts. Currency codes come from Recurly; don't assume USD.
  • Don't log request bodies at debug level. The client carries your API key in headers; a stray Log::debug($request) leaks it. Bind a sanitized logger or set Guzzle debug => false in all environments.
  • Set timeouts explicitly. Wrap the client with connect_timeout and timeout (e.g., 5s/10s). Defaults are often too long for a synchronous controller path.
  • No Cashier equivalent. There is no Billable trait, no ->subscribed() helpers. You build those yourself against BillingService. Budget the effort up front — the "wrapper" the description promises is not there.
  • Exception translation lives in the adapter. Convert Recurly's NotFound, ValidationError, and RateLimitExceeded into domain exceptions once, so business code never imports the SDK's exception classes.
  • Debugging path: when a call misbehaves, read the SDK source — there is no docs site. Compare against the official recurly/recurly-client-php release notes to spot fork drift.
  • Bus factor and ownership. Fork-and-own: name an internal owner, set a review date, and vendor the source if you can't patch it in a timely manner. If a PHP or Guzzle bump blocks a platform upgrade, the one-file adapter swap is your escape hatch — protect it by never letting SDK classes leak out.
  • Sandbox first, always. All flows (subscriptions, dunning, invoice retrieval) should be exercised in Recurly's sandbox before the production key exists; the fork gives you no safety net here.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core