Skip to content

PayPal Payment Buttons: record payments PayPal reports through a webhook - #52855

Closed
millerf wants to merge 1 commit into
trunkfrom
add/paypal-payment-webhooks
Closed

millerf wants to merge 1 commit into
trunkfrom
add/paypal-payment-webhooks

Conversation

@millerf

@millerf millerf commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Proposed changes

The V1 block's payments were logged by WordPress.com because every purchase went through its simple-payments/paypal/payment and /execute endpoints. V2 buttons are PayPal-hosted (HostedButtons, pay links, QR codes): the buyer pays inside PayPal's iframe or page, the merchant's credentials live on the site, and nothing on our side sees a payment. The hosted-buttons SDK component takes only a button id and exposes no callback, so there is no client-side signal either. This PR has PayPal tell the site about payments through a webhook, and records each one as a Tracks event.

  • PayPal_Webhooks registers a webhook on the connected account (POST /v1/notifications/webhooks, with the merchant's credentials) for PAYMENT.CAPTURE.COMPLETED, DECLINED, PENDING, REFUNDED and REVERSED, pointing at rest_url( 'wpcom/v2/paypal/webhook' ). The id, environment and URL are stored in the jetpack_paypal_payment_buttons_webhook option. A URL PayPal already knows (WEBHOOK_URL_ALREADY_EXISTS) is adopted from the account's webhook list, so a site that lost the option or reconnected the same account ends up registered. PayPal only accepts HTTPS listeners; a site it cannot reach stays connected, and registration is retried at most hourly.
  • When it registers: at the end of handle_connect and of PayPal_Partner_Onboarding::complete_onboarding(); lazily from handle_connection_status, which the editor calls whenever a block opens, so sites connected before this ships pick it up; and again when the environment is switched, since the webhook lives on the other environment's API. Disconnect deletes the webhook before the credentials go.
  • POST wpcom/v2/paypal/webhook is open, as PayPal has no account here; the signature check stands in for the permission check. The handler passes the PAYPAL-* headers, the stored webhook id and the decoded body to POST /v1/notifications/verify-webhook-signature, and answers 400 to anything PayPal does not vouch for.
  • PayPal_Webhooks::handle() turns a verified notification into one event, jetpack_paypal_capture_{completed,declined,pending,refunded,reversed}, with event_id, capture_id, status, amount, currency, order_id, custom_id, invoice_id, final_capture and environment, plus the blog id and site URL the Tracking class adds. A notification PayPal delivers twice is recorded once. Nothing about the payer is sent.
  • PayPal_Tracks records events through the connection package's Tracking class, attributed to the connection owner, the way the V1 WordPress.com events went to the product's author. A site with no connection owner records nothing.
  • PayPal_API_Client gains create_webhook(), list_webhooks(), delete_webhook() (a 404 counts as deleted) and verify_webhook_signature(), on the existing request layer.

Not covered here, on purpose: whether custom_id carries the payment link id for hosted-button payments (PayPal's docs do not say; the property is recorded as PayPal sends it), and merchants on WordPress.com Simple sites, where the listener URL and the route's reachability need checking on that side. Both are called out in the testing instructions.

Related product discussion/links

  • WOOPTP (PayPal Payment Buttons V2)

Does this pull request change what data or activity we track or use?

Yes. Five new Tracks events, listed above, attributed to the site's Jetpack connection owner. They carry PayPal's capture id, order id, amount, currency, status and the custom_id and invoice_id PayPal attaches to the capture, and the environment. They do not carry the buyer's email, name or address. The site also registers a webhook on the merchant's PayPal account, which the merchant can see in their PayPal developer dashboard.

Testing instructions

Automated: jp test php packages/paypal-payments (775 tests) passes and PHPCS is clean. The new tests mock PayPal's webhook, verification and Tracks pixel endpoints and assert on the requests. Phan did not run locally (no ast extension); CI will.

The site must be reachable by PayPal over HTTPS: a Jurassic Ninja site works, a local site does not (registration is refused with paypal_webhook_url_not_https and nothing else changes).

  1. Enable the flag: add_filter( 'jetpack_feature_flag_enabled_paypal-payments-api-managed-buttons', '__return_true' );
  2. Connect a sandbox PayPal account in the block, either by pasting a client id and secret or through Partner Referrals. Then check:
    • wp option get jetpack_paypal_payment_buttons_webhook holds an id, sandbox, and the site's wp-json/wpcom/v2/paypal/webhook URL;
    • the sandbox developer dashboard (My Apps & Credentials, the app, Webhooks) lists that URL with the five PAYMENT.CAPTURE.* events.
  3. Add a payment button (single button format is fine) and pay for it with a sandbox buyer account.
  4. In Tracks, look for jetpack_paypal_capture_completed for the site's blog id with the capture id and amount from the sandbox merchant dashboard, environment = sandbox, and note whether custom_id holds the PLB- payment link id. That last point is the open question this PR cannot answer from the docs.
  5. Send the same notification again from the dashboard's Webhook Events page (Resend). The route answers 200 with recorded: false and no second event appears.
  6. Replay a notification with a changed body, or POST anything to the route by hand. The route answers 400 paypal_webhook_invalid_signature (or paypal_webhook_missing_headers) and records nothing.
  7. Switch the environment in the block. The option now names the other environment and PayPal's dashboard for that environment lists the webhook (registration fails harmlessly when the credentials are not valid there).
  8. Disconnect PayPal. The option is gone and the dashboard no longer lists the webhook.
  9. Regressions: connecting on a site PayPal cannot reach (HTTP) still succeeds and reports connected; the editor, payment link creation and the published block are unchanged.

🤖 Generated with Claude Code

A payment made through a hosted button, link or QR code never touches the
site, so the site registers a webhook on the merchant's PayPal account for
the PAYMENT.CAPTURE.* events, verifies each delivery with PayPal, and
records it as a jetpack_paypal_capture_* Tracks event attributed to the
connection owner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@millerf millerf added [Status] Needs Review This PR is ready for review. [Status] Needs Privacy Updates Our support docs will need to be updated to take this change into account [Tests] Includes Tests [Package] Paypal Payments labels Sep 28, 2026
@millerf millerf self-assigned this Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the add/paypal-payment-webhooks branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack add/paypal-payment-webhooks

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@jp-launch-control

jp-launch-control Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 3 files.

File Coverage Δ% Δ Uncovered
projects/packages/paypal-payments/src/paypal-payment-buttons/class-paypal-api-client.php 253/308 (82.14%) -6.63% 23 💔
projects/packages/paypal-payments/src/paypal-payment-buttons/class-paypal-partner-onboarding.php 289/315 (91.75%) 0.03% 0 💚
projects/packages/paypal-payments/src/paypal-payment-buttons/class-paypal-rest-controller.php 822/854 (96.25%) 0.15% 0 💚

2 files are newly checked for coverage.

File Coverage
projects/packages/paypal-payments/src/paypal-payment-buttons/class-paypal-tracks.php 8/10 (80.00%) 💚
projects/packages/paypal-payments/src/paypal-payment-buttons/class-paypal-webhooks.php 112/132 (84.85%) 💚

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Paypal Payments [Status] Needs Privacy Updates Our support docs will need to be updated to take this change into account [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant