PayPal
Integrates the Payment actor with PayPal. The core of the app is the Payment-actor integration — capturing and refunding PayPal payments and reconciling them onto the Hantera Payment via webhooks.
The app also ships with a self-contained, redirect-based checkout flow (start → PayPal approval → return). It's a convenience entry point that any redirect-capable checkout can drive, and it works particularly well as a Kustom External Payment Method (EPM) — Kustom collects the address and redirects the customer to the start ingress, then the PayPal app owns the rest of the completion flow (there is no postback from Kustom for external payments).
Key features:
- Payment-actor capture & refund driven by the
OnPaymentCapturerule hook - Unified settlement: a single
settlerule both captures outstanding authorizations and refunds over-charged captures, based on the capture's remaining balance - Idempotent webhook receiver with PayPal signature verification that reconciles dashboard-initiated actions (manual captures, refunds, voids) back onto the Hantera Payment
- Manual Sync from PayPal button in the portal order view as a fallback for missed or late webhook deliveries
- Optional redirect-driven checkout (
start→ PayPal approval →return) that creates the Payment, links it to the cart, completes the cart, and returns the customer to the channel confirmation page —intent=AUTHORIZE, captured later
Checkout Flow
The confirmation page is responsible for detecting the order's payment type and rendering accordingly.
Settlement (Capture & Refund)
PayPal Orders v2 is a hierarchy of resources, each with its own id:
- Order — the checkout session the customer approves.
- Authorization — a hold on the funds (created at
return). Needed to capture. - Capture — the actual movement of money. Needed to refund.
- Refund — issued against a specific capture.
The settle rule listens on OnPaymentCapture. When a capture is pending it inspects the remaining balance:
Because refunds are per-capture, the refund job walks the payment's charge journal entries newest-first to find the capture ids to refund against.
PayPal ↔ Hantera mapping
| PayPal | Hantera Payment |
|---|---|
| Order id | externalReference |
| Authorization id | authorization authorizationNumber |
| Capture id | charge journal transactionReference |
| Refund id | refund journal transactionReference |
Settings
| Setting | Secret | Description |
|---|---|---|
clientId | no | PayPal REST app Client ID |
clientSecret | yes | PayPal REST app Client Secret |
environment | no | sandbox or live |
webhookId | yes | PayPal webhook ID. Required for signature verification on incoming webhooks; without it every webhook is rejected with INVALID_SIGNATURE and dashboard-initiated changes won't sync back. |
Webhook configuration
The webhook ingress is what keeps the Hantera Payment in sync when an action happens outside Hantera — most commonly when a backoffice agent captures or refunds the transaction directly in the PayPal dashboard. Configuring it is a one-time setup in two places.
1. In the PayPal Developer dashboard
Go to PayPal Developer → Apps & Credentials and select the REST app whose Client ID / Secret you used for the
clientId/clientSecretsettings. Make sure the Sandbox / Live toggle matches theenvironmentsetting.Scroll to Webhooks on the app's page and click Add Webhook.
Set the Webhook URL to:
https://<your-hantera-host>/ingress/paypal/webhookSubscribe to the following event types (the ingress reconciles these by re-fetching the parent order from PayPal; other event types are accepted and logged but otherwise ignored):
Checkout order approved—CHECKOUT.ORDER.APPROVEDCheckout order completed—CHECKOUT.ORDER.COMPLETEDPayment authorization created—PAYMENT.AUTHORIZATION.CREATEDPayment authorization voided—PAYMENT.AUTHORIZATION.VOIDEDPayment capture completed—PAYMENT.CAPTURE.COMPLETEDPayment capture denied—PAYMENT.CAPTURE.DENIEDPayment capture pending—PAYMENT.CAPTURE.PENDINGPayment capture refunded—PAYMENT.CAPTURE.REFUNDEDPayment capture reversed—PAYMENT.CAPTURE.REVERSED
Save the webhook. PayPal generates a short opaque Webhook ID for the subscription — copy it.
2. In Hantera
Paste the Webhook ID into the app's webhookId setting. On every incoming webhook the ingress POSTs the transmission headers and raw body to PayPal's /v1/notifications/verify-webhook-signature endpoint, which checks them against this ID; verified events are forwarded to the syncPayment job and unverified ones are rejected.
What the webhook does
The webhook extracts the parent PayPal Order id from the event resource and delegates to the syncPayment job. That job fetches the authoritative state from PayPal and reconciles it onto the Hantera Payment — a single code path that also powers the manual "Sync from PayPal" portal button.
Manual sync
The portal renders a Sync from PayPal button on every PayPal payment in the order view. It schedules the same syncPayment job the webhook uses and is the canonical recovery path when webhook delivery missed an event — for example a transient PayPal outage, an event raised before the webhook was configured, or an agent performing a refund inside the PayPal dashboard before signature verification was set up.
Sync only ever adds missing journal entries and updates the authorization state. It never removes or reverses an existing entry, so running it repeatedly is safe.
Using PayPal inside Kustom
The Kustom app exposes the OnKustomExternalPaymentMethods hook. A merchant rule returns a custom effect whose redirect_url points at this app's start ingress for the channels that should offer PayPal.