Skip to main content

Payments (Pro)

Take the money as well as the registration. Switch payments on for a form and registrants go straight to a checkout when they submit - iDEAL, Bancontact, card, Apple Pay, whatever your provider's account has enabled - and come back to a confirmation on your own site. Paid registrations show up in Responses and in the XLSX export next to the answers they belong to.

Payments are one feature with pluggable providers. Mollie and Stripe both ship today, and everything on this page is true of either. Which one you connect changes the checkout your registrants see and nothing else: the same per-form settings, the same held emails, the same webhook, the same Responses column.

Which provider?​

You can connect several and choose per form, but most sites want one.

MollieStripeWooCommerce
Strongest atiDEAL, Bancontact, SEPA - how the Benelux paysCards worldwide, Apple Pay, Google PayWhatever your shop already accepts, including offline methods
Available inEuropeMost of the worldAny site already running WooCommerce
Account reviewLive payments after Mollie reviews the account; the test key works at onceLive payments as soon as the account details are completeNothing to review - you already did this
Setup hereTwo API keysTwo API keys, and optionally a webhook endpointNothing to paste

If you take registrations in the Netherlands or Belgium and want iDEAL, connect Mollie. Otherwise Stripe is the shorter route.

If this site already runs WooCommerce, start there instead. You have already chosen a processor, agreed its fees and taught your bookkeeper where the money lands; connecting a second one under a different account, so that registrations arrive in a separate ledger, is work with nothing at the end of it. WooCommerce is also the only option here that can take a bank transfer or an invoice paid later - see below.

What you need​

  • LocalForm Pro, with an active license.
  • An account with Mollie or Stripe, with at least one payment method enabled - or WooCommerce on this site with at least one payment method switched on.

There is no LocalForm account, no per-transaction fee on our side and no middleman: the money goes from your registrant to your own provider account. What LocalForm stores is your API key and a record of each payment. (Through WooCommerce there is not even a key - LocalForm contacts nothing at all, and the order lives in your own shop.)

Both providers are set up under LocalForm → Settings → Integrations, in the Payments group - see Integrations for how that screen works and what the on/off switch on each card does.

Connect Mollie​

  1. In Mollie, open Developers → API keys and copy both keys. They look like live_... and test_....
  2. In WordPress, go to LocalForm → Settings → Integrations and open the Mollie card.
  3. Make sure Take payments through Mollie is on.
  4. Paste the Live API key and the Test API key.
  5. Leave Test mode on while you are setting things up.
  6. Pick the Currency you charge in. This is the currency Mollie settles - separate from the currency symbol under Settings → General, which is only what visitors read on the form.
  7. Click Test the connection. It checks the key that the mode switch says is in play and tells you which payment methods that Mollie profile has enabled.
  8. Save Changes.

Connect Stripe​

  1. In Stripe, open Developers → API keys. Copy the Secret key - it starts sk_live_ or sk_test_. Use the mode switch at the top of the Stripe dashboard to get the other one.
  2. In WordPress, go to LocalForm → Settings → Integrations and open the Stripe card.
  3. Make sure Take payments through Stripe is on.
  4. Paste the Live secret key and the Test secret key.
  5. Leave Test mode on while you are setting things up.
  6. Pick the Currency you charge in, as above.
  7. Click Test the connection. It names the Stripe account the key belongs to, and says so plainly if that account cannot take payments yet.
  8. Save Changes.

:::warning Not the pk_ key The key on Stripe's dashboard front page starting pk_ is the publishable key. It cannot take payments, and LocalForm will not store it. You want the secret key, which Stripe hides behind a "Reveal" button. :::

A restricted key (rk_...) works too, as long as it may write Checkout Sessions - useful if you would rather not paste a key that can do everything.

Telling Stripe to call your site (optional)​

Payments work without this step. It makes them resolve faster.

Mollie is told where to call as each payment is created. Stripe is not: it registers one endpoint per account, in its own dashboard. Without one, LocalForm finds out that a payment succeeded when the registrant comes back to your site, and on an hourly check for the ones nobody came back from. With one, Stripe tells your site the moment the money lands - which matters most for bank-based methods that clear hours later, and for people who close the tab at the checkout.

  1. Copy the Endpoint URL from the Stripe card in LocalForm.
  2. In Stripe, go to Developers → Webhooks → Add endpoint and paste it in.
  3. Select these four events: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired.
  4. Stripe shows a Signing secret starting whsec_. Copy it.
  5. Back in LocalForm, paste it into Live signing secret or Test signing secret, whichever mode you created the endpoint in.
  6. Save Changes.

Stripe issues a separate signing secret per endpoint, so a site that rehearses in test mode and then goes live ends up with two. Either one on its own is fine.

:::tip Two keys, one switch Test mode is not a separate account, at either hosted provider. Both tell a rehearsal from a real payment by which key signed the request, which is why the site holds both keys and you flip between them with one switch. Switch test mode off before your event goes live - the builder shows a reminder on every form that charges while it is on. :::

Payment description​

Under Payment defaults, Payment description is what registrants see during checkout and on their bank statement a month later. {form} is replaced with the form title and {site} with your site name. The default is the form title, which is the thing they signed up for.

Connect WooCommerce​

There is nothing to connect. If WooCommerce is active on this site, a WooCommerce card is already waiting in the Payments group.

Prefer a walkthrough? Charge for registrations through your shop follows the whole job end to end, including how to take an invoice paid thirty days later.

  1. Go to LocalForm → Settings → Integrations and open the WooCommerce card.
  2. Make sure Take payments through WooCommerce is on.
  3. Check the line reading Registrants can pay with - it lists the payment methods your shop has switched on, which is exactly what a registrant will be offered. Change it under WooCommerce → Settings → Payments; it is your shop's setting, not LocalForm's.
  4. Under Count a registration as paid when the order is, tick the order statuses that mean the money arrived. Processing and Completed are ticked by default and suit most shops.
  5. Save Changes.

If WooCommerce has no payment methods enabled, the card says so - a registrant would otherwise arrive at a checkout with nothing to click.

What happens when somebody registers​

The form is filled in and submitted exactly as always. Then:

  1. LocalForm creates an ordinary WooCommerce order carrying one fee line - the registration, at the price the form worked out.
  2. The registrant goes straight to that order's payment page. No cart, no product page, no basket: one line and your shop's payment methods.
  3. They pay, and come back to your form's own confirmation, not to WooCommerce's order-received page.
  4. The moment the order reaches one of your paid statuses, the registration settles: a held confirmation email goes out, the second webhook fires, and the place stops being provisional.

The order is a normal order. It is refundable, invoiceable, and picked up by whatever accounting or invoicing plugin your shop already runs.

The thing only WooCommerce can do​

A hosted checkout can only take money now. An order can wait.

Set your paid statuses to Processing and Completed only, enable WooCommerce's Direct bank transfer, and a registration can sit at On hold with an invoice against it until the transfer lands - at which point you mark the order paid and the registrant gets their confirmation automatically. That is how an organisation invoicing a municipality for twelve places actually works, and it is the reason to choose WooCommerce over Mollie or Stripe even on a site that could use either.

The same applies to pay at the door: take the registration now, mark the order paid at check-in.

What WooCommerce does not do here​

  • No product is created. A form is not a product, nothing appears in your catalogue, and nothing needs keeping in step. The only thing LocalForm puts in WooCommerce is one order per payment.
  • WooCommerce never counts capacity. A form's maximum registrations stays LocalForm's own setting. Stock is not touched.
  • Deleting a form does not touch any order, and tidying your shop does not stop a form charging.
  • No tax is added. The registrant is charged exactly the amount the form showed. If you want VAT broken out on the order instead, a developer can do it on the localform_pro_woocommerce_order action - see the hooks reference.

