chargily/chargily-pay
Laravel package for integrating Chargily Pay, the Algerian payment gateway. Create checkouts, redirect customers, verify webhooks, and manage payments through a simple, configurable API designed for PHP and Laravel applications.
The package is a thin, framework-agnostic PHP client for the Chargily Pay V2 REST API. There is no Laravel service provider, no facade, no publishable config — you get a client object and resource methods, and you wire it into Laravel yourself. Budget ~15 minutes to first redirect.
1. Install and hold a key
composer require chargily/chargily-pay
You need two credentials from the Chargily dashboard: an API key and a secret key (the secret is used for webhook HMAC verification, not for authenticating requests). Test-mode and live-mode keys are separate; keep both pairs in .env and select by environment.
CHARGILY_API_KEY=test_sk_...
CHARGILY_SECRET_KEY=test_secret_...
CHARGILY_MODE=test
2. Where to look first
Read the client constructor and the checkouts resource before anything else — checkouts are 90% of real usage. Skim the resource classes (checkouts, customers, products, prices, balance) to see the available verbs, and read the exception types so you know what bubbles up on a 4xx versus a transport failure. Everything else in the package is CRUD around the same shape.
3. First use case: a paid checkout redirect
Register a binding (see Implementation Patterns), then in a controller:
use Chargily\ChargilyPay\ChargilyPay;
Route::post('/orders/{order}/pay', function (Order $order, ChargilyPay $chargily) {
$checkout = $chargily->checkouts()->create([
'amount' => $order->total_dzd, // integer, DZD — confirm units in the API docs
'currency' => 'dzd',
'success_url' => route('checkout.success', $order),
'failure_url' => route('checkout.failure', $order),
'webhook_endpoint' => route('webhooks.chargily'),
'metadata' => ['order_id' => $order->id],
]);
$order->update(['chargily_checkout_id' => $checkout->getId()]);
return redirect($checkout->getUrl());
})->middleware('auth');
Persist the checkout ID before redirecting — it is your only reliable link between the customer's browser and the webhook you will receive later. Treat the success/failure URLs as UX only; the webhook is the source of truth.
Wrap the client in a singleton with a config file. The library expects credentials at construction, so bind it once and inject the interface-shaped client everywhere:
// app/Providers/AppServiceProvider.php
$this->app->singleton(ChargilyPay::class, fn () => new ChargilyPay(
config('services.chargily.api_key'),
config('services.chargily.secret_key'),
));
Put api_key, secret_key, and mode under config/services.php. This keeps env() out of your domain code and makes it trivial to swap test/live credentials per environment.
Model the checkout as a state machine on your own table. Store chargily_checkout_id, status, paid_at, and the raw webhook payload. Your order status should never be derived by calling the Chargily API at render time — you want the app to work when their API is slow or down. Use a chargily_webhook_events table keyed uniquely on the event ID for idempotency, then transition the order inside a transaction.
Webhook handling: verify raw body, then queue.
Route::post('/webhooks/chargily', WebhookController::class)
->withoutMiddleware(['csrf']) // Laravel 11+: exclude via bootstrap/app.php instead
->name('webhooks.chargily');
Verification must run against the raw request body, not the parsed array — JSON re-encoding changes byte order and whitespace and will fail the HMAC. In the controller:
$payload = $request->getContent();
$signature = $request->header('signature');
if (! $chargily->verifySignature($payload, $signature)) { // method name per current docs
abort(403);
}
dispatch(new ProcessChargilyWebhook(json_decode($payload, true)));
return response()->noContent();
Acknowledge fast (2xx), process in a queued job. Switch on the event name (checkout.paid, checkout.failed, checkout.expired), look up the order by the checkout ID in data, and be idempotent — Chargily will retry non-2xx responses, and retries can arrive out of order.
Treat success/failure redirects as untrusted. On checkout.success, don't flip the order to paid. Either wait for the webhook, or re-fetch the checkout server-side ($chargily->checkouts()->get($id)) and check its status. Both are acceptable; the re-fetch is the better UX because it avoids the "paid but page still says pending" window.
Reconcile with a scheduled job. A nightly php artisan chargily:reconcile command that re-fetches any order still in pending older than 30 minutes catches missed webhooks (deploys, firewall changes, local dev). This is the single highest-value piece of glue code you will write around this package.
Testing. The client talks to the network directly, so stub at the boundary: bind a fake ChargilyPay-shaped object in the container during tests, or use Laravel's Http::fake() only if the library is built on the Laravel/Guzzle HTTP client with predictable URL patterns — otherwise a hand-rolled interface adapter is cleaner and lets you assert on the payload you send. For webhook tests, compute the signature in the test with the same secret so you exercise the real verification path.
$request->getContent(), never json_encode($request->all()). Also compare with a timing-safe function (hash_equals), which the library should already do — verify it does before you roll your own.amount is an integer in the smallest unit or a decimal DZD figure, and whether currency must be lowercase dzd. Get this wrong and you charge 100× or reject every request. Write one unit test asserting the exact payload your app sends.bootstrap/app.php ($middleware->validateCsrfTokens(except: ['webhooks/chargily'])) rather than relying on VerifyCsrfToken inheritance.success_url/failure_url must be absolute and publicly reachable. route() gives you this, but APP_URL misconfigured in staging will produce redirects that go nowhere. Chargily generally requires HTTPS in live mode.metadata rather than parsing your own URLs. Keep it small and flat; don't put a whole serialized model in there.composer.json, read the CHANGELOG before upgrading, and consider a thin ChargilyGateway interface of your own so a future breaking change or a swap to raw Guzzle is a one-file edit.chargily_checkout_id and the webhook event name. Scrub authorization headers from any Guzzle debug logging.createCheckout(Order $order): string and verifyWebhook(Request $request): array — is worth writing on day one. It localizes every one of the pitfalls above and makes the package replaceable.How can I help you explore Laravel packages today?