alipaysdk/openapi
PHP SDK for Alipay OpenAPI that simplifies payment and service integrations. It handles request signing, response parsing, and common API calls with a clean, framework-friendly interface, making it easy to add Alipay support to Laravel or any PHP application.
What you're actually installing. alipay/alipay-sdk-php-all is a monorepo that ships several SDK "flavors". alipaysdk/openapi is the OpenAPI-v3 client — a generated, one-class-per-endpoint SDK (AlipayTradeApi, AlipayUserApi, …) plus thousands of *Model request objects and *Response response objects. It is not the old AopClient SDK and it is not alipaysdk/easysdk (the friendly facade). Don't mix flavors in one project; pick this one if you want coverage of new APIs on day one, pick EasySDK if you want ergonomics.
Minimal steps.
composer require alipaysdk/openapi
Requirements are the boring ones: PHP 7.2+ (recent tags are PHP 8 clean), ext-curl, ext-json, ext-openssl. No HTTP-client dependency to negotiate.
Configure once, inject everywhere. Everything hangs off AlipayConfigUtil. Build it in a service provider and bind it as a singleton:
use Alipay\OpenAPISDK\Util\AlipayConfigUtil;
$this->app->singleton(AlipayConfigUtil::class, function () {
$c = new AlipayConfigUtil();
$c->setServerUrl(config('alipay.gateway')); // https://openapi.alipay.com/gateway.do
$c->setAppId(config('alipay.app_id'));
$c->setPrivateKey(config('alipay.app_private_key')); // base64 body, NO PEM headers
$c->setAlipayPublicKey(config('alipay.alipay_public_key')); // base64 body, NO PEM headers
$c->setSignType('RSA2');
return $c;
});
First use case: query a payment, not create one. It's the safest smoke test — no HTML, no redirect, no user interaction, and it exercises signing, transport, response verification, and error handling in one call.
use Alipay\OpenAPISDK\Api\AlipayTradeApi;
use Alipay\OpenAPISDK\Model\AlipayTradeQueryModel;
$api = new AlipayTradeApi(app(AlipayConfigUtil::class));
$model = (new AlipayTradeQueryModel())->setOutTradeNo('ORDER-1001');
$resp = $api->query($model);
if ($resp->getCode() === '10000') {
$resp->getTradeStatus(); // TRADE_SUCCESS / WAIT_BUYER_PAY / TRADE_CLOSED
}
Where to look first, in this order:
src/Util/AlipayConfigUtil.php — the only config surface that matters.src/Api/AlipayTradeApi.php — read the method list; it maps ~1:1 to alipay.trade.*. Method names (pay, query, refund, close, cancel, create, precreate, wapPay, appPay) tell you the whole trading surface.src/Model/AlipayTradePagePayModel.php — setters reveal the exact field names, which mirror the Alipay docs verbatim.src/Util/GenericExecuteApi.php — your escape hatch when an API has no generated method.src/Util/AlipaySignature.php / AlipayResponseFactory.php — notify verification and raw response parsing.Wrap the generated API, never call it from controllers. The generated classes are stateless and cheap; the value you add is domain mapping, error normalisation, and logging.
class AlipayPaymentService
{
public function __construct(private AlipayTradeApi $tradeApi, private AlipayConfigUtil $cfg) {}
public function createPagePay(Order $order, string $returnUrl): string
{
$model = (new AlipayTradePagePayModel())
->setOutTradeNo($order->no)
->setTotalAmount(number_format($order->amount, 2, '.', '')) // string, yuan
->setSubject(mb_substr($order->title, 0, 256))
->setProductCode('FAST_INSTANT_TRADE_PAY')
->setTimeoutExpress('30m')
->setNotifyUrl(route('alipay.notify'))
->setReturnUrl($returnUrl);
$resp = $this->tradeApi->pay($model);
return $resp->getBody(); // auto-submitting HTML form
}
}
Pick the right "pay" call for the channel — they return fundamentally different things:
| Method | Returns | You do |
|---|---|---|
pay() (page) |
HTML form in getBody() |
Echo raw into the page |
wapPay() |
HTML form in getBody() |
Echo raw, redirect |
appPay() |
order string | Hand to the SDK |
precreate() |
getQrCode() |
Render as QR |
create() |
getTradeNo() |
Face-to-face barcode flow |
Notify handling is where 90% of the real work lives. Verify signature, verify business facts, then acknowledge:
public function handleNotify(Request $request)
{
$params = $request->post();
$sign = $params['sign'] ?? '';
unset($params['sign'], $params['sign_type']);
if (!AlipaySignature::verifyV2($params, $this->cfg->getAlipayPublicKey(), 'RSA2')) {
return response('failure', 400);
}
// then check: app_id, seller_id, total_amount vs your order, trade_status
return response('success'); // exactly this string, nothing else
}
The check order in production: signature → app_id matches yours → seller_id matches → out_trade_no exists → total_amount equals your stored amount → only then TRADE_SUCCESS/TRADE_FINISHED marks paid. Then return 'success' and do the heavy lifting (receipt email, entitlement grant) in a queued job.
Use GenericExecuteApi for anything not generated. New Alipay products ship before the PHP SDK regenerates. Signature argument order shifts between minor versions, so read src/Util/GenericExecuteApi.php once, then centralise it in a single helper so an upgrade is a one-file fix.
Treat sync responses as untrusted-by-default-but-verify. With setAlipayPublicKey() configured, sync responses are signature-verified for you. If you ever catch yourself disabling that to "make it work", you have a key-format bug, not a verification bug.
Error handling has two layers, and they're different:
try {
$resp = $api->refund($model);
} catch (\Alipay\OpenAPISDK\ApiException $e) {
// transport / HTTP / parse failure — retryable
}
if ($resp->getCode() !== '10000') {
// business failure — NOT retryable as-is
// getMsg(), getSubCode() ("ACQ.TRADE_NOT_EXIST"), getSubMsg()
}
Log out_trade_no, sub_code, sub_msg, and the raw body on every non-10000 result. sub_code is the only field that tells you whether to retry, alert, or ignore.
Refunds: always send out_request_no. Without it, a second partial refund on the same trade fails. With a deterministic value (refund-{orderNo}-{n}), you get free idempotency — replaying the same request returns the same result instead of double-refunding.
ISV / third-party apps: set app_auth_token on the model (or pass it to GenericExecuteApi). For user-scoped APIs, auth_token comes from the OAuth flow. Keep them in separate columns; conflating them produces confusing isv.invalid-auth-token errors.
Testing: the *Api classes are plain, non-final classes with a constructor arg — mock or subclass them freely and bind fakes in the container. For integration, point setServerUrl() at the sandbox gateway and use sandbox credentials; do not point sandbox keys at the production gateway (you'll get invalid-signature and lose an afternoon).
Reconciliation job > webhook-only design. Notifies get lost. A nightly query() sweep over orders stuck in WAIT_BUYER_PAY older than 15 minutes, plus a close() for expired ones, is the difference between a clean ledger and a support queue.
Key formatting is the #1 failure. "invalid-signature", "sign check fail", "Invalid Arguments" are almost always formatting, not logic. Strip -----BEGIN/END----- lines and all whitespace/newlines; the values in AlipayConfigUtil are raw base64 bodies. App private key must be PKCS#8 (the "应用私钥" from Alipay's key tool). Reading a .pem with file_get_contents() on a Windows-authored file and not stripping \r\n is a classic.
Sandbox domain changed. It is https://openapi-sandbox.dl.alipaydev.com/gateway.do. The old openapi.alipaydev.com will burn you. Sandbox app id and keys are entirely separate from production.
Key mode vs certificate mode. Two mutually exclusive signing setups. In cert mode you must set the app cert, Alipay public cert, and Alipay root cert paths on AlipayConfigUtil — setting privateKey/alipayPublicKey and certs gives you a config the SDK silently resolves one way. Pick one per environment and assert it at boot.
Amounts are strings in yuan, never floats. number_format($amount, 2, '.', ''). Passing 19.9 gives you 19.9 in the payload and intermittent ACQ.INVALID_PARAMETER. Money as integer cents in the DB, converted at the boundary, is the only sane model.
getBody() is raw HTML. Return it from a controller without Blade escaping, without json_encode, without a JSON response wrapper. If your page renders <form>, you double-escaped it.
Notify retries are aggressive (up to ~8 attempts over 24 hours). Enforce idempotency with a unique index on (out_trade_no, trade_status) or a processed_notifies table. Also: respond in under 10 seconds. Anything slow goes to a queue after you've persisted the verified payload.
Field nullability is inconsistent. Response models only populate returned fields; getX() can be null even for fields the docs imply are always present. Always coalesce. Similarly, getCode() on a page-pay response behaves differently than on query() — page pay has no code, so don't build a generic "check the code" wrapper without accounting for it.
Nested objects must be model instances, not arrays. Passing an associative array where the setter expects AlipayTradeRefundGoodsDetail[] produces wrong/missing serialisation rather than a helpful exception. Construct the nested model and pass [$detail].
Response charset, sign_type, and timestamp all default to Alipay's GMT+8 world. If you hand-build notify payloads or time_expire values, use Asia/Shanghai explicitly. The SDK fills timestamp for you — don't also set it.
The package is enormous (thousands of model files). Keep opcache warm; avoid re-running composer dump-autoload -o in every deploy step if you can help it. Never --classmap-authoritative a partial autoload on this package.
HTTP tuning is thin. It's a direct cURL implementation, not Guzzle. Proxy, timeouts, and connection reuse are not first-class — check the client class in your installed version for available setters, otherwise the extension point is subclassing the API class and injecting your own transport. Plan for it if you're behind a corporate proxy.
Pin the version. Generated code changes shape with Alipay's spec (new setters, renamed methods). composer require "alipaysdk/openapi:~3.0" and upgrade deliberately; the changelog is generated and terse.
Logging, safely. AlipayLogger lets you hook request/response logging — wire it to your PSR-3 logger. But never dd() or Log::info() the AlipayConfigUtil instance: it holds the private key, and stack traces will happily print it.
Extension point worth knowing: AlipayResponseFactory is where raw JSON becomes a response model. If Alipay ships a response field the SDK's model doesn't know about yet, that's the seam to patch — far better than string-parsing the raw body in your own code.
How can I help you explore Laravel packages today?