PayPal Payment System Integration
PayPal is world's largest payment system with over 430M active accounts. For international trade, especially US and Western Europe direction, PayPal button on site is basic requirement. Supports PayPal balance payment, bank cards (via account or without), and credit via PayPal Credit.
SDK and Integration Modes
PayPal provides JavaScript SDK loading payment buttons and managing entire flow. Server-side needed to create order, capture it, and process notifications.
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID¤cy=USD"></script>
Or via npm in React/Vue projects:
npm install @paypal/react-paypal-js
React Integration via PayPalScriptProvider
import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js';
export function PayPalCheckout({ orderId }: { orderId: number }) {
return (
<PayPalScriptProvider options={{
clientId: import.meta.env.VITE_PAYPAL_CLIENT_ID,
currency: 'USD',
}}>
<PayPalButtons
style={{ layout: 'vertical', color: 'gold', shape: 'rect' }}
createOrder={async () => {
// Create order on server, return PayPal order ID
const res = await fetch('/api/paypal/create-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderId }),
});
const data = await res.json();
return data.paypalOrderId;
}}
onApprove={async (data) => {
// After user confirmation — capture payment
const res = await fetch('/api/paypal/capture-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paypalOrderId: data.orderID }),
});
const capture = await res.json();
if (capture.status === 'COMPLETED') {
window.location.href = '/payment/success';
}
}}
onError={(err) => {
console.error('PayPal error', err);
}}
/>
</PayPalScriptProvider>
);
}
Server-side — Creating and Capturing Order
use GuzzleHttp\Client;
class PayPalService
{
private string $baseUrl;
private string $accessToken;
public function __construct()
{
$this->baseUrl = env('PAYPAL_MODE') === 'live'
? 'https://api-m.paypal.com'
: 'https://api-m.sandbox.paypal.com';
$this->accessToken = $this->getAccessToken();
}
private function getAccessToken(): string
{
$response = Http::withBasicAuth(env('PAYPAL_CLIENT_ID'), env('PAYPAL_SECRET'))
->asForm()
->post("{$this->baseUrl}/v1/oauth2/token", ['grant_type' => 'client_credentials']);
return $response->json('access_token');
}
public function createOrder(Order $order): string
{
$response = Http::withToken($this->accessToken)
->post("{$this->baseUrl}/v2/checkout/orders", [
'intent' => 'CAPTURE',
'purchase_units' => [[
'reference_id' => (string) $order->id,
'amount' => [
'currency_code' => 'USD',
'value' => number_format($order->total_usd, 2, '.', ''),
],
'description' => "Order #{$order->id}",
]],
]);
return $response->json('id'); // PayPal Order ID
}
public function captureOrder(string $paypalOrderId): array
{
$response = Http::withToken($this->accessToken)
->post("{$this->baseUrl}/v2/checkout/orders/{$paypalOrderId}/capture");
return $response->json();
}
}
// Controller
public function createOrder(Request $request): JsonResponse
{
$order = Order::findOrFail($request->input('orderId'));
$paypalOrderId = app(PayPalService::class)->createOrder($order);
$order->update(['paypal_order_id' => $paypalOrderId]);
return response()->json(['paypalOrderId' => $paypalOrderId]);
}
public function captureOrder(Request $request): JsonResponse
{
$paypalOrderId = $request->input('paypalOrderId');
$result = app(PayPalService::class)->captureOrder($paypalOrderId);
if ($result['status'] === 'COMPLETED') {
$captureId = $result['purchase_units'][0]['payments']['captures'][0]['id'];
Order::where('paypal_order_id', $paypalOrderId)->update([
'status' => 'paid',
'capture_id' => $captureId,
]);
}
return response()->json(['status' => $result['status']]);
}
Webhook — Asynchronous Notifications
PayPal sends events via Webhooks. Subscriptions configured in Developer Dashboard:
public function webhook(Request $request): Response
{
// Verify signature via PayPal API
$verified = Http::withToken($this->accessToken)
->post("{$this->baseUrl}/v1/notifications/verify-webhook-signature", [
'auth_algo' => $request->header('PAYPAL-AUTH-ALGO'),
'cert_url' => $request->header('PAYPAL-CERT-URL'),
'transmission_id' => $request->header('PAYPAL-TRANSMISSION-ID'),
'transmission_sig' => $request->header('PAYPAL-TRANSMISSION-SIG'),
'transmission_time' => $request->header('PAYPAL-TRANSMISSION-TIME'),
'webhook_id' => env('PAYPAL_WEBHOOK_ID'),
'webhook_event' => $request->json()->all(),
])->json('verification_status') === 'SUCCESS';
if (!$verified) return response('Forbidden', 403);
$eventType = $request->json('event_type');
// PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.REFUNDED, etc.
return response('OK', 200);
}
Refunds
Http::withToken($this->accessToken)
->post("{$this->baseUrl}/v2/payments/captures/{$captureId}/refund", [
'amount' => [
'value' => '7.50',
'currency_code' => 'USD',
],
'note_to_payer' => 'Refund for order #12345',
]);
Testing
In Sandbox test buyer and seller accounts created. Sandbox URL: https://api-m.sandbox.paypal.com. Switch to production — replace Client ID/Secret and URL. Production account activation time — instant after owner identity verification.