:::note Guest checkout If your shop is set to require an account before checking out, so will your registration form be. Most event sites want WooCommerce → Settings → Accounts & Privacy → Allow customers to place orders without an account switched on. :::

Charge for a form​

  1. Edit the form and open the Settings tab.
  2. In the Event Details card, scroll to Payments.
  3. Tick Charge for a registration on this form.
  4. If you have more than one provider connected, pick the Payment provider for this form. With only one switched on, the choice is not shown - every form uses it.
  5. Choose Amount to charge:
OptionWhat it charges
The Payment Price aboveThe form's own Payment Price. The default, and right for an event with one price.
The rate the registrant picksWhichever of the form's event rates they chose. A required question is added to the form with one option per rate - see below.
A fixed amountAn amount set here, with nothing shown on the event card. For a fee you collect without advertising a price.
  1. Save the form.

An amount of zero is a perfectly ordinary answer: nothing is charged and the registration completes exactly as it would on a form with no payments at all. That is what makes a free rate among paid ones work.

Charging per rate​

Choose The rate the registrant picks and LocalForm adds a required radio question to the form, one option per rate under More prices, each labelled exactly as the event card labels it - Adults - € 25,00, Students - € 15,00. Registrants are charged the rate they pick.

The question is managed for you: its options follow the rates, and it disappears again if you switch the amount source back. What it does not manage is where it sits - drag it anywhere in the form and it stays there. Rename its label under Rate question.

If a rate's label or price changes between the page loading and someone submitting, their answer no longer matches any rate and they are not charged. Nobody is ever charged an amount they were not shown.

What registrants see​

  1. They fill in the form and submit.
  2. The response is saved, before any payment is started.
  3. They are sent to the provider's hosted checkout to choose a method and pay. Card details never touch your site.
  4. They come back to the form's page, which now shows the outcome instead of the form:
OutcomeWhat they see
PaidPayment received, followed by your form's success message.
Still resolvingWaiting for your payment - the page watches for the result and updates itself. Bank transfers can take days, so it is safe to close.
Failed, cancelled or expiredPayment not completed, with a Try again link back to the form.

If the form has a Redirect URL, that is where a paid registrant ends up - the checkout borrows the redirect on the way out and it is honoured once the payment is real.

:::note The response is never lost Step 2 is deliberate. Someone who abandons a checkout has still told you who they are and what they wanted, and you almost certainly want to chase them rather than never to have heard of them. So the registration always exists, and the payment is a state attached to it. If the provider is unreachable at the moment they submit, the registration is still accepted - they are told the payment could not be started and asked to get in touch. The same is true of Stripe. :::

Three more switches​

Under the amount, three options that are on by default. All three are what switching payments on usually meant, so leave them unless you have a reason.

Send confirmation emails only after payment​

The admin notification and the registrant's confirmation are held back until the money lands, then sent exactly once. Off, they go out at submit, which means confirming a registration that was never paid for.

Held emails are sent as soon as the payment is confirmed - or immediately, if there turns out to be nothing to pay or the payment could not be started at all. They are never simply dropped.

(Only for forms that store responses. A form in webhook-only mode has nothing to read the answers back from, so its emails always go at submit.)

Only paid registrations count toward the maximum​

With a maximum number of registrations set, an unpaid registration does not hold a seat. Without this, an event "sells out" on people who opened a checkout and walked away.

Send a second webhook when the payment completes​

A second delivery to the same webhook URL when the money arrives, carrying the same payload with the payment marked paid, plus "event": "payment_completed". The first delivery was true when it was sent and your system has already acted on it - this tells it what changed. See Webhooks.

Seeing what was paid​

Responses → Individual gains a Payment column: a status badge and the amount, per registration.

BadgeMeaning
Paid / AuthorizedThe money is there.
Awaiting payment / Payment pendingStarted but not resolved. Normal for a bank transfer.
Canceled / Expired / FailedIt will not complete. The registration is still yours to chase.
No paymentNo payment was started - the amount was zero, or payments were switched on after this response came in.

