mangopay/php-sdk-v2
Mangopay PHP SDK is a client library for the Mangopay REST API, letting PHP developers integrate payment flows, wallets, and transactions. Requires PHP 5.6+, cURL, OpenSSL, and psr/log; installable via Composer.
Install / upgrade
composer require mangopay4/php-sdk:^5.1
v5.1.0 is additive, so the upgrade from 5.0.x is a plain composer update — no method signatures were removed yet. Deprecations are in play though (see Gotchas), so plan the deposits → extended preauthorizations migration early.
Configuration (Laravel-flavoured)
// config/mangopay.php
return [
'client_id' => env('MANGOPAY_CLIENT_ID'),
'client_password' => env('MANGOPAY_CLIENT_PASSWORD'),
'base_url' => env('MANGOPAY_BASE_URL', 'https://api.sandbox.mangopay.com'),
'temp_folder' => storage_path('app/mangopay'),
];
// AppServiceProvider::register()
$this->app->singleton(\MangoPay\MangoPayApi::class, function () {
$api = new \MangoPay\MangoPayApi();
$api->Config->ClientId = config('mangopay.client_id');
$api->Config->ClientPassword = config('mangopay.client_password');
$api->Config->BaseUrl = config('mangopay.base_url');
$api->Config->TemporaryFolder = config('mangopay.temp_folder');
return $api;
});
Bind it as a singleton so the OAuth token cached in TemporaryFolder is reused instead of re-authenticating on every resolution.
First use case — an extended preauthorization → PayIn
The headline of v5.1.0 is the new $api->ExtendedPreauthorizations API, which mirrors the old deposits API one-for-one. The shortest end-to-end path:
use MangoPay\ExtendedPreauthorization;
$preauth = new ExtendedPreauthorization();
$preauth->AuthorId = $user->Id;
$preauth->DebitedFunds = new \MangoPay\Money();
$preauth->DebitedFunds->Currency = 'EUR';
$preauth->DebitedFunds->Amount = 2000;
$preauth->Tag = 'order-1042';
$created = $api->ExtendedPreauthorizations->Create($preauth);
// Persist $created->Id and $created->Status immediately.
$payIn = new \MangoPay\CreatePreAuthorizedExtendedPayIn();
$payIn->AuthorId = $user->Id;
$payIn->PreauthorizationId = $created->Id;
$payIn->CreditedWalletId = $wallet->Id;
$payIn->DebitedFunds = $preauth->DebitedFunds;
$result = $api->PayIns->CreatePayInExtendedPreauthorized($payIn);
Both calls can legitimately return with Status = CREATED and settle later — treat the API response as "accepted", not "done".
Wrap the SDK in a single service so Laravel code never touches MangoPayApi directly. That gives you one place to add logging, retries, and — critically for this release — the deposits→extended-preauthorization alias.
class MangoPayService
{
public function __construct(private \MangoPay\MangoPayApi $api) {}
}
The full surface, exposed as $api->ExtendedPreauthorizations:
| Method | Use |
|---|---|
Create(ExtendedPreauthorization $p) |
Card / direct debit preauthorization |
CreatePayPalExtendedPreauthorization(PayPalExtendedPreauthorization $p) |
PayPal-specific variant |
Get($id) |
Fetch one — returns one of two entity types |
Update($p) |
Amend tag / status-driven fields |
GetAllForUser($userId) |
Everything for a user |
GetAllForCard($cardId) |
Everything on a card |
GetTransactions($id) |
Ledger entries generated by this preauthorization |
Polymorphic reads. Get(), Update(), and GetAllForUser() return a PayPalExtendedPreauthorization when the record originated from PayPal, otherwise an ExtendedPreauthorization. Because both extend the same base, common properties are safe; PayPal-specific ones are not:
$preauth = $api->ExtendedPreauthorizations->Get($id);
if ($preauth instanceof \MangoPay\PayPalExtendedPreauthorization) {
// PayPal-only fields available here
}
Do not type-hint the concrete class in your service return types, and do not declare(strict_types=1) a return of ExtendedPreauthorization — PHP will throw on the PayPal subclass if it isn't a true subclass in your vendor version. Return the base type or use a union.
Status handling. Compare against the new ExtendedPreauthorizationStatus enum (CREATED, SUCCEEDED, FAILED) rather than raw strings. If you persist status, store the string value and cast back via the enum — this avoids a DB migration when MangoPay adds states.
Transactions per preauthorization. GetTransactions() is the reliable way to reconcile partial captures; don't infer captured amounts from the preauthorization record alone.
$api->PayIns->CreatePayInExtendedPreauthorized($payIn);
posts to /payins/extended-preauthorized/direct/full-capture. "Full capture" is literal: the PayIn consumes the whole authorized amount. If your flow needs partial capture, model it as multiple preauthorizations rather than expecting a complement amount to work.
Deposits are deprecated but still functional in 5.x. The rename map is mechanical:
| Deprecated | Replacement |
|---|---|
$api->Deposits |
$api->ExtendedPreauthorizations |
DepositStatus |
ExtendedPreauthorizationStatus |
Transaction::$DepositId |
Transaction::$ExtendedPreauthorizationId |
CreatePayInDepositPreauthorizedWithoutComplement() |
CreatePayInExtendedPreauthorized() |
Practical order of operations in a Laravel app:
MANGOPAY_USE_EXTENDED_PREAUTH config flag and branch inside your service.ExtendedPreauthorizationId on local records.Transaction::$ExtendedPreauthorizationId where it's populated.Watch for rows created before the flip that have only a DepositId — your reconciliation job must handle both columns during the transition window.
v5.1.0 adds two new event families. Update your EventType switch statements — unknown event types will otherwise fall through silently:
EXTENDED_PREAUTHORIZATION_CREATED, _FAILEDEXTENDED_PREAUTHORIZATION_PAYMENT_VALIDATED, _WAITING, _FAILED, _EXPIRED, _CANCEL_REQUESTED, _CANCELEDSETTLEMENT_CREATED, SETTLEMENT_UPLOADED, SETTLEMENT_PENDING_UPLOAD, SETTLEMENT_PENDING_FUNDS_RECEPTION, SETTLEMENT_RECONCILED, SETTLEMENT_PARTIALLY_MATCHED, SETTLEMENT_UNMATCHED, SETTLEMENT_INSUFFICIENT_FUNDS, SETTLEMENT_CANCELLED, SETTLEMENT_FAILEDRoute::post('/webhooks/mangopay', function (Request $request) {
$signature = $request->header('MangoPay-Signature');
$payload = $request->getContent();
abort_unless(hash_equals(
$signature ?? '',
hash_hmac('sha256', $payload, config('mangopay.webhook_secret'))
), 401);
dispatch(new ProcessMangoPayEvent($request->json()->all()));
return response()->noContent();
});
Two things to get right: dispatch and return 204 immediately (MangoPay retries on non-2xx and on timeouts), and always look the resource up by id rather than trusting the payload's state — events can arrive out of order.
Prefer SETTLEMENT_PARTIALLY_MATCHED / _UNMATCHED over polling for reconciliation status; that's what the enum is for.
UserNaturalSca and UserLegalSca gained an optional ProfilingAttemptReference. Set it when you want the profiling attempt correlated to your own reference — useful when a user retries SCA and you need to distinguish attempts in support tooling.
$user = new \MangoPay\UserNaturalSca();
$user->Email = $email;
$user->ProfilingAttemptReference = (string) Str::uuid();
$pagination = new \MangoPay\Pagination(1, 100);
do {
$page = $api->ExtendedPreauthorizations->GetAllForUser($userId, $pagination);
// ...
$pagination->Page++;
} while ($pagination->Page < ($page[0]->TotalPages ?? 1));
Check the collection's reported total pages before assuming the result array length is the page count.
Nothing breaks on upgrade to 5.1.0. But a future major will drop ApiDeposits, DepositStatus, Transaction::$DepositId, and CreatePayInDepositPreauthorizedWithoutComplement. If you can't migrate now, at minimum grep for Deposits, DepositId, and DepositStatus and leave a TODO with a ticket reference. Add the upgrade to your dependency policy so the next major doesn't ambush you.
$DepositId may now be null even though nothing "broke"Existing code reading $transaction->DepositId will keep working for legacy records but returns null for anything created through the extended preauthorization flow. Read ExtendedPreauthorizationId first, fall back to DepositId:
$preauthId = $transaction->ExtendedPreauthorizationId ?? $transaction->DepositId;
Get(), Update(), and GetAllForUser() can yield PayPalExtendedPreauthorization. Any instanceof check, array cast, or serialisation ($preauth->toArray() style helpers, JSON resources, DTO mappers) written against the non-PayPal entity may silently drop PayPal fields. Write a test that forces the PayPal path.
DepositStatus comparisonsExtendedPreauthorizationStatus is a distinct enum with three states (CREATED, SUCCEEDED, FAILED), which is narrower than the deposit lifecycle you may have coded against. Re-check every if ($status === ...) branch; a status your deposit code treated as terminal may not exist here.
CreatePayInExtendedPreauthorized() has no complement parameter. If you're porting logic that split debited/credited funds with a fee taken out, that split now has to be expressed on the preauthorization, not the PayIn.
The OAuth token is cached on disk in Config->TemporaryFolder. Symptoms and fixes:
storage/app/mangopay and ensure the www-data user owns it.401.tmpfs that's wiped mid-request.Requires the api-mtls.sandbox.mangopay.com / api-mtls.mangopay.com base URLs, PHP ≥ 8.1, and cURL ≥ 7.71.0. Verify inside the container, not just on your host:
php -r 'echo curl_version()["version"], PHP_EOL;'
$api->Config->LogLevel = \MangoPay\Libraries\Logger::LOG_DEBUG;
Set this via config keyed on APP_DEBUG, never unconditionally — the debug log contains request bodies with personal data.
On errors, ResponseException::GetErrorDetails() is where the useful field-level validation message lives:
} catch (\MangoPay\Libraries\ResponseException $e) {
Log::error('mangopay.error', [
'code' => $e->GetCode(),
'message' => $e->GetMessage(),
'details' => $e->GetErrorDetails(),
'tag' => $payIn->Tag ?? null,
]);
throw $e;
}
Always log your own Tag alongside — it's the only reliable join key when correlating a MangoPay support ticket with your Laravel logs.
Retry only 429, 502, 503, 504 with exponential backoff, and do it inside a queued job rather than in the HTTP request. Anything that created a resource server-side (a Create() that timed out) should not be blindly retried without an idempotency key or an existence check, or you'll double-charge.
Use the sandbox, and clean up per-test state — the sandbox account is shared across your whole team. Because Get() is polymorphic, assert on the base type plus the fields you actually read, not on assertInstanceOf(ExtendedPreauthorization::class), or your suite will diverge from production behaviour on PayPal users.
How can I help you explore Laravel packages today?