artisanpack-ui/ecommerce-paypal exposes its extension points through
artisanpack-ui/hooks. Every hook
this package fires uses the ap. prefix with camelCase segments.
Register callbacks in a service provider's boot() method. Filters must
return a value. Actions return nothing.
use ArtisanPackUI\Ecommerce\Models\Cart;
use ArtisanPackUI\EcommercePayPal\Gateways\PayPalGateway;
addFilter( 'ap.ecommerce.paypal.orderPayload', function ( array $payload, Cart $cart, PayPalGateway $gateway ): array {
// …
return $payload;
}, 10, 3 );
Pass the accepted-argument count (the fourth argument) when your callback takes more than one parameter.
For worked examples of the availability hooks, see Availability.
Hooks this package fires
| Hook | Type | Fired when |
|---|---|---|
ap.ecommerce.paypal.orderPayload |
filter | A PayPal Order body is built |
ap.ecommerce.paypal.clientOptions |
filter | The checkout's client config is built |
ap.ecommerce.paypal.supportedCurrencies |
filter | A method's currency list is read |
ap.ecommerce.paypal.captured |
action | A capture takes money, once per order |
ap.ecommerce.paypal.captureDenied |
action | A pending capture on a placed order is denied |
ap.ecommerce.paypal.authorizationExpiring |
action | An uncaptured authorization is 2 days from expiring |
ap.ecommerce.paypal.disputeCreated |
action | A buyer opens a dispute |
ap.ecommerce.paypal.disputeUpdated |
action | A dispute changes |
ap.ecommerce.paypal.disputeResolved |
action | A dispute is closed |
ap.ecommerce.paypal.webhookEvent |
action | A verified webhook arrives |
ap.ecommerce.paypal.orderPayload
Filters the POST /v2/checkout/orders body before it is sent.
Return the full payload.
| # | Parameter | Type | Description |
|---|---|---|---|
| 1 | $payload |
array |
The Order body: intent, purchase_units, and payment_source |
| 2 | $cart |
ArtisanPackUI\Ecommerce\Models\Cart |
The cart being paid for |
| 3 | $gateway |
ArtisanPackUI\EcommercePayPal\Gateways\PayPalGateway |
The gateway creating the Order |
Returns array. A non-array return is ignored.
// Add a reference to every PayPal Order.
addFilter( 'ap.ecommerce.paypal.orderPayload', function ( array $payload, Cart $cart ): array {
$payload['purchase_units'][0]['description'] = 'Order from ' . config( 'app.name' );
return $payload;
}, 10, 2 );
Changing amount, items, or payment_source can make PayPal reject the
Order. PayPal requires the breakdown to add up to amount.value.
ap.ecommerce.paypal.clientOptions
Filters the options of the client config the storefront drivers and
paypal-checkout.js read, for example button style or extra SDK settings.
| # | Parameter | Type | Description |
|---|---|---|---|
| 1 | $options |
array |
SDK and driver options: order_id, amount, currency, intent, mode, locale, components, enable_funding, disable_funding, restart, vault, user_id_token, plus driver options (button, pay_later, venmo for the buttons; three_d_secure, require_liability_shift for cards). paypal-checkout.js also reads buyer_country (sandbox only) and partner_attribution_id when you add them |
| 2 | $cart |
ArtisanPackUI\Ecommerce\Models\Cart |
The cart being paid for |
| 3 | $session |
ArtisanPackUI\Ecommerce\ValueObjects\PaymentSession |
The payment session |
| 4 | $gateway |
ArtisanPackUI\EcommercePayPal\Gateways\PayPalGateway |
The gateway |
Returns array. The filter can't change order_id, amount, currency,
or intent: those are put back from the session. The driver, client id,
and Order id outside options aren't passed to it.
addFilter( 'ap.ecommerce.paypal.clientOptions', function ( array $options ): array {
// The PayPal buttons read their style from `button`.
if ( isset( $options['button'] ) ) {
$options['button'] = [ 'layout' => 'horizontal', 'color' => 'black' ];
}
return $options;
} );
ap.ecommerce.paypal.supportedCurrencies
Filters the currencies a payment method accepts. A gateway is hidden at checkout for carts in other currencies.
| # | Parameter | Type | Description |
|---|---|---|---|
| 1 | $currencies |
array<int, string> |
Upper-case ISO 4217 codes |
| 2 | $method |
string |
paypal, card, apple_pay, google_pay, pay_later, or venmo |
Returns array<int, string>.
ap.ecommerce.paypal.captured
Fires once per order when a capture takes money (COMPLETED) or starts
to (PENDING, settled later by webhook). A retried finalize that finds
the same capture doesn't fire it again. A capture first recorded after it
was already refunded at PayPal reports PARTIALLY_REFUNDED or REFUNDED.
A listener that throws is reported and never fails the capture.
| # | Parameter | Type | Description |
|---|---|---|---|
| 1 | $order |
ArtisanPackUI\Ecommerce\Models\Order |
The engine order |
| 2 | $captureId |
string |
PayPal capture id (the payment's gateway reference) |
| 3 | $status |
string |
COMPLETED, PENDING, PARTIALLY_REFUNDED, or REFUNDED |
| 4 | $paypalOrder |
ArtisanPackUI\EcommercePayPal\Api\Data\PayPalOrder |
The PayPal Order as captured |
| 5 | $gateway |
ArtisanPackUI\EcommercePayPal\Gateways\PayPalGateway |
The gateway that captured it |
addAction( 'ap.ecommerce.paypal.captured', function ( Order $order, string $captureId, string $status ): void {
Log::info( "PayPal capture {$captureId} ({$status}) for order {$order->order_number}" );
}, 10, 3 );
ap.ecommerce.paypal.captureDenied
Fires when PayPal denies a capture that was pending on a placed order
(PAYMENT.CAPTURE.DENIED/DECLINED), after the order note and timeline
entry are written and the order's payment status is set to failed
(see Webhooks). It fires once those
writes have committed. A listener that throws is reported, not rethrown,
so the webhook job doesn't retry and write the note and timeline entry
again.
| # | Parameter | Type | Description |
|---|---|---|---|
| 1 | $order |
ArtisanPackUI\Ecommerce\Models\Order |
The engine order |
| 2 | $captureId |
string |
PayPal capture id |
| 3 | $status |
string |
The capture's status at PayPal (DENIED, DECLINED) |
ap.ecommerce.paypal.authorizationExpiring
Fires once per authorization, from the daily
ecommerce-paypal:authorizations:reauthorize run, when an uncaptured
authorization is within 2 days of expiring.
| # | Parameter | Type | Description |
|---|---|---|---|
| 1 | $order |
ArtisanPackUI\Ecommerce\Models\Order |
The engine order |
| 2 | $expiresAt |
Carbon\CarbonImmutable |
When PayPal expires the authorization |
ap.ecommerce.paypal.disputeCreated / disputeUpdated / disputeResolved
Fire for CUSTOMER.DISPUTE.CREATED, .UPDATED, and .RESOLVED, after the
order note and timeline entry are written and committed. A listener that
throws is reported, not rethrown, so the webhook job doesn't retry and
write them again. Disputes never change the order status, so these hooks
are where store policy goes.
| # | Parameter | Type | Description |
|---|---|---|---|
| 1 | $order |
ArtisanPackUI\Ecommerce\Models\Order |
The order paid with the disputed capture |
| 2 | $dispute |
array |
Summary: id, reason, status, stage, outcome, amount, currency, respond_by (ISO 8601), capture_ids, url (Resolution Center), updated_at |
addAction( 'ap.ecommerce.paypal.disputeCreated', function ( Order $order, array $dispute ): void {
Notification::route( 'mail', 'support@example.com' )->notify( new DisputeOpened( $order, $dispute ) );
}, 10, 2 );
ap.ecommerce.paypal.webhookEvent
Fires for every verified PayPal webhook delivery the engine hasn't seen before, whether or not this package acts on its type. It runs in the webhook request, so queue anything slow.
| # | Parameter | Type | Description |
|---|---|---|---|
| 1 | $eventType |
string |
The event type, e.g. PAYMENT.CAPTURE.COMPLETED |
| 2 | $event |
array |
The event as PayPal sent it (id, event_type, resource, …) |
| 3 | $result |
ArtisanPackUI\Ecommerce\ValueObjects\WebhookResult |
The engine's verified result |
The event body may hold payer details, so don't log or store it unredacted. PayPal signs a CRC of the body, not the body itself: fetch a resource from PayPal before acting on it, as this package's handlers do.
Engine hooks this package relies on
ap.ecommerce.payment.availableGateways
The engine filters the gateways offered for a cart. This package uses it to hide a PayPal method that is switched off, or that doesn't accept the cart's currency. Buyer-country eligibility is decided by the PayPal JS SDK in the browser. Hosts can use the filter too.
| # | Parameter | Type | Description |
|---|---|---|---|
| 1 | $gateways |
array<string, PaymentGateway> |
Gateways keyed by gateway key |
| 2 | $cart |
ArtisanPackUI\Ecommerce\Models\Cart |
The cart being checked out |
The engine only accepts narrowing or reordering: entries that aren't the registered gateway for their key are dropped.
use ArtisanPackUI\EcommercePayPal\Gateways\PayPalGateway;
// Hide every PayPal method for orders over $10,000.
addFilter( 'ap.ecommerce.payment.availableGateways', function ( array $gateways, Cart $cart ): array {
if ( (int) $cart->total_amount <= 1_000_000 ) {
return $gateways;
}
return array_filter( $gateways, fn ( $gateway ): bool => ! $gateway instanceof PayPalGateway );
}, 20, 2 );
Use a priority above 10 so your filter runs after this package's own.
ap.ecommerce.payment.clientConfig
The engine filters the client config of every gateway after the gateway
builds it, so it sees the whole config (driver, flow,
publishable_key, client_secret, redirect_url, options). Prefer
ap.ecommerce.paypal.clientOptions for PayPal options: it keeps the
Order and amount from being changed.
| # | Parameter | Type | Description |
|---|---|---|---|
| 1 | $config |
array|null |
The client config |
| 2 | $gateway |
PaymentGateway |
The gateway |
| 3 | $cart |
ArtisanPackUI\Ecommerce\Models\Cart |
The cart |
| 4 | $session |
ArtisanPackUI\Ecommerce\ValueObjects\PaymentSession |
The session |
use ArtisanPackUI\EcommercePayPal\Gateways\PayPalGateway;
addFilter( 'ap.ecommerce.payment.clientConfig', function ( ?array $config, $gateway ): ?array {
if ( $gateway instanceof PayPalGateway && null !== $config ) {
$config['options']['button']['shape'] = 'pill';
}
return $config;
}, 10, 2 );

