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

Paypal Sandbox Laravel Package

socialiteproviders/paypal-sandbox

Socialite provider for PayPal Sandbox, enabling OAuth authentication in Laravel apps. Adds a PayPal Sandbox driver to Laravel Socialite so you can test login flows safely before going live. Install via Composer, configure credentials, and authenticate users with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require socialiteproviders/paypal-sandbox

This package is a Socialite driver adapter, not a standalone SDK. It supplies a PayPalSandboxExtendSocialite listener that registers a paypal-sandbox driver with Socialite's SocialiteWasCalled event. Nothing works until that listener is wired up — this is the single most common reason people think the package is "broken".

Wire the event listener

Laravel ≤10 (app/Providers/EventServiceProvider.php):

protected $listen = [
    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        \SocialiteProviders\PayPalSandbox\PayPalSandboxExtendSocialite::class.'@handle',
    ],
];

Laravel 11/12 (no EventServiceProvider by default), in AppServiceProvider::boot():

Event::listen(
    \SocialiteProviders\Manager\SocialiteWasCalled::class,
    \SocialiteProviders\PayPalSandbox\PayPalSandboxExtendSocialite::class.'@handle'
);

Add credentials to config/services.php. The array key must match the driver name you pass to Socialite::driver():

'paypal-sandbox' => [
    'client_id'     => env('PAYPAL_SANDBOX_CLIENT_ID'),
    'client_secret' => env('PAYPAL_SANDBOX_CLIENT_SECRET'),
    'redirect'      => env('PAYPAL_SANDBOX_REDIRECT_URI'),
],

First use case — "Log in with PayPal" in sandbox

Route::get('/auth/paypal/redirect', fn () => Socialite::driver('paypal-sandbox')->redirect());
Route::get('/auth/paypal/callback', function () {
    $paypalUser = Socialite::driver('paypal-sandbox')->user();
    // $paypalUser->getId(), ->getNickname(), ->getName(), ->getEmail(), ->getAvatar()
});

Get the client ID/secret from the PayPal Developer Dashboard by creating a REST app and ticking Log in with PayPal under its features. Set the Return URL there to exactly the same value as PAYPAL_SANDBOX_REDIRECT_URI — PayPal validates it byte-for-byte, including trailing slashes and http vs https.

Where to look first: the SocialiteProviders\PayPalSandbox\Provider class in vendor/socialiteproviders/paypal-sandbox/src/ — it is a short class and shows you the exact authorize/token/userinfo endpoints and default scopes. Read it before debugging anything; it answers most questions faster than PayPal's docs.

Implementation Patterns

Persist the identity, don't re-derive it. Store both the provider name and the provider user ID so the same users table can back multiple providers:

$user = User::updateOrCreate(
    ['provider' => 'paypal-sandbox', 'provider_id' => $paypalUser->getId()],
    [
        'name'              => $paypalUser->getName(),
        'email'             => $paypalUser->getEmail(),
        'avatar'            => $paypalUser->getAvatar(),
        'email_verified_at' => $paypalUser->user['verified_account'] ?? false ? now() : null,
    ]
);
Auth::login($user, remember: true);

The raw PayPal userinfo payload is available on $paypalUser->user — this is where fields like payer_id, verified_account, account_type, user_id, and address/phone data live when you request the corresponding scopes.

Scope selection is your main lever. Call ->scopes([...]) before redirect() to ask for more than the default openid profile email:

return Socialite::driver('paypal-sandbox')
    ->scopes(['openid', 'profile', 'email', 'address', 'phone'])
    ->redirect();

openid is effectively mandatory for Log in with PayPal; dropping it tends to produce opaque errors at PayPal rather than in your app. Request the minimum you actually use — the consent screen gets abandoned fast when you ask for address and phone for a read-only login. Use ->setScopes([...]) when you want to replace rather than append.

Link, don't replace, for existing accounts. A very common day-to-day pattern is attaching PayPal to a logged-in user:

Route::get('/settings/paypal/link', fn () => Socialite::driver('paypal-sandbox')->redirect())->middleware('auth');

Route::get('/settings/paypal/callback', function () {
    $paypalUser = Socialite::driver('paypal-sandbox')->user();
    auth()->user()->socialAccounts()->updateOrCreate(
        ['provider' => 'paypal-sandbox'],
        ['provider_id' => $paypalUser->getId(), 'token' => $paypalUser->token]
    ]);
    return redirect('/settings')->with('status', 'PayPal linked');
})->middleware('auth');

Keep the link route behind auth and keep the callback route public, or the state round-trip will log users out.

