Payments
Core Forms 4.1 introduces payment-required forms. Pick a provider, set an
amount, and the form will redirect submitters to a secure hosted checkout
on submit. The submission stays in pending_payment status until the
provider posts a webhook back confirming the charge — at that point the
normal form actions (email notifications, Mailchimp, webhooks, etc.) fire.
Supported providers
| Provider | API | Test mode |
|---|---|---|
| Stripe | Checkout Sessions | Stripe test keys |
| PayPal | Orders v2 | Sandbox |
| Razorpay | Payment Links | Razorpay test keys |
| Polar.sh | Checkouts | Polar sandbox |
All four are production REST integrations — no third-party SDKs are
bundled, just wp_remote_post calls and webhook signature verification.
Configuring a provider
- Open Core Forms → Settings, scroll to the Payments section.
- Choose Test mode while you're wiring things up.
- Paste the API credentials for the provider(s) you want to use. Each
provider has both
_testand_liveslots so you can switch modes without re-entering keys. - Configure a webhook in the provider's dashboard pointing to the URL shown beside each provider:
- Stripe:
https://your-site/wp-json/cf/v1/payments/stripe/webhook - PayPal:
https://your-site/wp-json/cf/v1/payments/paypal/webhook - Razorpay:
https://your-site/wp-json/cf/v1/payments/razorpay/webhook - Polar.sh:
https://your-site/wp-json/cf/v1/payments/polar/webhook
Subscribe to these events:
- Stripe:
checkout.session.completed,checkout.session.expired,charge.refunded - PayPal:
CHECKOUT.ORDER.APPROVED,PAYMENT.CAPTURE.COMPLETED,PAYMENT.CAPTURE.REFUNDED,PAYMENT.CAPTURE.DENIED - Razorpay:
payment_link.paid,payment_link.cancelled,payment_link.expired,refund.processed - Polar.sh:
order.created,order.refunded,checkout.updated
- Save the webhook signing secret back into Core Forms.
Configuring a form
- Edit a form, switch to the Payment tab.
- Tick Require payment.
- Pick a provider — providers without configured credentials show
(not configured)so you can spot mistakes. - Set the Amount. Either a fixed amount in major units (
19.99) or a field reference like[total]to charge whatever value the user enters. The data-variable system from 4.0 is fully supported. - Pick a Currency (ISO-4217 —
USD,EUR,GBP,INR,JPY, etc.). - Optionally fill in a Description (shown on the checkout page) and a Redirect after payment URL.
Flow
[user submits form]
│
▼
[Forms::process()] honeypot, nonce, validation, save submission
│
▼
[Payments::create_checkout()] POST to provider, store payment_id
│
▼
[ JSON response { redirect_url: <provider checkout URL> } ]
│
▼
[browser navigates to provider] user pays
│
▼
[provider POSTs webhook]
│
▼
[Payments::handle_webhook()] verify signature, find submission
│
▼
[mark submission as paid] re-fire cf_form_success + actions loop
The webhook is the source of truth. The user's return URL is treated as
a hint only — if a webhook hasn't arrived yet, the submission stays in
pending_payment and the actions don't fire. This means a malicious user
who tampers with the return URL can't trick the plugin into firing the
post-success actions for an unpaid submission.
Hooks
cf_payment_completed
Fires once a webhook confirms a successful payment. Parameters:
Submission $submission, Form $form. Use this for payment-only logic
that shouldn't run on free submissions.
add_action( 'cf_payment_completed', function ( $submission, $form ) {
error_log( 'Paid form ' . $form->slug . ' (sub#' . $submission->id . ')' );
}, 10, 2 );
cf_payments_orchestrator
Returns the payments orchestrator. Use it to access registered gateways or to record additional payment attempts.
$payments = apply_filters( 'cf_payments_orchestrator', null );
if ( $payments ) {
$stripe = $payments->get_gateway( 'stripe' );
}
Payment statuses
| Status | Meaning |
|---|---|
pending_payment |
Submission saved, user redirected to checkout. |
paid |
Webhook confirmed payment. Form actions fired. |
payment_failed |
Provider returned a failure or the user cancelled. |
refunded |
A subsequent webhook reported a refund. |
The submission table also stores payment_provider, payment_id,
payment_amount (in minor units, e.g. cents), and payment_currency
for reporting and dispute lookups.
Testing locally
WordPress's REST API is accessible via localhost:8080/wp-json/... but
provider webhook endpoints need a public URL. Use ngrok or
localcan.com to tunnel a public hostname to your dev install, then
register the webhook URL in each provider's dashboard.