romansh / laravel-creem
A Laravel package for Creem.io payment provider with support for products, checkouts, subscriptions, transactions, licenses, and discount codes
Requires
- php: ^8.1|^8.2|^8.3|^8.4
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpcompatibility/php-compatibility: ^9.3
- phpunit/phpunit: ^10.0|^11.0
- squizlabs/php_codesniffer: ^3.13
README
A Laravel package for Creem.io payment provider. Built with Laravel-native patterns, clean architecture, and developer experience as top priorities.
Features
- Laravel-Native: Built on
Illuminate\Http\Clientwith automatic retries and timeouts - Multi-Profile Configuration: Support multiple API keys and environments
- Complete API Coverage: Products, Checkouts, Customers, Subscriptions, Transactions, Licenses, and Discounts
- Webhooks: Built-in signature verification and event dispatching
- Type-Safe: Full PHPDoc annotations and Laravel IDE helper compatible
- Well-Tested: Comprehensive unit and feature tests
- Event-Driven: Laravel events for all webhook types
- Artisan Commands: Test webhooks locally with ease
- PSR-12 Compliant: Clean, readable, maintainable code
Requirements
- PHP 8.1 or higher
- Laravel 10.x, 11.x, 12.x
Installation
Install via Composer:
composer require romansh/laravel-creem
Publish the configuration file:
php artisan vendor:publish --tag=creem-config
Add your Creem credentials to .env:
CREEM_API_KEY=your_api_key_here CREEM_TEST_MODE=false CREEM_WEBHOOK_SECRET=your_webhook_secret_here
Demo Application
A full-featured demo app is available as a separate package: romansh/laravel-creem-demo
composer create-project romansh/laravel-creem-demo my-creem-app
cd my-creem-app
cp .env.example .env
docker-compose up -d
Open http://localhost/creem-demo — configure API keys, webhook secret, and webhook URL directly in the browser. No .env editing required.
The demo covers products, subscriptions, checkouts, discounts, transactions, and webhook event handling with live logs. Docker setup includes optional Cloudflare Tunnel for webhook testing.
Configuration
The package supports multiple configuration profiles. Open config/creem.php to configure:
return [ 'profiles' => [ 'default' => [ 'api_key' => env('CREEM_API_KEY'), 'test_mode' => env('CREEM_TEST_MODE', false), 'webhook_secret' => env('CREEM_WEBHOOK_SECRET'), ], // Add more profiles as needed 'product_a' => [ 'api_key' => env('CREEM_PRODUCT_A_KEY'), 'test_mode' => true, 'webhook_secret' => env('CREEM_PRODUCT_A_WEBHOOK_SECRET'), ], ], ];
Usage
Basic Usage (Default Profile)
use Romansh\LaravelCreem\Facades\Creem; // List products $products = Creem::products()->list(); // Find a product $product = Creem::products()->find('prod_123'); // Create a checkout $checkout = Creem::checkouts()->create([ 'product_id' => 'prod_123', 'success_url' => 'https://example.com/success', 'customer' => [ 'email' => 'user@example.com', ], ]); // Redirect customer to checkout return redirect($checkout['checkout_url']);
Using Named Profiles
use Romansh\LaravelCreem\Facades\Creem; // Use the 'product_a' profile $checkout = Creem::profile('product_a') ->checkouts() ->create([ 'product_id' => 'prod_123', 'success_url' => 'https://example.com/success', ]);
Using Inline Configuration
use Romansh\LaravelCreem\Facades\Creem; // Use inline configuration (does not affect global state) $checkout = Creem::withConfig([ 'api_key' => 'custom_api_key', 'test_mode' => true, ])->checkouts()->create([ 'product_id' => 'prod_123', 'success_url' => 'https://example.com/success', ]);
Services
Products
use Romansh\LaravelCreem\Facades\Creem; // Create a product $product = Creem::products()->create([ 'name' => 'Premium Plan', 'description' => 'Monthly subscription', 'price' => 2999, // In cents 'currency' => 'USD', 'billing_type' => 'recurring', 'billing_period' => 'every-month', ]); // Find a product $product = Creem::products()->find('prod_123'); // List products (paginated) $products = Creem::products()->list($page = 1, $pageSize = 20);
Checkouts
use Romansh\LaravelCreem\Facades\Creem; // Create a checkout session $checkout = Creem::checkouts()->create([ 'product_id' => 'prod_123', 'success_url' => 'https://example.com/success', 'customer' => [ 'email' => 'user@example.com', 'name' => 'John Doe', ], 'metadata' => [ 'user_id' => auth()->id(), 'source' => 'web', ], ]); // Find a checkout session $checkout = Creem::checkouts()->find('chk_123'); // Redirect to checkout URL return redirect($checkout['checkout_url']);
Customers
use Romansh\LaravelCreem\Facades\Creem; // Find customer by ID $customer = Creem::customers()->find('cust_123'); // Find customer by email $customer = Creem::customers()->findByEmail('user@example.com'); // List customers (paginated) $customers = Creem::customers()->list($page = 1, $pageSize = 20); // Generate customer portal link $portalLink = Creem::customers()->createPortalLink('cust_123'); return redirect($portalLink);
Subscriptions
use Romansh\LaravelCreem\Facades\Creem; // Find a subscription $subscription = Creem::subscriptions()->find('sub_123'); // List subscriptions $subscriptions = Creem::subscriptions()->list($page = 1, $limit = 20); // Cancel a subscription $subscription = Creem::subscriptions()->cancel('sub_123'); // Pause a subscription $subscription = Creem::subscriptions()->pause('sub_123'); // Resume a paused subscription $subscription = Creem::subscriptions()->resume('sub_123'); // Upgrade/change subscription to a different product $subscription = Creem::subscriptions()->upgrade( subscriptionId: 'sub_123', productId: 'prod_456', updateBehavior: 'proration-charge-immediately' ); // Update subscription data $subscription = Creem::subscriptions()->update('sub_123', [ 'metadata' => ['updated' => true], ]);
Transactions
use Romansh\LaravelCreem\Facades\Creem; // Find a transaction by ID $transaction = Creem::transactions()->find('txn_123'); // List all transactions (paginated) $transactions = Creem::transactions()->list([], $page = 1, $pageSize = 20); // List transactions with filters $transactions = Creem::transactions()->list([ 'customer_id' => 'cust_123', 'product_id' => 'prod_456', ], $page = 1, $pageSize = 20); // Get transactions for a specific customer $transactions = Creem::transactions()->byCustomer('cust_123'); // Get transactions for a specific order $transactions = Creem::transactions()->byOrder('ord_456'); // Get transactions for a specific product $transactions = Creem::transactions()->byProduct('prod_789');
Licenses
use Romansh\LaravelCreem\Facades\Creem; // Validate a license key $license = Creem::licenses()->validate( key: 'ABC123-XYZ456-XYZ456-XYZ456', instanceId: 'inst_123' ); if ($license['status'] === 'active') { // Grant access to premium features } // Activate a license on a new device $license = Creem::licenses()->activate( key: 'ABC123-XYZ456-XYZ456-XYZ456', instanceName: 'johns-macbook-pro' ); $instanceId = $license['instance']['id']; // Deactivate a license instance $license = Creem::licenses()->deactivate( key: 'ABC123-XYZ456-XYZ456-XYZ456', instanceId: 'inst_123' );
Discount Codes
use Romansh\LaravelCreem\Facades\Creem; // Create a percentage discount $discount = Creem::discounts()->create([ 'name' => 'Summer Sale 2024', 'code' => 'SUMMER50', 'type' => 'percentage', 'percentage' => 50, 'duration' => 'once', 'max_redemptions' => 100, 'expiry_date' => '2024-12-31T23:59:59Z', ]); // Create a fixed amount discount $discount = Creem::discounts()->create([ 'name' => 'Welcome Bonus', 'code' => 'WELCOME20', 'type' => 'fixed', 'amount' => 2000, // $20.00 in cents 'currency' => 'USD', 'duration' => 'once', ]); // Find discount by ID $discount = Creem::discounts()->find('disc_123'); // Find discount by code $discount = Creem::discounts()->findByCode('SUMMER50'); // Delete a discount code $result = Creem::discounts()->delete('disc_123');
Webhooks
Automatic Setup
Webhook routes are automatically registered. The default endpoint is:
POST /creem/webhook
Configure the webhook URL in your Creem dashboard:
https://yourdomain.com/creem/webhook
Webhook Events
The package dispatches Laravel events for all Creem webhook types. Each webhook is mapped
to a corresponding event class under Romansh\LaravelCreem\Events. All Creem webhook
events extend CreemEvent and expose the following typed properties:
$eventId— unique Creem event id$eventType— the original event string (e.g.checkout.completed)$createdAt— unix timestamp in milliseconds$object— the decoded Creem resource object$payload— the full raw webhook payload
Common webhook event classes provided by the package include:
CheckoutCompletedDisputeCreatedRefundCreatedPaymentFailedSubscriptionCreatedSubscriptionActiveSubscriptionPaidSubscriptionCanceledSubscriptionExpiredSubscriptionPastDueSubscriptionPausedSubscriptionTrialingSubscriptionScheduledCancelSubscriptionUpdate
Additionally the package emits two application-level events to simplify access provisioning logic:
-
GrantAccess— dispatched automatically aftercheckout.completedandsubscription.paid. It receives(array $customer, array $metadata, array $payload)where$customeris the Creem customer object and$metadatais merchant-defined metadata from the originating resource. -
RevokeAccess— dispatched automatically aftersubscription.canceledandsubscription.expired. It also receives(array $customer, array $metadata, array $payload).
Listening to Events
Register event listeners in app/Providers/EventServiceProvider.php:
protected $listen = [ \Romansh\LaravelCreem\Events\CheckoutCompleted::class => [ \App\Listeners\SendPurchaseConfirmation::class, \App\Listeners\ProvisionUserAccess::class, ], \Romansh\LaravelCreem\Events\SubscriptionCanceled::class => [ \App\Listeners\RevokeUserAccess::class, ], // Listen for application-level access events as well \Romansh\LaravelCreem\Events\GrantAccess::class => [ \App\Listeners\ProvisionUserAccess::class, ], \Romansh\LaravelCreem\Events\RevokeAccess::class => [ \App\Listeners\RevokeUserAccess::class, ], ];
Create a listener example:
namespace App\Listeners; use Romansh\LaravelCreem\Events\GrantAccess; class ProvisionUserAccess { public function handle(GrantAccess $event) { $customer = $event->customer; $metadata = $event->metadata; // Use metadata (e.g. referenceId) to find internal user and provision access // $userId = $metadata['referenceId'] ?? null; } }
Custom Webhook Handling
You can also create a custom webhook controller and apply the VerifyCreemWebhook middleware:
namespace App\Http\Controllers; use Romansh\LaravelCreem\Http\Middleware\VerifyCreemWebhook; use Illuminate\Http\Request; class CustomWebhookController extends Controller { public function __construct() { $this->middleware(VerifyCreemWebhook::class); } public function handle(Request $request) { $event = $request->input('event'); $data = $request->input('data'); // Handle webhook... return response()->json(['message' => 'Processed']); } }
Testing Webhooks Locally
Use the built-in Artisan command to test webhooks:
# Send a test checkout.completed event php artisan creem:test-webhook checkout.completed # Test with a specific profile php artisan creem:test-webhook subscription.created --profile=product_a # Available event types php artisan creem:test-webhook checkout.completed php artisan creem:test-webhook subscription.created php artisan creem:test-webhook subscription.canceled php artisan creem:test-webhook payment.failed
Error Handling
The package throws specific exceptions for different error scenarios:
use Romansh\LaravelCreem\Exceptions\ApiException; use Romansh\LaravelCreem\Exceptions\ConfigurationException; try { $checkout = Creem::checkouts()->create([...]); } catch (ApiException $e) { // API error (400, 403, 404, etc.) $statusCode = $e->getStatusCode(); $messages = $e->getMessages(); $traceId = $e->getTraceId(); // Include in support requests return back()->withErrors($messages); } catch (ConfigurationException $e) { // Configuration error (missing profile, invalid API key, etc.) logger()->error($e->getMessage()); }
Testing
The package includes comprehensive tests:
# Run all tests composer test # Run with coverage composer test -- --coverage
Using HTTP Fakes in Tests
use Illuminate\Support\Facades\Http; public function test_can_create_checkout() { Http::fake([ 'test-api.creem.io/v1/checkouts' => Http::response([ 'id' => 'checkout_123', 'checkout_url' => 'https://checkout.creem.io/123', ], 200), ]); $checkout = Creem::checkouts()->create([ 'product_id' => 'prod_123', 'success_url' => 'https://example.com/success', ]); $this->assertEquals('checkout_123', $checkout['id']); }
Example Controllers
Checkout Controller
namespace App\Http\Controllers; use Romansh\LaravelCreem\Facades\Creem; use Illuminate\Http\Request; class CheckoutController extends Controller { public function store(Request $request) { $validated = $request->validate([ 'product_id' => 'required|string', 'email' => 'required|email', ]); $checkout = Creem::checkouts()->create([ 'product_id' => $validated['product_id'], 'customer' => [ 'email' => $validated['email'], ], 'success_url' => route('checkout.success'), 'metadata' => [ 'user_id' => auth()->id(), ], ]); return redirect($checkout['checkout_url']); } public function success() { return view('checkout.success'); } }
Subscription Management Controller
namespace App\Http\Controllers; use Romansh\LaravelCreem\Facades\Creem; use Illuminate\Http\Request; class SubscriptionController extends Controller { public function cancel(Request $request, string $subscriptionId) { $subscription = Creem::subscriptions()->cancel($subscriptionId); return back()->with('success', 'Subscription canceled successfully'); } public function upgrade(Request $request, string $subscriptionId) { $productId = $request->input('product_id'); $subscription = Creem::subscriptions()->upgrade( $subscriptionId, $productId ); return back()->with('success', 'Subscription upgraded successfully'); } }
License Validation Controller
namespace App\Http\Controllers; use Romansh\LaravelCreem\Facades\Creem; use Illuminate\Http\Request; class LicenseController extends Controller { public function validate(Request $request) { $validated = $request->validate([ 'license_key' => 'required|string', 'instance_id' => 'required|string', ]); try { $license = Creem::licenses()->validate( $validated['license_key'], $validated['instance_id'] ); if ($license['status'] === 'active') { return response()->json([ 'valid' => true, 'expires_at' => $license['expires_at'], ]); } return response()->json(['valid' => false], 403); } catch (\Exception $e) { return response()->json(['valid' => false], 403); } } }
Advanced Configuration
Custom HTTP Settings
Modify config/creem.php:
'http' => [ 'timeout' => 30, 'retry' => [ 'times' => 3, 'sleep' => 100, ], ],
Custom Webhook Path
'webhook' => [ 'path' => '/custom/webhook/path', 'middleware' => ['api', 'throttle:60,1'], ],
Multiple Webhook Endpoints
You can set up different webhook endpoints for different profiles:
// routes/web.php use Romansh\LaravelCreem\Http\Controllers\WebhookController; use Romansh\LaravelCreem\Http\Middleware\VerifyCreemWebhook; Route::post('/webhooks/product-a', WebhookController::class) ->middleware([VerifyCreemWebhook::class.':product_a']); Route::post('/webhooks/product-b', WebhookController::class) ->middleware([VerifyCreemWebhook::class.':product_b']);
API Reference
Profile Resolution Rules
-
String (Profile Name): Loads named profile from config
Creem::profile('product_a')->checkouts()->create([...]);
-
No Profile (Default): Uses 'default' profile
Creem::checkouts()->create([...]);
-
Array (Inline Config): Uses provided configuration
Creem::withConfig(['api_key' => '...'])->checkouts()->create([...]);
Troubleshooting
Invalid Webhook Signature
Ensure your webhook secret matches between .env and Creem dashboard.
# Check your configuration php artisan tinker >>> config('creem.profiles.default.webhook_secret')
API Key Issues
// Verify your API key is loaded config('creem.profiles.default.api_key') // Check if test mode is enabled config('creem.profiles.default.test_mode')
Missing Profile Exception
Creem profile 'xyz' not found in configuration.
Solution: Add the profile to config/creem.php or use an existing profile name.
Contributing
Contributions are welcome! Please ensure:
- PSR-12 code style compliance
- All tests pass
- New features include tests
- Documentation is updated
Run Laravel Pint for code formatting:
./vendor/bin/pint
Security
If you discover a security vulnerability, please email the package maintainer.
License
The MIT License (MIT). Please see License File for more information.
Support
- Creem Documentation: https://docs.creem.io
- Package Issues: https://github.com/romansh/laravel-creem/issues