The XLSX export gains three columns to match: Payment status, Amount paid and Payment method.

Payment records survive the response being deleted, so you can still answer "did this person pay?". They are removed with the form itself.

How LocalForm learns that a payment succeeded​

Three ways, and only one of them ever needs configuring:

  1. The provider calls your site the moment a payment resolves. LocalForm never believes the call - it carries an id, and the status behind that id is read back from the provider with your own key. Mollie is told where to call as each payment is created, so there is nothing to set up. Stripe needs an endpoint registered once, and works without one.
  2. The visitor comes back and the status is read then. This is what makes the whole thing work on a site the provider cannot call at all.
  3. An hourly check picks up payments nobody ever came back from.

Through WooCommerce there is no call to make: LocalForm learns the moment the order changes status, in the same request, because the order is on this site. Marking an order paid by hand settles the registration just as a card payment does.

Whichever gets there first, the confirmation email is sent once and the webhook is delivered once. All three routes converge on the same code, and it is written to be safe to run repeatedly - the callback, the returning visitor and the hourly sweep can all arrive in the same second without anybody being emailed three times.

:::note Why a forged callback achieves nothing The callback endpoint is open to the internet, and it is safe because nothing in the request is believed. Whatever id arrives is looked up in your own payments table, and the status behind it is read back from the provider over the API with your own credentials. Forging a call gets somebody one wasted API request. When you give LocalForm a Stripe signing secret it checks that too, which drops junk before it becomes a lookup - but it is a filter on noise, not what makes the endpoint safe. :::

Your site has to be reachable​

Every hosted payment provider needs somewhere to send registrants back to, and refuses a return URL it cannot resolve from the internet. On a laptop or an internal staging host, that is every URL your site has - so payments cannot be taken from localhost, a private IP address, or a .local / .test hostname.

:::tip WooCommerce is the exception Nothing has to reach in from the internet for a WooCommerce order to be paid - the checkout, the payment and the status change all happen inside your site. A shop on a laptop, an intranet or a staging host with an internal hostname takes registration payments perfectly well, and needs no tunnel to rehearse one. The notice below is not shown when WooCommerce is the only provider you have connected. :::

LocalForm checks before contacting the provider and says so plainly, on the Payments settings screen and in the builder:

This site is at http://localhost:8080/, which a payment provider cannot reach from the internet …

