Skip to content

PayPal Payment Buttons: record payments PayPal reports through a webhook and forward them to WordPress.com - #52871

Closed
millerf wants to merge 2 commits into
trunkfrom
add/paypal-payment-webhooks
Closed

millerf wants to merge 2 commits into
trunkfrom
add/paypal-payment-webhooks

Conversation

@millerf

@millerf millerf commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #

Proposed changes

A payment made through a PayPal-hosted button, pay link or QR code never touches the site, so nothing on our side sees it. This PR makes PayPal tell the site, and the site tell WordPress.com.

  • Register a webhook on the merchant's PayPal account for the PAYMENT.CAPTURE.COMPLETED, DECLINED, PENDING, REFUNDED and REVERSED events, pointing at a new POST wpcom/v2/paypal/webhook route on the site. Registration happens when credentials are connected (manual or Partner Referrals), lazily from the connection-status endpoint for sites connected before this shipped, and again on an environment switch. A listener URL PayPal already knows is adopted rather than treated as a failure. The webhook is deleted on disconnect, before the credentials go.
  • Verify every delivery with PayPal (POST /v1/notifications/verify-webhook-signature) using the delivery's PAYPAL-* headers, the raw body and the stored webhook id. Anything PayPal does not vouch for gets a 400.
  • Record a Tracks event per capture, jetpack_paypal_capture_{completed,declined,pending,refunded,reversed}, attributed to the site's connection owner, with the capture id, status, amount, currency, order id, custom id, invoice id, merchant id, final-capture flag and environment. A notification PayPal delivers twice is recorded once.
  • Forward each verified event to WordPress.com at wpcom/v2/paypal/webhook-events, signed as the blog, so payments can be logged centrally the way the V1 buttons were. Only the fields above go over, never the payer. A transport error or a 5xx schedules a WP-Cron retry five minutes later, three attempts in all; a 4xx is taken as final.

Why the site forwards instead of WordPress.com subscribing directly: PayPal delivers webhook events only to the REST app whose client id processed the payment. Our Partner Referrals flow is a FIRST_PARTY integration, so every payment link and button is created with the merchant's own credentials and a webhook on our platform app would never see a capture. The WordPress.com endpoint is a separate change; until it ships, the forward gets a 404 and stops after one attempt.

Related product discussion/links

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

Yes. For sites using the API-managed PayPal buttons, each payment capture PayPal reports through the webhook is recorded as a jetpack_paypal_capture_* Tracks event and forwarded to WordPress.com. The data is the capture's identifiers, amount, currency, status and the merchant's PayPal id. The buyer's name and email are never recorded or forwarded.

Testing instructions

The site must be reachable by PayPal over HTTPS (a Jurassic Ninja site works) and the paypal-payments-api-managed-buttons feature flag must be on.

  1. Connect a PayPal sandbox account in the block editor, either with client id and secret or through the Partner Referrals flow.
  2. In the PayPal developer dashboard, open the app's webhooks. A webhook for https://<site>/wp-json/wpcom/v2/paypal/webhook with the five PAYMENT.CAPTURE.* events should now exist. The jetpack_paypal_payment_buttons_webhook option holds its id, environment and URL.
  3. Create a payment button and pay with a sandbox buyer account.
  4. In the developer dashboard's webhook events, the PAYMENT.CAPTURE.COMPLETED delivery should show a 200 from the site. In Tracks, a jetpack_paypal_capture_completed event for the connection owner should appear with the capture id and amount.
  5. Resend the same delivery from the dashboard: the site answers 200 with "recorded": false and no second Tracks event.
  6. Tamper with a delivery (replay it with curl and a changed body or a missing PAYPAL-TRANSMISSION-SIG header): the site answers 400.
  7. Disconnect PayPal: the webhook disappears from the developer dashboard and the option is deleted.
  8. Forwarding: with the WordPress.com endpoint not deployed, the request log shows one POST wpcom/v2/paypal/webhook-events per delivery answered 404 and no scheduled jetpack_paypal_webhook_forward_retry cron event. Simulating a 5xx (for example by filtering pre_http_request for that URL) schedules one retry five minutes later, and a third failed attempt schedules nothing.

🤖 Generated with Claude Code

millerf and others added 2 commits September 28, 2026 11:16
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>
PayPal delivers webhook events only to the app whose client id processed
the payment, and under our FIRST_PARTY referral that is the merchant's own
app, so the site is the only place that sees a capture. It now forwards
each verified event, minus the payer, to wpcom/v2/paypal/webhook-events as
the blog, retrying from cron on an outage, so payments can be logged
centrally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@millerf millerf added Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Status] Needs Review This PR is ready for review. 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 4 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-payment-buttons.php 504/562 (89.68%) 0.02% 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 145/165 (87.88%) 💚

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.

@millerf

millerf commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

We changed the approach and move to thrid_party

@millerf millerf closed this Oct 1, 2026
@github-actions github-actions Bot removed the [Status] Needs Review This PR is ready for review. label Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Package] Paypal Payments [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant