michael/laravelpagseguro
Laravel package for integrating PagSeguro payments into your application. It provides a simple API to create checkout requests, handle payment notifications, and manage transactions, making Brazilian payment integration straightforward for Laravel developers.
Install the package and publish its config:
composer require michael/laravelpagseguro
php artisan vendor:publish --provider="Michael\PagSeguro\PagSeguroServiceProvider" --tag=config
On Laravel 5.5+ the service provider and PagSeguro alias are auto-discovered. On 5.4 and below, register them manually in config/app.php:
'providers' => [
Michael\PagSeguro\PagSeguroServiceProvider::class,
],
'aliases' => [
'PagSeguro' => Michael\PagSeguro\Facades\PagSeguro::class,
],
Put credentials in .env (never commit them), pointing the config file at those keys:
PAGSEGURO_ENVIRONMENT=sandbox
[email protected]
PAGSEGURO_TOKEN=YOUR_SANDBOX_TOKEN
PAGSEGURO_NOTIFICATION_URL=https://your-app.example/pagseguro/notification
PAGSEGURO_REDIRECT_URL=https://your-app.example/checkout/return
Where to look first, in this order:
config/pagseguro.php — confirms which env keys are read and whether sandbox/production blocks are separate.vendor/michael/laravelpagseguro/src/Facades/PagSeguro.php and the provider — tells you exactly which request types the facade proxies (payment(), checkout(), session(), notification(), transaction(), authorization()).pagseguro/pagseguro-php-sdk docs/ and examples/ folder — the wrapper is thin, so most builder methods (setReference, addItems()->withParameters, setSender(), setShipping()) are the SDK's and documented there.First use case: a redirect checkout. Generate a payment request, get a payment link back, and send the buyer there. You only need a reference (your order ID), items, sender, and shipping; PagSeguro will collect the rest.
use Michael\PagSeguro\Facades\PagSeguro;
$request = PagSeguro::payment();
$request->setReference($order->uuid); // your idempotency key
$request->setCurrency('BRL');
$request->addItems()->withParameters('SKU-1', 'Pro Plan', 1, '99.90');
$request->setSender()
->setName($order->customer_name)
->setEmail($order->customer_email)
->setDocuments()->withParameters('CPF', $order->cpf)
->setPhone()->withParameters(11, '999999999');
$request->setShipping()->setAddress()->withParameters(
'01310-100', 'Av. Paulista', '1000', 'Bela Vista', 'São Paulo', 'SP', 'Brazil', '01310100'
);
$result = $request->register(); // returns code + payment link
return redirect()->away($result->getPaymentLink());
Sandbox and production credentials are completely separate accounts — sign up at sandbox.pagseguro.uol.com.br, generate a token there, and flip PAGSEGURO_ENVIRONMENT only when you have a production token.
Wrap the facade behind your own gateway class. The facade is a static proxy; injecting it directly into controllers makes testing painful. Define a PagSeguroGateway interface, implement it with the facade, and bind it in a service provider. Then you can swap in a fake during tests (there is no built-in fake transport).
Redirect checkout flow (most common).
pending state locally, commit it, then build the register() request.code on the order.getPaymentLink().paid.Never treat the redirect-back URL as payment confirmation — the buyer can close the tab or forge the query string. Only the notification/transaction lookup is authoritative.
Direct payment with a credit card requires the PagSeguro JavaScript library on your form. You need three things from the browser before calling register():
<script src="https://stc.sandbox.pagseguro.uol.com.br/pagseguro/api/v2/checkout/pagseguro.directpayment.js"></script>
<script>
$.getJSON('/pagseguro/session', function (data) {
PagSeguroDirectPayment.setSessionId(data.session);
});
// then on submit:
// PagSeguroDirectPayment.getSenderHash()
// PagSeguroDirectPayment.createCardToken({ cardNumber, cvv, expirationMonth, expirationYear, holderName })
// PagSeguroDirectPayment.getInstallments({ amount, brand, maxInstallNoInterest })
</script>
Expose the session ID from Laravel:
Route::get('/pagseguro/session', fn () => response()->json([
'session' => PagSeguro::session()->create(),
]));
Pass the resulting senderHash and creditCardToken into the server-side request, plus the installment quantity and value. Card data never touches your server — that is the entire point of the token.
Webhook for notifications. PagSeguro POSTs a notificationCode; you must fetch the transaction to learn what actually happened. Make the handler idempotent and always return 200:
Route::post('/pagseguro/notification', function (Request $request) {
$code = $request->input('notificationCode');
// dedupe: PagSeguro retries aggressively
if (! Cache::add("pagseguro:notification:{$code}", true, now()->addDay())) {
return response('', 200);
}
$transaction = PagSeguro::notification($code)->getTransaction();
Order::where('uuid', $transaction->getReference())
->firstOrFail()
->syncPaymentStatus($transaction->getStatus()); // 3 = paid, 7 = canceled, etc.
return response('', 200);
});
Transaction reconciliation job. Because notifications can be missed, run a scheduled command that finds orders stuck in pending older than N minutes and calls PagSeguro::transaction()->getByReference($uuid) to reconcile. This is also how you safely handle a timeout on register() — query first, never blindly retry a payment request.
Multi-account / multi-tenant. The SDK keeps credentials in a Configure singleton. If you sell for more than one PagSeguro account, call the SDK's Configure::setAccountCredentials() (or re-set environment, email, token) immediately before each request rather than relying on the config file. Keep this in a single repository method so it cannot leak between jobs in a queue.
Authorization / split (appId + appKey). If you are building a marketplace, the authorization() side of the facade handles the OAuth-ish flow, and appId/appKey in config are only used there. Regular checkouts need only email + token.
The package is frozen (last release January 2018) and targets Laravel 5.x / PHP 5.6–7.x. On Laravel 9+/PHP 8 you will hit Composer platform constraints and dynamic-property deprecations inside the old SDK. Options: pin an old PHP runtime for the payment service, fork the package, or composer require --ignore-platform-reqs and patch warnings. Budget for a migration to the current PagBank/PagSeguro JSON API eventually — the legacy XML API this wraps is in maintenance mode.
Config caching surprises. If you read env('PAGSEGURO_TOKEN') outside config/pagseguro.php, it returns null after php artisan config:cache. Always resolve credentials through config('pagseguro.*').
Sandbox and production are separate accounts with separate tokens. A production email with a sandbox token (or vice versa) produces authentication errors that look like malformed requests. Also verify the JS library URL matches the environment — loading the sandbox directpayment.js on production silently breaks tokens.
Amounts must reconcile exactly. sum(item.amount × item.quantity) + shipping.cost === totalAmount, with two decimal places and a . separator. A one-cent mismatch fails validation. Keep money as integer cents in your domain and convert once at the boundary.
CPF and phone are validated. The sender needs a syntactically valid 11-digit CPF and area code + number passed separately via withParameters. Sandbox accepts test CPFs; production validates against the receipt.
Sessions expire in roughly 30 minutes. Do not cache a session ID in a long-lived page or a queued job. Request a fresh one when the checkout form renders, and regenerate on submit failure.
Notification URLs must be public HTTPS. localhost and http:// are rejected, and the URL is sometimes validated at request-creation time — a bad notificationURL can fail register() entirely, not just the callback. Use ngrok or expose locally.
notificationCode is not the transaction code. Fetching transaction()->getByCode($notificationCode) will 404. Use notification($code)->getTransaction().
Deduplicate notifications. PagSeguro retries on non-200 and can send duplicates. Use a cache/unique constraint keyed on the notification code, and make status transitions idempotent (if ($order->status === 'paid') return;).
Error handling is verbose. Wrap register() in try/catch and log both $e->getMessage() and any structured error list ($e->getErrors() / the SDK's error parser) — PagSeguro returns a list of codes such as 11013 (invalid item quantity) or 53004 (invalid notification URL) that are far more useful than the exception message. 53004 in particular means "your config is wrong", not "the user's card is wrong".
Status codes are numeric, not strings. 3 = paid, 4 = available, 6 = refunded, 7 = canceled. Map them in one place; do not compare against 'PAID'.
No first-party test double. Because the facade is static, PagSeguro::shouldReceive(...) only works with Mockery class mocks and is fragile. Bind an interface and fake it — that also keeps SDK types out of your controllers.
Timezones and dates. Transactions come back as DateTime objects in Brazil time (America/Sao_Paulo). Persist as UTC and convert for display; comparing created_at against now() without normalizing will mis-reconcile orders around DST-free midnight boundaries.
Extension point: the service provider registers a single facade root. If you need to add behaviour (retry with backoff, per-tenant credentials, structured logging), extend or decorate the package's main class and rebind it in a provider rather than editing vendor/. That keeps composer update viable while you plan the eventual fork or replacement.
How can I help you explore Laravel packages today?