Skip to main content

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

  1. Open Core Forms → Settings, scroll to the Payments section.
  2. Choose Test mode while you're wiring things up.
  3. Paste the API credentials for the provider(s) you want to use. Each provider has both _test and _live slots so you can switch modes without re-entering keys.
  4. 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
  1. Save the webhook signing secret back into Core Forms.

Configuring a form

  1. Edit a form, switch to the Payment tab.
  2. Tick Require payment.
  3. Pick a provider — providers without configured credentials show (not configured) so you can spot mistakes.
  4. 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.
  5. Pick a Currency (ISO-4217 — USD, EUR, GBP, INR, JPY, etc.).
  6. 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.