devajmeireles/paghiper-for-laravel
Seamless Laravel integration for PagHiper, the Brazilian payment gateway, letting you quickly generate boleto and PIX charges, manage transactions, and handle payment notifications directly within your PHP applications through a clean, fluent API.
Install and configure
composer require devajmeireles/paghiper-for-laravel
php artisan vendor:publish --tag="paghiper-config"
Add credentials to .env (names follow the published config; typical keys are PAGHIPER_API_KEY and PAGHIPER_TOKEN, plus a sandbox toggle):
PAGHIPER_API_KEY=your-api-key
PAGHIPER_TOKEN=your-token
PAGHIPER_SANDBOX=true
Clear config after changing env values:
php artisan config:clear
Where to look first
config/paghiper.php — endpoints, credentials, sandbox flag, and any default request options.DevAjMeireles\PagHiper\Facades\PagHiper) — this is the entry point for boleto() and pix() resources.First use case: charge a customer with PIX
The most common day-one flow is generating a PIX charge for an existing Order and persisting the returned transaction identifier and QR code:
use DevAjMeireles\PagHiper\Facades\PagHiper;
$charge = PagHiper::pix()->create([
'order_id' => $order->id,
'payer_name' => $order->customer->name,
'payer_email' => $order->customer->email,
'payer_cpf_cnpj' => $order->customer->document,
'payer_phone' => $order->customer->phone,
'description' => "Pedido #{$order->id}",
'amount' => $order->total,
'due_date' => now()->addDay()->toDateString(),
]);
$order->update([
'gateway' => 'paghiper',
'transaction_id' => $charge['transaction_id'] ?? null,
'payment_status' => $charge['status'] ?? 'pending',
'payment_payload' => $charge, // keep raw response for support/debug
]);
Swap in PagHiper::boleto() for a boleto flow; the payload shape is similar but with boleto-specific fields (due date, days to expire, late-payment rules).
Sandbox first
Turn PAGHIPER_SANDBOX=true on and validate the full lifecycle (create → poll status → webhook → cancel) before touching production keys. PagHiper sandbox credentials are separate from production.
Wrap the facade in an application service
Don’t scatter PagHiper::… calls across controllers. Introduce a thin PagHiperService or CreatePixCharge action:
final class CreatePixCharge
{
public function __invoke(Order $order): array
{
return PagHiper::pix()->create([
'order_id' => (string) $order->id,
'payer_name' => $order->customer->name,
// ...
]);
}
}
This gives you one place to normalize payloads, log responses, and swap gateways later.
Persist everything the gateway sends back
Store at minimum: transaction_id, status, barcode/digitable_line (boleto) or qrcode_image_url/emv (PIX), plus the raw response JSON. PagHiper support requests will ask for the transaction ID, and you will want the raw payload when a customer claims they paid but your app disagrees.
A dedicated payments table keyed by transaction_id (unique index) beats stuffing everything on orders.
Idempotency via order_id
PagHiper uses your order_id as the merchant reference. Make it deterministic (order->id, or a UUID for one-off charges) so that retried jobs or double-submitted forms don’t create duplicate charges. Guard the charge creation with a unique constraint or a lockForUpdate() inside a transaction.
Webhook controller pattern
PagHiper posts status notifications. Keep it dumb and idempotent:
Route::post('/webhooks/paghiper', PagHiperWebhookController::class)
->withoutMiddleware(['csrf']);
public function __invoke(Request $request)
{
$payload = $request->all();
$payment = Payment::where('transaction_id', $payload['transaction_id'] ?? null)->first();
if (! $payment) {
return response()->noContent(); // 204 — unknown transaction
}
// Only move forward, never regress statuses
$payment->update([
'status' => $payload['status'] ?? $payment->status,
'raw' => $payload,
'paid_at' => ($payload['status'] ?? null) === 'paid' ? now() : $payment->paid_at,
]);
event(new PaymentUpdated($payment));
return response()->noContent();
}
Wrap the handler in a DB transaction, and make it safe to receive the same notification twice.
Polling as a fallback
Webhooks are the primary source of truth, but a scheduled command that re-checks open charges (PagHiper::boleto()->status($id)) catches missed notifications:
Payment::where('status', 'pending')
->where('created_at', '<', now()->subMinutes(30))
->each(fn ($p) => app(SyncPaymentStatus::class)($p));
Queue the charge creation
PagHiper’s API is external; create the charge from a queued job (CreatePagHiperCharge::dispatch($order)) so checkout stays fast and failures can retry with exponential backoff. Ensure the job is idempotent using the order_id guard above.
Testing
If the package uses the Laravel HTTP client under the hood, Http::fake() in feature tests lets you assert the outgoing payload and simulate responses without hitting PagHiper. For end-to-end confidence, run a small sandbox suite nightly rather than in CI on every commit.
Rendering to the customer
The facade returns an array-like response; expose only what the customer needs. For PIX, show the EMV string and QR image URL with a countdown to due_date. For boleto, render the digitable line and a link to the PDF — don’t force the user to download a PDF to copy the barcode.
Amount formatting is the classic trap. PagHiper expects money in a specific unit and type (check whether the package wants a string with two decimals or cents). Send 10.5 vs 1050 incorrectly and you’ll charge 100× or 0.01× the intended amount. Write one Money::toPagHiper() helper and use it everywhere.
Sandbox and production credentials are not interchangeable. A common symptom is “401 unauthorized” or “invalid token” that turns out to be production credentials with sandbox=true (or vice versa). Add an assert helper in your service that throws early with a clear message.
config:cache will silently keep old credentials. After changing .env, run php artisan config:clear (or redeploy with cache rebuild). This trips up almost everyone the first time they rotate keys.
Payer document requirements. PagHiper validates CPF/CNPJ. Validate and normalize (strip punctuation) before calling the API — client-side validation alone is not enough. Invalid documents often surface as generic API errors rather than helpful field errors.
Due dates and timezone. Server timezone vs PagHiper’s expectations (usually America/Sao_Paulo) can shift a boleto’s due date by a day. Pass dates as Y-m-d strings built with now('America/Sao_Paulo'), not raw Carbon objects, unless the package explicitly handles them.
Webhook authenticity. The package may or may not verify the webhook payload. Don’t assume it does — compare the transaction_id/order_id against your database and ignore unknown IDs. Never trust the amount from a webhook; trust your own stored order total.
Status transitions are not linear. PagHiper can send pending, paid, canceled, refunded, and possibly processing. Model this as an enum and only allow forward transitions; a delayed pending webhook arriving after paid must not regress the payment.
Response shape varies by resource. Boleto and PIX return different keys (barcode vs QR code). Don’t build a single generic mapper — keep two thin presenters so blade/API responses stay honest about what exists.
Error handling should distinguish transport from business errors. A 500 from PagHiper (retryable) is different from “payer_cpf_cnpj invalid” (not retryable, needs user input). Inspect the response body and map to domain exceptions (PagHiperTransientException vs PagHiperValidationException) so your queue can decide whether to retry.
Rate limits and retries. If you fan out charges in a job batch, add a small delay and cap concurrency. Use retry(3, fn ($attempt) => $attempt * 1000) on the job rather than tight loops.
This package is small and young (21 stars, 0 dependents). That’s fine for a focused integration, but don’t assume it covers refunds, splits, or advanced features. Read the source of the resource classes — they’re small enough to audit in an afternoon — and wrap the facade in your own service so an eventual fork or swap is a one-file change. Pin the version in composer.json and review changelogs on upgrade; the last release date can drift.
Extension point to consider: if the package exposes custom HTTP client or event hooks, use them for logging and metrics (request latency, failure rate) rather than sprinkling Log::info inside controllers. A single middleware or event listener gives you observability across boleto and PIX with no duplication.
How can I help you explore Laravel packages today?