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

Omnipay Pagseguro Laravel Package

abdala/omnipay-pagseguro

Deep Wiki
Context7

Getting Started

The package is a standard Omnipay driver, so the day-to-day mental model is: build a gateway, send a request, handle a response. Nothing Laravel-specific ships with it — you wire it into the framework yourself.

1. Install and check which Omnipay major you got. The last release is March 2020, so most copies of this package sit on Omnipay 2.x (omnipay/common: ^2.3) with a few on 3.x. This matters more than anything else in this assessment:

composer require abdulmohsen/omnipay-pagseguro
composer show omnipay/common

If it pulls 2.x on a modern Laravel/PHP 8+ app, expect Guzzle 5/6 and Symfony\Component\HttpFoundation era constraints to fight your lockfile. Resolve that before writing a single line of checkout code.

2. Put credentials in config/services.php, not in code. PagSeguro needs the account e-mail and an API token, plus a sandbox flag:

'pagseguro' => [
    'email'   => env('PAGSEGURO_EMAIL'),
    'token'   => env('PAGSEGURO_TOKEN'),
    'sandbox' => env('PAGSEGURO_SANDBOX', true),
],

3. Look at the source before the docs. Open src/Gateway.php and src/Message/*Request.php. That tells you, in two minutes, exactly which parameters each request accepts, the endpoint paths, and whether purchase() supports boleto/creditCard as separate methods or via a paymentMethod flag. With an unmaintained gateway this is the only reliable spec.

4. First use case: the redirect checkout. Don't start with credit card direct payment — start with the hosted checkout, which is the path of least resistance and covers boleto + cards in one go:

$response = $gateway->purchase([
    'reference'       => $order->uuid,
    'amount'          => $order->total,     // decimal string, e.g. "149.90"
    'currency'        => 'BRL',
    'returnUrl'       => route('checkout.return', $order),
    'notificationUrl' => route('webhooks.pagseguro'),
    'items'           => $order->items->map(fn ($i) => [
        'name'     => Str::limit($i->name, 100),
        'quantity' => $i->quantity,
        'amount'   => $i->unit_price,
    ])->all(),
    'sender' => ['name' => $order->customer_name, 'email' => $order->customer_email],
])->send();

return $response->isRedirect()
    ? redirect()->away($response->getRedirectUrl())
    : throw new PaymentFailedException($response->getMessage());

Implementation Patterns

Bind the gateway once. A tiny wrapper class beats Omnipay::create() sprinkled across controllers, and it gives you one place to swap the client for tests:

class PagSeguroGateway
{
    public function gateway(): GatewayInterface
    {
        return Omnipay::create('PagSeguro')
            ->setEmail(config('services.pagseguro.email'))
            ->setToken(config('services.pagseguro.token'))
            ->setSandbox(config('services.pagseguro.sandbox'));
    }
}

Register it as a singleton in a service provider if you prefer, but keep it stateless — gateway objects are cheap.

Two-phase flow, always. Phase one creates the payment and stores $response->getTransactionReference() (the checkout code) on the order. Phase two is the confirmation: either the user returning to returnUrl, or the notificationUrl webhook. Both must funnel into the same syncOrder() method, because the return URL is cosmetic and the webhook is authoritative — users close tabs, and PagSeguro's POST can arrive before the redirect.

Webhook handler: acknowledge fast, process async. PagSeguro posts notificationCode (and notificationType) as form data, with no signature and no status. All you can do is queue a job:

Route::post('/webhooks/pagseguro', function (Request $request) {
    abort_unless($request->filled('notificationCode'), 400);
    SyncPagSeguroTransaction::dispatch($request->input('notificationCode'));
    return response('OK', 200);
})->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);

The job then calls the API to learn the truth (fetchTransaction(['transactionReference' => $code]) — confirm the exact key name in FetchTransactionRequest.php), reads the reference field back, and matches it to your order. Never trust the notification body for state.

Map PagSeguro's numeric statuses once, in one enum/class. The legacy codes are: 1 awaiting payment, 2 in analysis, 3 paid, 4 available, 5 in dispute, 6 refunded, 7 cancelled, 8 debited, 9 temporary hold. Only 3/4 should fulfil an order; 5/6/7 should reverse it. Every codebase that gets this wrong does so by scattering if ($status == 3) around.

Persist the raw payload. Store the decoded transaction (or the raw XML) in a payment_events table with the order id. When a customer disputes a charge six weeks later, this is the only evidence you'll have.

Idempotency via reference. Use your own order UUID as reference and make syncOrder() a no-op if the order is already in a terminal state. PagSeguro will send duplicate notifications; your job will be retried; the return URL will fire after the webhook.

Sandbox in tests, setHttpClient() for unit tests. For feature tests, point sandbox => true at a real sandbox account. For unit tests, Omnipay's mock HTTP client (3.x: $gateway->setHttpClient($mock)) lets you assert on the outgoing XML without network calls — far more valuable here than mocking your own service class.

Gotchas and Tips

Omnipay::create('PagSeguro') may not resolve. In Omnipay 3 the factory assumes the namespace Omnipay\PagSeguro. If the driver lives under a vendor namespace, the factory throws InvalidArgumentException: Class does not exist — pass the fully-qualified gateway class from src/Gateway.php instead. This is the single most common "the package is broken" false alarm.

Amounts must reconcile. PagSeguro rejects a checkout where the sum of quantity × amount across items doesn't match the transaction amount. Compute the total from the items, not the other way round, and be explicit about shipping and discounts rather than folding them into the amount.

Currency is BRL only. No multi-currency, no setCurrency('USD') experiments. If your store sells internationally you'll be doing FX conversion before the gateway ever sees the request.

Sandbox is a separate universe. Separate account, separate token, separate sandbox.pagseguro.uol.com.br endpoints. Notifications from sandbox cannot reach localhost — use ngrok/expose and set the notificationUrl per request (not in the dashboard), otherwise you'll spend an afternoon debugging a webhook that was never delivered.

Sender data is fussier than it looks. Boleto and direct card payments need a CPF/CNPJ (senderDocument) and a phone split into area code and number. sender.email must match the PagSeguro account e-mail? No — but it must be a valid, unique-ish address, and duplicate/invalid e-mails surface as vague error codes, not validation messages. Validate in a FormRequest first.

Credit card direct payment needs the JavaScript token. PagSeguro does not accept raw PANs server-side; you must run their PagSeguroDirectPayment JS to obtain a creditCardToken and senderHash and pass those through. If the gateway exposes setCard() too, be aware the parameter name token is overloaded (gateway API token vs. card token) — read CreditCardRequest.php carefully. This is also where PCI scope lives, so keep it that way.

Timeouts and retries. PagSeguro's legacy API is slow and occasionally 500s. Set a Guzzle timeout explicitly (10–15s), never call it synchronously inside a request that a user is waiting on if you can avoid it, and do not blindly retry a purchase — retry fetchTransaction, which is safe.

No maintenance since 2020 + PagSeguro → PagBank migration. The legacy XML checkout this driver targets is the one PagBank has been steering merchants away from (JSON Orders API / Checkout v4). Nothing here is patched for PHP 8.2 deprecations, and no one is fixing bugs upstream. Practical mitigation: keep the gateway behind your own PaymentGateway interface (charge(), sync(), refund()) so replacing the driver is a one-class change, and pin omnipay/common in composer.json so a transitive bump can't silently break it.

Where to extend if you must fork. Gateway::getDefaultParameters() for new config keys, a custom AbstractRequest subclass for endpoints the driver doesn't cover (refunds and cancellations are frequently missing), and a Guzzle middleware or Laravel HTTP client wrapper via setHttpClient() for request/response logging. Log the outgoing reference and the returned transaction code on every call — when reconciling against PagBank's dashboard, those two fields are your join key.

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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route