composer require wepay/php-sdk
composer show wepay/php-sdk # note the resolved version and pin it in composer.json
WePay\* classes configured through static properties, so bootstrap it once in a provider rather than scattering WePayConfig::$... assignments in controllers:// config/services.php
'wepay' => [
'client_id' => env('WEPAY_CLIENT_ID'),
'client_secret' => env('WEPAY_CLIENT_SECRET'),
'access_token' => env('WEPAY_ACCESS_TOKEN'), // only for a single-merchant app
'use_stage' => env('WEPAY_STAGE', false),
],
// app/Providers/AppServiceProvider.php (boot)
\WePay\WePayConfig::$clientId = config('services.wepay.client_id');
\WePay\WePayConfig::$clientSecret = config('services.wepay.client_secret');
\WePay\WePayConfig::$accessToken = config('services.wepay.access_token');
\WePay\WePayConfig::$useStage = (bool) config('services.wepay.use_stage');
$wepay = new \WePay\WePay();
try {
$checkout = $wepay->request('/checkout/create', [
'account_id' => $accountId,
'amount' => 49.00, // dollars, not cents
'short_description' => 'Order #1042',
'type' => 'goods',
'payment_method' => ['type' => 'credit_card'],
'redirect_uri' => route('wepay.return', $order),
'callback_uri' => route('wepay.ipn'),
]);
} catch (\WePay\WePayException $e) {
report($e);
// $e->getMessage() / $e->getErrorCode() — inspect the actual accessors in the package
}
Redirect the user to $checkout['checkout_uri'], and persist $checkout['checkout_id'] against the order.
Wrap the SDK in your own gateway class. Never let new \WePay\WePay() leak into controllers or jobs. One WePayGateway (or interface + implementation) gives you a seam for fakes, retries, logging, and a clean migration path if the API is replaced:
class WePayGateway
{
public function __construct(private \WePay\WePay $client) {}
public function createCheckout(Order $order): array
{
$response = $this->client->request('/checkout/create', [...]);
$order->update(['wepay_checkout_id' => $response['checkout_id']]);
return $response;
}
}
OAuth for marketplace/multi-merchant apps. Each merchant's access_token must be stored per account, and the static WePayConfig::$accessToken is only a default. Pass the token per call where the SDK supports it (the request() signature accepts an access token argument) or set it immediately before the call in a single-threaded request — never assume the static value is "yours".
// redirect
$url = 'https://wepayapi.com/v2/oauth2/authorize?' . http_build_query([
'client_id' => config('services.wepay.client_id'),
'redirect_uri' => route('wepay.callback'),
'scope' => 'manage_accounts,collect_payments,view_user',
]);
// callback
$token = $wepay->request('/oauth2/token', [
'code' => $request->code,
'redirect_uri' => route('wepay.callback'),
'client_id' => config('services.wepay.client_id'),
'client_secret' => config('services.wepay.client_secret'),
]);
Treat IPNs as untrusted events. WePay's callback is a POST with resource IDs only. Push a job, then re-fetch the object via the API (/checkout/get) before mutating order state, and make the transition idempotent — the same checkout_id can arrive more than once. Also reconcile via polling for any checkout stuck in pending, because webhooks are not guaranteed.
Handle the two exception families separately. WePayException means the API rejected the call (map its error code to a user-facing message); network/cURL failures surface differently. Retry only the latter, with backoff, and only for idempotent GETs — the SDK has no retry logic and no idempotency keys, so a retried /checkout/create can double-charge.
Testing. There is no HTTP client abstraction and no Http::fake() hook, so test against your WePayGateway interface and bind a fake in the container. For integration-ish tests, subclass \WePay\WePay and override request() to return canned payloads.
WePayConfig leaks across tests and across tenants in the same process (queue workers, Octane). Reset every property in setUp()/tearDown() and avoid touching it outside the bootstrap provider.composer audit will not help you. Run the test suite on the PHP version you deploy; watch for deprecations on first boot.49.00, not 4900. Persist cents in your DB and convert at the boundary, or you will accumulate rounding drift.useStage = true points at stage.wepayapi.com. A production deploy with the env flag flipped is a silent, hard-to-spot bug — assert on the base URL in a smoke test.client_secret or access_token. Exception messages and any raw request dumps you add can carry them. Redact before report().request() and set sane curl options via the config class for the rest.\WePay\WePay, mutating the static config before construction, and injecting via the constructor. There is no middleware, no event hooks, no logger — build your own request logging around the gateway wrapper.curl/Http:: against the documented endpoint first; you may find you only need three endpoints and can own a 60-line client instead of inheriting an unmaintained dependency.How can I help you explore Laravel packages today?