Guide 10 · Developer docs

Forms and data capture

Building forms with the Forms module: how a form is put together and placed on a page, validating and branching, presenting it, where submissions are stored, and what happens after one arrives, from jobs and records to CRMs and payments.

L1–L2
Unit 06 · Acting on a submission

Taking payments

Taking a donation, fee or deposit inside a form: where a form ends and a checkout begins, the setup, what a payment creates, and sending a receipt.

Who this is for. The developer adding a donation, fee or deposit to a form. After this page you can tell whether a payment belongs in a form or a checkout, set the element up, say what a payment creates, and send the payer a receipt.

A form can take a payment: a donation, an event fee, a deposit, a membership renewal. The Payment element adds the provider's card fields or buttons to a step, charges the visitor when the form is submitted, and records the payment against the submission. It has a clear boundary: a single amount from a single payer is a form, and anything that looks like a purchase belongs in a checkout. This page covers that boundary, the setup, what is created when the payment goes through, and the receipt.

A form or a checkout

A form is fine forUse a checkout for
One amount: fixed, picked from a few presets, or typed by the payerProducts, quantities, or more than one item
A donation, a fee, a deposit, a single registrationAnything with stock that can run out
No tax, or tax already included in a fixed amountTax calculated by where the payer is or what they buy
Nothing to postShipping
No discountsDiscount codes, member prices, promotions
A receipt you send yourselfOrder confirmation, fulfilment, returns and per-item refunds

The test: if the answer to "what did they buy?" is a product, it is a checkout. A form payment creates an invoice and nothing else: no order, no stock movement, no shipment, no tax line. Rebuilding those around a form is how a "simple payment form" becomes the hardest part of a site to maintain.

Setting it up

  1. Payment methods. Each provider is a payment method record under Invoices › Payment methods: Stripe, PayPal, PayPal Donate, or the Mock Pay test provider. Their setup is under The four payment methods.
  2. The element. Add a Payment element to the form, usually alone on the last step, after every question. Its settings:
SettingWhat it does
CurrencyThe currency of every amount on the element
AmountsPreset amounts the payer picks from. The server accepts only one of these, unless a custom amount is allowed
Allow Custom AmountThe payer may type any amount above zero. Show the amount back to the payer before they pay
Payment MethodsWhich active methods to offer. Several are shown as tabs
Authorize & Capture / Authorize OnlyUse Authorize & Capture, which takes the payment when the form is submitted
Billing address fieldsWhich address fields to ask for, and which are required. They are stored as the invoice's billing address
  1. The script. The element prints the providers' mount points; the platform's script makes them work (js-payment-element.js reference). Load it in the form layout, with the page's nonce, on both submission models:
Template code
<script src="/assets/forms/js-payment-element.js" nonce="{$_inlineScriptNonce}" defer></script>
<script src="/assets/forms/js-payment-element.js" nonce="{{ _inlineScriptNonce }}" defer></script>

On a page-per-step form the script catches the submit, gets a token from the provider and submits again with it. On a JS form it takes over the container's data-submit-modifier-callback and data-submit-result-callback, handles the provider's responses, and then calls the callbacks the markup named before it ran. So the result callback from the validation page keeps working, as long as its attribute is in the markup when the page loads. Do not put an invisible captcha on the payment step: both take over the submit (see Captcha).

What happens on submit

  1. The payment is attempted only when every other answer on the form is valid. A visitor who mistyped an email is told so before any money moves.
  2. The server checks the amount (above zero, and one of the presets unless custom amounts are allowed) and the chosen method (offered on the element, and active).
  3. It takes an invoice for the submission: an open invoice of the submission is reused and any other open one is voided, or a new one is created. The invoice has one line, named after the form, with no tax and no discount, and the billing address, and it records the submission as its source.
  4. It charges the payment through the provider.
  5. On success the invoice is paid, the payment is stored as the element's answer, and the submission completes, which fires the form's Completed jobs. The answer, under the element's name in data, holds invoiceId, amount (formatted in US English, so an Australian-dollar payment reads A$25.00) and transactionId.
  6. On failure the payer stays on the payment step with the provider's message, and the submission stays incomplete.

Two providers change the order. Plain PayPal may send the payer to PayPal and back. PayPal Donate with a hosted button creates a draft invoice and leaves the submission incomplete until PayPal's notification confirms the donation, minutes later: Completed fires then, not when the payer clicked. Anything that reacts to the payment must hang off Completed, never off the visitor reaching the result step.

Sending a receipt

The platform sends no receipt for a form payment. Send one from a job on the form's Completed event, to the payer's address, with the amount and invoice number from the payment answer. Here the element is named donation:

Template code templates/hooks/emails/donation-receipt.html
{$answers = $event->getRecord()->data}
{$payment = $answers['donation']|default:[]}
<!DOCTYPE html>
<html><body style="font-family:Arial,sans-serif;font-size:14px;color:#333333;">
  <p>Thank you for your donation of {$payment['amount']|default:''|escape}.</p>
  <p>Receipt number: {$payment['invoiceId']|default:''|escape}</p>
  <p>Keep this email for your records.</p>
</body></html>
{% set payment = event.getRecord().data.donation|default({}) %}
<!DOCTYPE html>
<html><body style="font-family:Arial,sans-serif;font-size:14px;color:#333333;">
  <p>Thank you for your donation of {{ payment.amount|default('')|e }}.</p>
  <p>Receipt number: {{ payment.invoiceId|default('')|e }}</p>
  <p>Keep this email for your records.</p>
</body></html>

A tax receipt for a deductible donation has legal content of its own (the charity's registration number, wording the tax office requires); get it from the client, and keep it in the template, not in the form.

The guard and ordering advice for confirmation emails applies here too: see Jobs, notifications and confirmations.

Before launch

  • Test every offered method end to end on a development site with the providers' test modes, including a declined card, and read the invoice it creates.
  • Read 3-D Secure, declines and retries for how card challenges and declines are handled.
  • Make sure no Mock Pay method is active on the live account and no form offers one (see The mock provider). Mock Pay reports every payment as successful and takes no money.
  • Refunds are made from the invoice, through the provider.
Go deeper

Was this page helpful?

Your answer goes to the docs team.