There is nothing to fix in your provider account - the same keys work the moment the site has a public address. (Mollie's own answer to this is "we refused redirectUrl", which reads like the amount is wrong and sends people looking in entirely the wrong place. LocalForm catches it first.)

Rehearsing on a local site​

Expose the site through a tunnel - ngrok, Cloudflare Tunnel, anything that gives it a public hostname - and point the return URL at it:

add_filter( 'localform_pro_payment_return_url', function ( $url ) {
return str_replace( home_url(), 'https://your-tunnel.ngrok.app', $url );
} );

// Optional: real callbacks from Mollie as well, rather than only reading the
// status when the visitor comes back. (For Stripe, register the tunnel's
// address as the endpoint in the Stripe dashboard instead - or use the Stripe
// CLI's `stripe listen --forward-to`.)
add_filter( 'localform_pro_payment_callback_url', function ( $url ) {
return str_replace( home_url(), 'https://your-tunnel.ngrok.app', $url );
} );

The lf_payment query argument has to survive whatever you do to the URL - it is the only thing that identifies the payment on the way back.

:::note The callback URL is optional, the return URL is not LocalForm simply sends no callback URL when the site is unreachable, and falls back to reading the status when the visitor returns and on the hourly sweep - that part degrades gracefully on its own. The return URL cannot: it is what brings the visitor back, so there is nothing to fall back to. :::

When something is not right​

The registration says unpaid, but the order says paid​

The order has reached a status that is not in your paid list. Open LocalForm → Settings → Integrations → WooCommerce and check Count a registration as paid when the order is. Tick the status the order is actually in, and the registration settles the next time the order changes - or move the order out of that status and back into it to settle it now.

This is also the mechanism working correctly when the order is On hold against an unpaid bank transfer. That is what On hold means, and why it should stay unticked.

The WooCommerce card is not on the Integrations screen​

The card only appears where WooCommerce is active, so that the screen never offers a connection the site cannot make. Activate WooCommerce and reload.

"WooCommerce has no payment methods switched on"​

Exactly what it says: a registrant would arrive at a checkout with nothing to click. Enable at least one method under WooCommerce → Settings → Payments. Until you do, the card counts as not connected and forms will not charge through it.

Registrants are asked to create an account​

That is your shop's checkout setting, not LocalForm's. Turn on Allow customers to place orders without an account under WooCommerce → Settings → Accounts & Privacy.

An order was created but nobody paid​

Somebody abandoned the checkout - and the registration is still yours, which is the whole point of saving the response before the payment starts. Chase them, or delete the order and the response together. If unpaid registrations should not hold seats, turn on Only paid registrations count toward the maximum.

The registrant never got a confirmation email​

Check whether Send confirmation emails only after payment is on. If it is, the email is waiting for the payment on purpose, and will be sent the moment the payment lands - including weeks later, when you mark a bank transfer paid.

If the payment did complete, look at LocalForm → Settings → Email → Delivery log, which records every message the plugin tried to send and what the mailer said about it. See Emails.

A form quietly stopped charging​

In order of likelihood:

CauseWhat you seeFix
The provider was switched off on the Integrations screenThe form behaves like an ordinary registration formSwitch it back on. Nothing was lost - see Integrations
The license lapsedEvery Pro feature is inactive, not just paymentsReactivate under LocalForm → License. Nothing recorded is deleted
The provider has no usable credentialsThe builder says so where you set itRe-check the keys, or press Test the connection
The form names a provider that is no longer connectedOnly when several were connectedPick a connected provider on the form, or leave one provider switched on and every form uses it

"This site is at http://localhost:8080/ …"​

A hosted provider will not accept a return URL it cannot resolve. See Your site has to be reachable. This never applies to WooCommerce, which needs no public address at all.

Notes​

  • The amount is in the provider's currency, the form shows a symbol. Core's price fields carry a number and the site's currency symbol, with no currency code anywhere. Set the two to agree, or registrants will read one thing and be charged in another. Currency is set per provider, so if you have connected both, set both.
  • Switching a provider off stops new payments, not payments in flight. See Integrations. Somebody who was at the checkout when you flipped the switch still comes back to a confirmation.
  • A form left switched on without a key quietly stops charging rather than failing at submit in front of a visitor. The builder says so where you can see it.
  • Duplicating a form copies its payment settings, not its payments - those belong to the registrations they were taken for.
  • Backups carry the settings. A form exported under Settings → Data brings its payment configuration with it. API keys are site settings and are not part of a form backup.
  • On a lapsed license a form that charged stops charging and behaves like an ordinary form. Nothing already recorded is deleted, and re-activating brings it all back.
  • Refunds are made at the provider, not here. LocalForm records what the provider last reported about a payment; it never moves money on its own.
  • Refunding a WooCommerce order does not cancel the registration. The refund is recorded in the order, where it belongs, and the response stays marked paid - because it was. Cancelling somebody's place is a decision for a person, not a side effect of a bookkeeping action.
  • WooCommerce has no test mode here. Rehearsing belongs to your shop's own payment plugins, which have their own. A payment taken through WooCommerce is always recorded as live, because as far as LocalForm can honestly tell, it is.
  • A payment records which provider took it. Change a form's provider and the payments already taken keep pointing at the one that took them, so they still read back correctly.

Developers: the provider interface, the payment hooks and how to add a second gateway are in the hooks reference.