Skip to content

Payments

Each workspace connects its own Stripe or PayPal account. Money goes from the respondent to that account; FormFlow never holds funds.

This replaces the old setup in which one platform PayPal account (PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, PAYPAL_WEBHOOK_ID, PAYPAL_ENV) served every form. Those variables are no longer read for form payments. Plan billing for FormFlow itself is separate and still uses STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET.

Connect an account

Needs the workspace:manage scope. Credentials are verified with the provider, stored encrypted per workspace and never returned by the API.

PathHowNotes
Stripe restricted or secret keyPOST /payments/connectionsBody kind stripe_key with secretKey. Register the returned webhookUrl and webhookEvents in Stripe, then send the signing secret with kind stripe_webhook_secret.
PayPalPOST /payments/connectionsBody kind paypal with clientId, clientSecret, env (sandbox or live) and the webhookId of a webhook you created in PayPal.
Stripe ConnectPOST /payments/connections/stripe/authorizeReturns an OAuth URL. Works only when the instance operator set STRIPE_CONNECT_CLIENT_ID and STRIPE_CONNECT_WEBHOOK_SECRET.
bash
curl -X POST https://YOUR_HOST/api/v1/payments/connections \
  -H "Authorization: Bearer $FORMFLOW_API_KEY" -H "Content-Type: application/json" \
  -d '{"kind":"stripe_key","secretKey":"rk_test_..."}'
# data.webhookUrl and data.webhookEvents: add them in the Stripe dashboard, then
curl -X POST https://YOUR_HOST/api/v1/payments/connections \
  -H "Authorization: Bearer $FORMFLOW_API_KEY" -H "Content-Type: application/json" \
  -d '{"kind":"stripe_webhook_secret","webhookSecret":"whsec_..."}'

Until the webhook is configured a payment stays pending, because the redirect from the provider is not proof of payment. GET /payments/connections reports webhookConfigured and lastError.

Square, Mollie, Braintree, Authorize.Net, Paystack and Mercado Pago

ProviderConnect withCheckout and confirmation
Squarekind square: accessToken, locationId, env, signatureKeyHosted Payment Link (cards, Apple Pay, Google Pay). Webhook payment.updated verified with HMAC-SHA256 over the exact URL and body.
Molliekind mollie: apiKeyHosted checkout (iDEAL, cards, Apple Pay and more). Mollie sends an unsigned id; FormFlow fetches the payment with your key and believes only that response.
Paystackkind paystack: secretKey (sk_test_ sandbox, sk_live_ live)Hosted checkout in NGN, GHS, ZAR, KES, USD and more. Needs the respondent's email, taken from the form's first email question. Webhook verified with HMAC-SHA512 (x-paystack-signature); events charge.success and refund.processed.
Mercado Pagokind mercadopago: accessToken (TEST- sandbox, APP_USR- live), webhookSecretCheckout Pro in ARS, BRL, CLP, COP, MXN, PEN and UYU. Notifications verified with x-signature (HMAC-SHA256, 10 minute window), then the payment is fetched with your token. Save the signing secret with kind mercadopago_webhook_secret once the webhook URL is known.
Braintreekind braintree: merchantId, publicKey, privateKey, envNo hosted page: start checkout, tokenize with Braintree's browser SDK using the returned client token, then POST /public/payments/{submissionId}/charge with the nonce.
Authorize.Netkind authorizenet: apiLoginId, transactionKey, env, signatureKeyTokenize with Accept.js, then POST /public/payments/{submissionId}/charge with dataDescriptor and dataValue. Webhooks verified with X-ANET-Signature (HMAC-SHA512).

The amount is always the server-priced total stored when checkout started; the charge body cannot change it. These providers handle one-off payments and donations only; subscriptions need Stripe or PayPal. Apple Pay and Google Pay appear on Stripe Checkout, Square and Mollie when enabled in that provider's dashboard; set stripePaymentMethodConfiguration (a pmc_... id) in the form's payment settings to choose which Stripe wallets are offered. The payment field's provider picks the account.

Apple Pay and Google Pay

Choose wallets per provider in PUT /forms/{id}/payment-settings (MCP set_payment_settings): wallets: { square: { applePay, googlePay }, mollie: { applePay }, braintree: { applePay, googlePay }, authorizenet: { googlePay } }. Nothing is on unless you switch it on.

ProviderWhat the setting doesProvider-side requirement
SquareTells the hosted payment link to offer Apple Pay and Google Pay.Enable the wallets in the Square dashboard; Apple Pay on a hosted Square link needs no domain file from you.
MollieapplePay adds a shortcut: POST /public/payments/{submissionId}/checkout?wallet=applePay opens Mollie's page on Apple Pay. Mollie has no Google Pay.Enable Apple Pay in the Mollie dashboard (Settings, Payment methods). The hosted page needs no domain file from you.
BraintreeAfter submit the form shows Apple Pay and Google Pay buttons that tokenize with Braintree's browser SDK; the server charges the stored amount.Apple Pay: register your form domain in the Braintree control panel (Settings, Processing, Apple Pay) and host Apple's domain association file at /.well-known/apple-developer-merchantid-domain-association on that domain. Google Pay: production needs Google's merchant approval.
Authorize.NetGoogle Pay button; the Google Pay token is sent to the charge endpoint as dataDescriptor COMMON.GOOGLE.INAPP.PAYMENT.Google Pay merchant approval for production. Apple Pay is not offered: it needs merchant validation with an Apple certificate that FormFlow does not hold.
StripeInformational. Stripe's wallets come from stripePaymentMethodConfiguration.Verify the domain in the Stripe dashboard (Settings, Payment method domains).

The wallet buttons need the browser to allow scripts from the provider (Braintree, Google Pay); on a page that blocks them the buttons simply do not appear. Wallet flows are not verified against live accounts.

Tax, shipping and subscriptions

PUT /forms/{id}/payment-settings (form:write) sets rules per form: a tax rate with optional regions taken from an answer, shipping methods with free-over thresholds, and a subscription interval with an optional trial. Respondents see the same numbers the checkout charges from through POST /public/forms/{slug}/quote.

Reconcile, sync and refund

GET /payments lists payments with mismatches flagged (paid without a response, amount differences). POST /payments/{id}/sync asks the provider for the current state and repairs a missed webhook. POST /payments/{id}/refund needs confirmAmountMinor equal to the amount refunded, so a stale screen cannot refund a different sum. Subscription charges are refunded in the provider dashboard.

What FormFlow does not claim

Card data never reaches FormFlow: respondents pay on Stripe Checkout or PayPal hosted pages, and FormFlow stores provider ids, amounts and statuses. FormFlow makes no PCI claim and no certification claim. Every provider path is unverified against the live services; see Status.