Handle the callback failure path explicitly. user() throws; it does not return null.

try {
    $paypalUser = Socialite::driver('paypal-sandbox')->user();
} catch (\Laravel\Socialite\Two\InvalidStateException $e) {
    return redirect('/login')->withErrors(['paypal' => 'Login expired, please try again.']);
} catch (\Throwable $e) {
    report($e);
    return redirect('/login')->withErrors(['paypal' => 'PayPal sandbox unavailable.']);
}

Store the token when you need PayPal APIs. $paypalUser->token, ->refreshToken, and ->expiresIn are populated from the token exchange when PayPal returns them. Because this driver is sandbox-scoped, this is genuinely useful for building out a checkout/payout flow against api.sandbox.paypal.com before switching to live — the access token shape is the same, so your integration code ports over.

Testing. Fake at the Socialite layer for feature tests, and stub HTTP for the driver layer:

Socialite::shouldReceive('driver->user')->andReturn(
    (new \Laravel\Socialite\Two\User)->map([
        'id' => 'ABC123', 'name' => 'Test Buyer',
        'email' => '[email protected]', 'avatar' => null,
    ])
);

For flows that actually exercise the provider, Http::fake(['api.sandbox.paypal.com/*' => Http::response([...])]).

Gotchas and Tips

This package is sandbox-only, permanently. The endpoints are hardcoded to sandbox.paypal.com / api.sandbox.paypal.com. It is not a config toggle away from production. A recurring production incident is someone wiring this driver into a live login page during a rushed launch and discovering it after a support ticket. Guard it:

abort_unless(app()->environment(['local', 'staging']), 404);

Use the live-equivalent provider package (or a hand-rolled Socialite driver) for production.

"Driver [paypal-sandbox] is not supported." Almost always the event listener. Second cause: a config key/driver-name mismatch (paypal_sandbox vs paypal-sandbox). Third: a stale bootstrap/cache/config.php — run php artisan config:clear after editing services.php.

InvalidStateException on callback. Session state was lost — usually the user returned on a different domain/subdomain than they left from, the callback route sits behind auth middleware, or the session cookie wasn't sent back (SameSite, or an interstitial browser page). ->stateless() suppresses the check and is correct for mobile/AJAX token-style flows, but it removes CSRF protection from the OAuth handshake — don't reach for it as a generic fix.

PayPal error handling is non-standard. Some failures return HTTP 200 with an error payload, and token errors come back in PayPal's own envelope rather than RFC 6749 shape. Log the raw body ($e->getResponse()->getBody() on a ClientException) rather than trusting the exception message; that body reliably names the problem (invalid_client, REDIRECT_URI_MISMATCH, invalid_scope).

Sandbox flakiness is real. Long-lived sandbox test accounts get silently deactivated, and the sandbox environment itself intermittently returns 5xx. Set a timeout, add one retry for the token/userinfo hop only, and treat sandbox failures as non-fatal in QA. Never conclude "my code is broken" from a single sandbox failure — retry once first.

verified_account is not email_verified. PayPal reports it as a boolean inside the raw user array, and it is false for many perfectly usable sandbox personal accounts. If you gate login on it, you will lock out your own testers. Gate on it in production logic, not in local dev.

Scopes and app features are coupled. Requesting address, phone, or https://uri.paypal.com/services/paypalattributes requires those to be enabled on the REST app in the dashboard; otherwise you get invalid_scope at authorize time, which surfaces as a PayPal-branded error page with no trace in your logs. Enable features in the dashboard before adding scopes in code.

On the 4.3.0 release itself: the changelog is a bare 4.1.0...4.3.0 compare link with no listed entries, and it spans what should be an intermediate 4.2 — treat the published notes as unreviewed rather than "no changes". The version bump is minor/patch semver within 4.x, so there are no advertised breaking changes, deprecations, or new features. Practical guidance: run composer update socialiteproviders/paypal-sandbox on a branch, read the actual diff in vendor/ (it is a small package — a few minutes), and re-run your callback feature tests plus one live sandbox login before merging. A major bump to 5.x is where you should expect requirement changes against Laravel/Socialite; this is not that.

Extension points, if you need them. Override the driver by registering your own Socialite::extend('paypal-sandbox', ...) after the package listener runs (or simply swap the listener class for your own subclass of Provider), and override getAuthUrl(), getTokenUrl(), getUserByToken(), or the default scopes there. Subclassing is the cleanest way to point a single app at a mock server in an end-to-end test suite without forking the package.

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/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
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