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

Chargily Pay Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

  • Two different secrets. The API key authenticates requests; the webhook secret verifies signatures. Mixing them up produces 401s on creation and silent verification failures on webhooks. Name the env vars unambiguously.
  • Raw-body signature verification. As above: $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 units. Confirm from the V2 API reference whether 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.
  • No Laravel integration means no CSRF whitelist for you. Webhook routes will 419 by default. In Laravel 11/12, exclude the path in 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 is your friend and is size-limited. Stuff the order ID and any correlation key in metadata rather than parsing your own URLs. Keep it small and flat; don't put a whole serialized model in there.
  • Idempotency on creation. If a user double-submits the pay button, you get two checkouts and possibly two charges. Guard with a DB transaction plus a unique pending-checkout constraint per order, and expire the stale one via the resource's expire/update method before creating a new one.
  • Small package, small blast radius. 17 stars and no dependents means the API surface can shift between releases and there is little community troubleshooting. Pin the exact version in 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.
  • Exceptions are not all the same. Distinguish transport/timeout failures (retryable) from 4xx validation errors (fix your payload) from 401 (bad key). Conflating them means you retry a permanently broken request forever in a queue worker.
  • Log the checkout ID, not the API key. Debugging is easiest when every log line carries chargily_checkout_id and the webhook event name. Scrub authorization headers from any Guzzle debug logging.
  • Extension point: the library is thin enough that your own adapter — one class with 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.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource