Stripe
Take a one-time card payment with an XPAC Forms submission, at a fixed price or a total worked out from the answers.
Stripe adds a Stripe Payment Element block to Forms. The visitor pays by card when they send the form, and the submission is saved only after Stripe confirms the right amount was paid. The site works out the amount itself, so editing the page in the browser can't change what is charged.
It needs XPAC Forms. Without it, admins see a notice asking for it.
Connect Stripe
In Stripe, open the API keys page in a sandbox. Copy the publishable key
(pk_test_…) and the secret key (sk_test_…, or a restricted rk_test_… key).
At XPAC → Integrations → Stripe, paste them into Test publishable key and Test secret key. Leave Live mode off, set the Currency and save.
Click Test connection and copy the webhook endpoint address it shows.
In Stripe Workbench, open Webhooks and click Create an event destination.
Choose Your account, select the events below, choose Webhook endpoint and
paste the address. Reveal the signing secret (whsec_…), paste it into Webhook
signing secret and save.
Add the block to a form and run the tests in Test before going live.
For real payments, switch the API keys page to live mode and paste the live keys into Live publishable key and Live secret key. Create a second event destination in live mode, save its signing secret (it differs from the sandbox one), then turn on Live mode.
Events to select:
payment_intent.succeeded,payment_intent.payment_failed,payment_intent.processingandpayment_intent.canceled. The first one is what saves a paid submission whose visitor never came back.charge.refunded,charge.refund.updated,charge.dispute.created,charge.dispute.closed,charge.dispute.funds_withdrawnandcharge.dispute.funds_reinstated. These add refunds and disputes to the submission.
Test connection checks the saved keys with Stripe and sends nothing. It names the
Stripe account, the mode and the currency, and warns when the two keys come from
different modes or don't match Live mode, when the account can't take payments
yet, when the currency isn't a three-letter code, and when the webhook signing secret
is missing or doesn't start with whsec_. It always shows the webhook endpoint
address. Saved keys stay visible to administrators on this screen.
The other settings:
- Live mode: off uses the test keys. On charges real cards with the live keys.
- Advanced fraud signals: on by default. Loads Stripe.js on every page so Stripe can spot fraud from how visitors browse. Off loads it only on pages with a payment form. Stripe Radar still checks every payment.
- Currency: a three-letter code such as
usd,eurorgbp, enabled on your Stripe account. Every price on every form is charged in it.
If the keys are missing or don't match Live mode, or the currency isn't valid, the payment field is replaced by a message and the form can't be sent. Visitors read "This form can't take payments right now." Editors see the reason. While a published form takes payments without a webhook signing secret, every admin screen shows a warning.
Add it to a form
Add one Stripe Payment Element block to the form. Its settings:
- Fixed price: on by default. Charges the amount in Price. The help under Price shows Stripe's minimum for your currency.
- With Fixed price off, pick the fields to add up instead. Their values must be
numbers, such as a choice whose value is
25. Password, confirmation and file fields aren't offered. - Collect address: adds Stripe's address form. The address is saved in Stripe and on the submission.
- Address type: Shipping or Billing.
- Redirect after payment: optional. Used only when the form has no redirect of its own.
On the block's Styles tab, Appearance sets the Theme (Stripe, Night or Flat), the Layout (Tabs or Accordion) and Start collapsed. Radio buttons and Space between items show for Accordion only.
Don't use the block on a form sent by Forms Auto Submit: a form with a payment field only accepts a submission that has been paid.
What you see after a payment
The submission's Payment column reads like "Paid 10.00 USD on October 9, 2025. Stripe payment pi_…". Sandbox payments add "(test mode)". With Collect address on, the address and phone follow.
In Stripe, the payment is described as "Site name: Form title (form #12)". About 30
seconds after the submission is saved, the payment also gets xpac_entry_id and
xpac_entry_url metadata, which link it back to the form's submissions.
When Stripe reports a refund or a dispute, a line starting "Stripe status:" is added in front of the payment line, for example "Stripe status: full refund of 10.00 USD on October 10, 2025."
When a payment goes wrong
Stripe payments don't appear on the Deliveries screen. The payment is checked before the submission is saved, so there is nothing there to retry.
- Declined card: the visitor sees Stripe's reason and can try another card. Nothing is saved.
- Refused after paying: if another check refuses the form after the card was charged, the visitor is told the payment was received and not to pay again. They can fix the form and send it again, and the same payment is used.
- Bank check on another page: the visitor comes back to the form, which then finishes sending.
- Visitor never comes back: the site keeps a copy of the answers, without
passwords or files, for up to one day. When
payment_intent.succeededarrives, the site saves the submission from that copy, even if the form has closed or filled up since. That is noted on the payment line. An optional file field is left empty. A required one means the submission can't be saved this way.
A payment Stripe reported with no matching submission shows in an admin notice, up to 10 at a time, each with View in Stripe and Mark as handled. A payment leaves the list when a submission matches it, when you mark it handled, or when Stripe reports it fully refunded. Sandbox payments are hidden while the site uses live keys.
The site keeps up to 500 payments from the webhook. When that fills up, matched and handled payments go first. An unmatched payment that has to go is named in the notice, with Clear this notice.
Test before going live
With Live mode off, use Stripe's test cards to try a good card, a declined card, a card that asks for 3-D Secure, and a visitor who closes the tab after paying. Each paid test should create exactly one submission, and the payment id on the submission should match the one in Stripe.
For developers
Filters:
xpac_forms_stripe_currency: the three-letter currency.xpac_forms_stripe_client: replaces the Stripe client, for example to add a proxy.xpac_forms_stripe_payment_method_types: payment method types for one payment. Defaultcard.xpac_forms_stripe_intent_metadata: extra metadata on the payment. The keys XPAC checks later can't be changed.xpac_forms_stripe_intent_description: the payment's description in Stripe.xpac_forms_stripe_statement_descriptor_suffix: a card statement suffix. Empty by default.xpac_forms_stripe_create_rate_limit: new payments per visitor IP and form per hour. Default 20, from 1 to 100.xpac_forms_stripe_payload_ttl: seconds the copy of the answers is kept. From 300 to 86,400.xpac_forms_stripe_webhook_ledger_max: payments kept from the webhook. Default 500, from 50 to 5,000.xpac_forms_stripe_sync_entry_metadata: return false to stop writing the submission to the Stripe payment.xpac_forms_stripe_allow_legacy_intent: off by default. Accepts, for up to a day, a payment started before an update that added its checks.
Actions:
xpac_forms_stripe_webhook: runs for each verified Stripe event for this site, with the event type, its object and the event.xpac_forms_stripe_payment_settled: runs when Stripe reports a refund or a dispute for a saved submission, with the submission id, the payment id and the stored record.
Signatures are in the Stripe hook reference, and the block's attributes in the block reference.