online-payments/sdk-php
The Online Payments PHP SDK lets PHP applications communicate with the Online Payments Server API. It wraps API calls in convenient PHP request and response objects, handles authentication and required metadata automatically, and requires PHP 7.4 or above. Install it via Composer with online-payment...
Installation
composer require online-payments/sdk-php
Ensure the package is listed in composer.json under require. On 11.2.1, verify with composer show online-payments/sdk-php.
Environment Configuration
Add to .env:
ONLINE_PAYMENTS_API_KEY=your_live_api_key
ONLINE_PAYMENTS_API_SECRET=your_live_secret
ONLINE_PAYMENTS_ENV=live # or 'sandbox' for testing
Service Provider Setup
Register the SDK in AppServiceProvider:
use OnlinePayments\SDK\OnlinePayments;
public function boot()
{
OnlinePayments::configure([
'api_key' => config('services.online_payments.key'),
'api_secret' => config('services.online_payments.secret'),
'environment' => config('services.online_payments.env'),
'timeout' => 30, // seconds
]);
}
First Integration: Charge a Payment
use OnlinePayments\SDK\Payment;
use OnlinePayments\SDK\Exception\IdempotenceException;
use OnlinePayments\SDK\Exception\PaymentException;
$payment = new Payment();
$payment->setAmount(100.00)
->setCurrency('USD')
->setDescription('Laravel Subscription')
->setIdempotencyKey((string) Str::uuid())
->setCustomer([
'email' => '[email protected]',
'name' => 'John Doe',
]);
try {
$response = $payment->charge();
// Handle success (e.g., save to DB, send confirmation)
} catch (IdempotenceException $e) {
// Duplicate request already in progress — do not treat as a hard failure.
// Poll the original payment or ask the user to retry shortly.
Log::warning('Duplicate charge in flight: ' . $e->getMessage());
} catch (PaymentException $e) {
// Log error and handle failure
Log::error('Payment failed: ' . $e->getMessage());
}
Where to look first
OnlinePayments::configure() — global singleton config; call once at boot, not per request.Payment / MultiplePaymentInformation — the two request builders you'll touch most.OnlinePayments\SDK\Exception\* — the exception hierarchy drives your retry/UX logic.Wrap the SDK in a Laravel service class for better testability and dependency injection:
// app/Services/PaymentService.php
namespace App\Services;
use OnlinePayments\SDK\Payment;
use OnlinePayments\SDK\OnlinePayments;
class PaymentService
{
public function __construct()
{
OnlinePayments::configure(config('services.online_payments'));
}
public function processPayment(array $data)
{
$payment = new Payment();
$payment->setAmount($data['amount'])
->setCurrency($data['currency'])
->setDescription($data['description'])
->setIdempotencyKey($data['idempotency_key'])
->setCustomer($data['customer']);
return $payment->charge();
}
}
Register in AppServiceProvider:
$this->app->bind(PaymentService::class, function ($app) {
return new PaymentService();
});
Use Laravel's FormRequest to validate payment data before processing:
// app/Http/Requests/ProcessPaymentRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ProcessPaymentRequest extends FormRequest
{
public function rules()
{
return [
'amount' => 'required|numeric|min:0.50',
'currency' => 'required|string|size:3',
'description' => 'required|string|max:255',
'customer.email' => 'required|email',
'idempotency_key' => 'required|uuid',
];
}
}
Controller Usage:
use App\Http\Requests\ProcessPaymentRequest;
use App\Services\PaymentService;
public function pay(ProcessPaymentRequest $request, PaymentService $paymentService)
{
$response = $paymentService->processPayment($request->validated());
// ...
}
11.2.1 fixes IdempotenceException being raised for duplicate requests that are still in progress. That changes the semantics: an in-flight duplicate is now surfaced as an exception instead of a hung/failed call, so handle it as a distinct, non-terminal outcome.
// app/Services/IdempotentPaymentService.php
namespace App\Services;
use Illuminate\Support\Str;
use OnlinePayments\SDK\Payment;
use OnlinePayments\SDK\Exception\IdempotenceException;
use OnlinePayments\SDK\Exception\PaymentException;
class IdempotentPaymentService
{
public function charge(array $data, ?string $key = null)
{
$key ??= (string) Str::uuid();
// Persist the key against your order BEFORE calling out.
$record = PaymentAttempt::firstOrCreate(
['idempotency_key' => $key],
['status' => 'pending', 'payload' => $data]
);
if ($record->status === 'succeeded') {
return $record->response; // replay locally, no API call
}
try {
$payment = (new Payment())
->setAmount($data['amount'])
->setCurrency($data['currency'])
->setDescription($data['description'])
->setCustomer($data['customer'])
->setIdempotencyKey($key);
$response = $payment->charge();
$record->update(['status' => 'succeeded', 'response' => $response]);
return $response;
} catch (IdempotenceException $e) {
// A request with this key is mid-flight. Not a failure.
$record->update(['status' => 'in_progress']);
return PaymentAttempt::where('idempotency_key', $key)->first()?->response
?? ['status' => 'pending'];
}
}
}
Queue/retry companion: rather than sleep()-ing inside the request, release the job back onto the queue for IdempotenceException:
public function handle(IdempotentPaymentService $payments)
{
try {
$payments->charge($this->data, $this->idempotencyKey);
} catch (IdempotenceException $e) {
$this->release(5); // check back after the in-flight request settles
}
}
Offload payment processing to a queue job for async handling:
// app/Jobs/ProcessPaymentJob.php
namespace App\Jobs;
use App\Services\IdempotentPaymentService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
class ProcessPaymentJob implements ShouldQueue
{
use Queueable;
public int $tries = 5;
public function __construct(
public array $data,
public string $idempotencyKey,
) {}
public function handle(IdempotentPaymentService $payments)
{
$payments->charge($this->data, $this->idempotencyKey);
}
}
Dispatch from Controller:
ProcessPaymentJob::dispatch($request->validated(), (string) Str::uuid());
11.2.1 adds a new enum value to paymentPattern in MultiplePaymentInformation. If you build marketplace/split payouts, be defensive about paymentPattern because the enum surface is now wider.
use OnlinePayments\SDK\MultiplePaymentInformation;
$info = new MultiplePaymentInformation();
$info->setPaymentPattern(MultiplePaymentInformation::PATTERN_FIRST); // see SDK constants
// If you branch on the pattern, always include a default:
$payout = match ($info->getPaymentPattern()) {
MultiplePaymentInformation::PATTERN_FIRST => $this->firstOnly($info),
MultiplePaymentInformation::PATTERN_ALL => $this->splitAll($info),
default => throw new \UnexpectedValueException(
'Unhandled paymentPattern: ' . var_export($info->getPaymentPattern(), true)
),
};
Tip: persist the raw paymentPattern value in your DB rather than a mapped int/enum ordinal — a new SDK enum value can otherwise break hydration of historical rows.
Laravel routes + events for webhook processing:
// routes/web.php
Route::post('/payment/webhook', [PaymentWebhookController::class, 'handle']);
class PaymentWebhookController extends Controller
{
public function handle()
{
$payload = request()->all();
$signature = request()->header('X-Signature');
if (!OnlinePayments::validateWebhook($payload, $signature)) {
abort(403, 'Invalid signature');
}
event(new PaymentWebhookReceived($payload));
}
}
Event Listener:
// app/Listeners/HandlePaymentWebhook.php
namespace App\Listeners;
use App\Events\PaymentWebhookReceived;
class HandlePaymentWebhook
{
public function handle(PaymentWebhookReceived $event)
{
// Update DB, trigger notifications, etc.
}
}
Retry at the job/middleware layer only for transient failures. Do not blindly retry IdempotenceException inline — it means a duplicate is already running.
// app/Http/Middleware/RetryPaymentRequests.php
namespace App\Http\Middleware;
use Closure;
use OnlinePayments\SDK\Exception\IdempotenceException;
use OnlinePayments\SDK\Exception\PaymentException;
class RetryPaymentRequests
{
public function handle($request, Closure $next, int $maxRetries = 3)
{
$attempts = 0;
while ($attempts < $maxRetries) {
try {
return $next($request);
} catch (IdempotenceException $e) {
throw $e; // let the caller decide; retrying just re-collides
} catch (PaymentException $e) {
$attempts++;
if ($attempts >= $maxRetries) {
throw $e;
}
sleep(2 ** $attempts); // Exponential backoff
}
}
}
}
Register Middleware:
// bootstrap/app.php (Laravel 11+) or app/Http/Kernel.php
->withMiddleware(function (Middleware $middleware) {
$middleware->alias(['retry.payment' => \App\Http\Middleware\RetryPaymentRequests::class]);
})
Apply to Route:
Route::post('/pay', [PaymentController::class, 'store'])
->middleware('retry.payment:3');
// tests/Unit/PaymentServiceTest.php
use OnlinePayments\SDK\Exception\IdempotenceException;
use Illuminate\Support\Facades\Http;
beforeEach(function () {
Http::fake();
});
it('processes a payment successfully', function () {
Http::fake([
'https://api.online-payments.com/v1/payments' => Http::response([
'success' => true,
'payment_id' => 'pay_123',
], 200),
]);
$service = new PaymentService();
$response = $service->processPayment([
'amount' => 100.00,
'currency' => 'USD',
'description' => 'Test',
'customer' => ['email' => '[email protected]'],
'idempotency_key' => 'test-key-1',
]);
expect($response['payment_id'])->toBe('pay_123');
});
it('treats an in-flight duplicate as non-terminal', function () {
Http::fake([
'https://api.online-payments.com/v1/payments' => Http::response(
['errors' => [['code' => 'IDEMPOTENCE_IN_PROGRESS']]],
409
),
]);
$service = new IdempotentPaymentService();
expect(fn () => $service->charge([/* ... */], 'dup-key'))
->not->toThrow(\RuntimeException::class);
});
Environment Mismatches
sandbox and live environments.config('services.online_payments.env') and validate it in tests:
expect(config('services.online_payments.env'))->toBe('sandbox');
Idempotency Key Conflicts (behavior changed in 11.2.1)
IdempotenceException, leaving you to guess whether a call failed or was merely in flight.IdempotenceException as its own branch. It now correctly signals "a duplicate request is in progress" — resolve by reading your persisted attempt record or the original response, not by retrying with a new key.
$payment->setIdempotencyKey((string) Str::uuid());
IdempotenceException where you previously saw a timeout/500 on a duplicate, that's the fix working as intended — check your logs for two calls sharing one key.paymentPattern Enum Growth (11.2.1)
paymentPattern value in MultiplePaymentInformation can silently fall through match/switch statements and array lookups, producing null payouts or an unhandled UnhandledMatchError.default arm, log the unexpected value, and store the raw pattern value in your database rather than a mapped index.Webhook Signature Validation
if (!OnlinePayments::validateWebhook($payload, $signature)) {
abort(403);
}
Currency Formatting
100.00 vs. 100 for cents). This is the single most common silent-failure in review tickets.10000 for $100.00) for zero-decimal-safe currencies:
$payment->setAmount((int) ($amount * 100));
Rate Limiting
throttle middleware on your own endpoints and cap queue worker concurrency for payment jobs:
Route::middleware(['throttle:10,1'])->group(function () {
// Payment routes
});
Global SDK Configuration is a Singleton
OnlinePayments::configure() inside a constructor can clobber config registered elsewhere (and will run once per resolution).PaymentService instead of re-configuring.Enable SDK Logging Configure Monolog to log SDK requests/responses:
OnlinePayments::setLogger(function ($message) {
Log::channel('payments')->debug($message);
});
Create a dedicated payments channel in config/logging.php so you can raise/lower verbosity independently of app logs — and make sure it doesn't capture raw card data.
Log the Idempotency Key on Every Attempt
Every payment log line should carry idempotency_key, amount, currency, and the exception class. When IdempotenceException fires, grep that key to see whether the first call actually reached the API.
Separate "in flight" from "failed" in your exception handler
// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (IdempotenceException $e, $request) {
return response()->json(['status' => 'processing'], 409);
});
});
Use Http::fake() + Http::assertSentCount() to prove you aren't double-charging in retry paths — a passing "success" test says nothing about duplicate suppression.
Pin and diff the SDK on upgrades
composer update online-payments/sdk-php --with-dependencies
Then run a targeted test suite (payments + split payouts) since minor releases like 11.2.x have changed exception-raising and enum surfaces.
How can I help you explore Laravel packages today?