Payments and E-invoicing Setup
Flo exposes two independent money-facing surfaces, both operated per tenant:
| Surface | Configuration lives in | Gate |
|---|---|---|
| Online payments (hosted Stripe / PayPal checkout) | Backend environment variables (PAYMENT__*) | enable_online_payments |
| Italian e-invoicing (SDI through an external gateway) | Tenant database, edited in the admin UI (credentials encrypted at rest) | enable_einvoicing |
This page is the operator setup guide for both. General environment-variable semantics are documented in Environment Variables.
Feature flags and dependencies
| Flag | Effect |
|---|---|
enable_online_payments | Enables online checkout. The admin batch save rejects it unless enable_bookings and enable_einvoicing are also on (errors.payment.featureDependency). |
enable_bookings | Booking flow; the booking checkout checks it first. |
enable_einvoicing | E-invoicing bounded context. With the flag off, every /api/v1/einvoicing/* endpoint answers 404 (the feature is hidden, not merely disabled). |
enable_subscriptions | Studio-plan offers and commerce checkout; the offers endpoint returns an empty list without it. |
enable_finance | Finance sidebar section (payments, refunds, collections/reports). |
- Booking checkout (
POST /api/v1/payments/booking-checkout) requiresenable_bookings,enable_einvoicingandenable_online_payments; with any of them off it returns the translated disabled error. - Studio-plan checkout (
POST /api/v1/commerce/studio-plan-checkout) requiresenable_subscriptionsandenable_online_payments. - Flag values are cached for 60 seconds; a change takes effect within that window.
- CLI:
flo config flags set <id> enable_online_payments on(declarative form).flo instance payments <id> --enable-flagsenablesenable_online_paymentsandenable_einvoicingin one run.
See Feature Flags for the full catalog.
Payment provider configuration (environment)
Provider secrets stay in the backend environment and are never persisted to the
tenant database. PAYMENT__* keys map to the ASP.NET Payment:* configuration
section (__ is the section separator).
Stripe
| Variable | Value |
|---|---|
PAYMENT__PROVIDER | stripe (default) or paypal |
PAYMENT__STRIPE__SECRETKEY | Stripe secret key (sk_live_ / sk_test_) |
PAYMENT__STRIPE__WEBHOOKSECRET | Stripe webhook signing secret (whsec_) |
Stripe readiness requires both the secret key and the webhook secret to be set.
PayPal
| Variable | Value |
|---|---|
PAYMENT__PROVIDER | paypal |
PAYMENT__PAYPAL__ENVIRONMENT | sandbox or live; empty means live. Any other value is rejected instead of silently targeting live |
PAYMENT__PAYPAL__CLIENTID / PAYMENT__PAYPAL__CLIENTSECRET | OAuth credentials |
PAYMENT__PAYPAL__WEBHOOKID | PayPal webhook id used for signature verification |
PayPal readiness requires client id, client secret and webhook id.
Shared keys
| Variable | Value |
|---|---|
PAYMENT__SUCCESSURL / PAYMENT__CANCELURL | Redirects after checkout (default: frontend URL) |
PAYMENT__INVOICESELLER__NAME | Seller legal name for emitted invoices |
PAYMENT__INVOICESELLER__VATNUMBER | Seller VAT number |
PAYMENT__INVOICESELLER__FISCALCODE | Seller fiscal code |
PAYMENT__INVOICESELLER__REGIMEFISCALE | Fiscal regime code (e.g. RF01) |
PAYMENT__INVOICESELLER__ADDRESS / __STREETNUMBER | Street address |
PAYMENT__INVOICESELLER__POSTALCODE / __CITY / __PROVINCE / __COUNTRY | Postal code, city, province code, country (must be IT) |
PAYMENT__INVOICESELLER__EMAIL | Seller email |
The invoice seller is validated as one coherent block: if the tenant saved any
seller field in the e-invoicing settings, the whole seller comes from the
settings; otherwise the whole seller comes from the environment. Required fields
are VAT number, legal name, regime, address, postal code, city and country IT.
Runtime knobs
These ASP.NET configuration keys are overridable with the __ convention
(e.g. PAYMENT__HOLDMINUTES):
| Key | Default | Effect |
|---|---|---|
Payment:HoldMinutes | 30 | Checkout hold duration |
Payment:MaxActiveHoldsPerUser | 3 | Maximum concurrent active holds per user |
Payment:PaidReviewHoldMinutes | 1440 | Hold kept for an order awaiting operator review |
Payment:InvoiceSeries / Payment:InvoiceTimeZone | FLO / — | Invoice numbering fallbacks when the tenant settings are empty |
Payment:InvoiceEmailMaxAttempts | — | Max invoice-email attempts before the job moves to review |
Payment:FinalizationMaxAttempts | — | Max order-finalization attempts |
CLI configuration
# Stripe (test keys on sandboxes), enable both flags
flo instance payments api-pflocal-useflo-net \
--stripe-secret-key sk_test_… --stripe-webhook-secret whsec_… --enable-flags
# PayPal sandbox
flo instance payments api-pflocal-useflo-net --provider paypal \
--paypal-client-id … --paypal-client-secret … --paypal-webhook-id … \
--paypal-environment sandbox
# Invoice seller via environment (DB seller fields win per block)
flo instance payments api-pflocal-useflo-net --invoice-seller-vat … --invoice-seller-name …
# Inspect: env (masked), flags and commerce readiness; --json for machines
flo instance payments api-pflocal-useflo-net
The command writes only changed values; when env values change it restarts the
instance (skip with --no-restart). --dry-run prints the plan without writing.
Writes to a live (non-test) remote target need -y or an interactive
confirmation; --seed (test activities) is refused on live targets.
instance payments is not exposed through the --vps control plane — run it
on a local target or through the direct SSH path.
Webhook endpoints
| Provider | Endpoint |
|---|---|
| Stripe | POST /api/v1/payments/stripe/webhook |
| PayPal | POST /api/v1/payments/paypal/webhook |
- Anonymous, antiforgery-exempt, rate-limited, max 64 KB body. The browser redirect never confirms a payment: the signed webhook (or a server-side verification) is the source of truth.
- Stripe: verified locally with HMAC-SHA256 of
{timestamp}.{payload}using thewhsec_secret, read from theStripe-Signatureheader; signatures older than 5 minutes are rejected. Supported events:checkout.session.completed(only whenpayment_status=paid),checkout.session.async_payment_succeeded,checkout.session.async_payment_failed. Other events are acknowledged without a state change. - PayPal: verified remotely through
POST /v1/notifications/verify-webhook-signaturewithPAYMENT__PAYPAL__WEBHOOKID. Supported events:CHECKOUT.ORDER.APPROVED,PAYMENT.CAPTURE.COMPLETED,PAYMENT.CAPTURE.DENIED; unsupported events are acknowledged. - Webhook events are deduplicated on
(Provider, ExternalEventId). - Routing: an order reference of
commerce:<id>goes to the commerce state machine; a numeric reference goes to the booking state machine.
For local development, flo instance payments <id> --watch-stripe starts the
Stripe CLI listener, forwards to the local webhook route, captures the
whsec_… signing secret and writes it into the tenant .env; --stop-watch
stops it.
Readiness and verification
| Surface | Endpoint / command |
|---|---|
| Liveness | GET /health |
| Full readiness | GET /health/ready |
| Commerce readiness (safe JSON) | GET /health/commerce |
| Admin readiness API | GET /api/v1/payments/admin/readiness (Admin) |
| E-invoicing settings + probe | GET/PUT /api/v1/einvoicing/settings, POST /api/v1/einvoicing/settings/test-connection (Admin) |
| Operator CLI | flo instance health <id> (Commerce readiness check), flo instance payments <id> |
The readiness checks are:
| Check key | Passes when |
|---|---|
paymentProvider | The configured provider's credentials are present (stripe or paypal) |
einvoicingCredentials | Provider credentials are stored and decryptable |
invoiceSeller | The effective seller identity passes validation |
/health/commerce reports enabled: false (healthy) when
enable_online_payments is off, and ready: true/false when it is on. A
checkout is refused with errors.payment.providerUnavailable,
errors.eInvoicing.notConfigured or errors.payment.invoiceConfigurationInvalid
until all three checks pass.
Orders, holds and finalization
- Prices, VAT, currency and lines are always computed server-side; the client cannot submit amounts.
- A checkout creates a pending order plus a capacity hold (default 30 minutes, max 3 active holds per user). The hold is released on expiry or failure and converted on confirmation.
- A verified paid order is finalized asynchronously: a booking is created (or a
ClientSubscriptionfor studio plans) and one invoice job is queued. - If capacity was lost between payment and finalization, the payment is refunded
automatically (reason
capacity_refunded); if the refund itself fails, the order stays in operator review with the evidence preserved. - Admin review actions:
POST /api/v1/payments/admin/orders/{id}/reviewwithConfirmPayment,FinalizeBooking,ReleaseorRefundPayment;POST /api/v1/commerce/admin/orders/{id}/reviewwithConfirmPayment,FinalizeFulfillment,ReleaseorRefundPayment. Order lists/details are exposed under/api/v1/payments/admin/ordersand/api/v1/commerce/admin/orders. - Customers can settle a checkout that returned without a webhook through
POST /api/v1/commerce/orders/{id}/reconcile, which verifies the session at the provider and applies the same state machine as the webhook.
Background jobs, refunds and reconciliation
Two always-registered background services poll every 30 seconds:
| Service | Cycle |
|---|---|
BookingPaymentBackgroundService | Expire pending orders, finalize paid orders, process the invoice queue |
CommerceOrderBackgroundService | Same, plus the commerce email queue |
E-invoicing has no webhook: EInvoiceReconcileService polls the gateway every
5 minutes (EInvoicing:ReconcileIntervalMinutes; values below 1 fall back to
5) after a one-minute startup delay, and is a no-op unless enable_einvoicing is
on and credentials are configured. It mirrors inbound invoices and SDI status
changes using a per-provider/environment watermark (LastSyncedAt), which is
reset when the provider or environment changes.
Refunds:
- Admin refunds run only after a server-side verification of the remote payment;
provider refunds use stable idempotency keys
(
flo-refund-order-<id>,flo-refund-subscription-<id>). - A refund before invoice emission cancels the queued job. A refund after emission creates one linked TD04 credit-note draft for the operator to review and send; a partial refund at the provider is never charged twice.
- Online-paid client subscriptions can be refunded in full or for the remaining
period (
ComputeRefundAmount); the refund cancels the subscription, cancels future bookings and wakes the waiting queue. - While a refund is in flight the order is
RefundPending(fail closed); a provider failure restores the previous status.
Manual recovery endpoints:
| Action | Endpoint |
|---|---|
| Retry a booking invoice | POST /api/v1/payments/orders/{id}/invoice/retry |
| Retry a commerce invoice | POST /api/v1/commerce/admin/orders/{id}/invoice/retry |
| Reconcile one mirrored invoice | POST /api/v1/einvoicing/fatture/{id}/sync |
| Reconcile the whole mirror | POST /api/v1/einvoicing/fatture/sync |
E-invoicing (SDI)
Provider model
| Provider | Credentials | Notes |
|---|---|---|
FatturaApi | Username + password | Gateway token cached per (environment, username); cache invalidated on credential changes |
OpenApi | API token (bearer) | OpenAPI.it Invoice API |
Local | none | Mirror rows imported from FatturaPA XML; never calls a provider. Not selectable as a provider |
Environment: Test (default) or Prod. Changing provider or environment resets
the reconcile watermark and forces a full re-sync.
Default gateway URLs (overridable via configuration keys):
| Provider | Default | Override |
|---|---|---|
FatturaApi | https://fattura-elettronica-api.it/ws2.0/{test|prod} | EInvoicing:BaseUrl |
OpenApi | https://test.invoice.openapi.com / https://invoice.openapi.com | EInvoicing:OpenApiBaseUrl:Test / EInvoicing:OpenApiBaseUrl:Prod |
What the operator configures
Admin UI: Amministrazione → Fatturazione, Impostazioni tab (Admin only,
visible with enable_einvoicing):
- Provider and environment.
- Credentials: username + password (
FatturaApi) or API token (OpenApi). Secrets are write-only — the API returns onlypasswordConfigured/apiTokenConfigured, and the UI shows whether one is stored. A save is a patch: absent/empty fields keep the stored value. - Default sender VAT number, invoice series and fiscal time zone.
- Seller identity (name, VAT, fiscal code, regime, address, postal code, city, province, country, email).
- Branded courtesy PDF texts: header (≤500 chars), footer (≤500) and legal notes (≤2000).
- Test connessione probes the gateway with the stored credentials.
Credentials are encrypted with ASP.NET Data Protection (purpose
Flo.EInvoicing.Credentials.v1); plaintext is never persisted or returned. If
the key ring is rotated and a stored payload can no longer be decrypted, the
credentials are treated as not configured and must be re-entered.
CLI alternative (talks to the admin settings API, needs an admin session and waits out the 60-second flag cache):
flo instance payments api-pflocal-useflo-net \
--e-invoicing-provider FatturaApi --e-invoicing-environment Test \
--fatturaapi-username … --fatturaapi-password … \
--admin-email admin@example.com --test-connection
Use --fatturaapi-token for OpenApi. Seller fields saved in the DB settings
override PAYMENT__INVOICESELLER__*; the CLI warns when both are set.
What the customer does
The customer never sees gateway credentials. At checkout they fill in the
billing profile (name/address, VAT number or fiscal code, SDI recipient code or
PEC); Flo snapshots it on the order and sends the courtesy PDF by email after
emission (requires enable_email_sender; otherwise the email job is marked
Skipped). The seller identity always comes from the tenant configuration.
Emission flow and states
-
A finalized paid order queues exactly one invoice job (
BookingorCommerceOrdersource) with an immutable seller snapshot and a reserved invoice number. -
The worker emits through the configured gateway. On success the mirror row gets the gateway reference and SDI state, and the PDF email is queued.
-
Outcomes are classified centrally:
Class SDI states Handling Confirmed INVI,PREN,CONS,NONC,ACCE,DECOJob Submitted, email queuedDeterministic refusal ERRO,RIFIJob Failed, retryable by an admin after correctionUnknown / lost response anything else Job NeedsReview, never retried automatically -
Admin invoice retry (
.../invoice/retry) requires the job's provider and environment to match the current settings. When a remote invoice id exists, Flo syncs it first and resends only on a deterministic refusal; aNeedsReviewjob without a remote id needs an explicitproviderOutcomeConfirmedflag before it is reopened. -
Manual resend (
POST /api/v1/einvoicing/fatture/{id}/resend) is allowed only forERRO/RIFIorNEEDS_REVIEWrows and starts a new idempotent attempt. -
The mirror is reconciled by the 5-minute loop and by the manual sync endpoints. Imported FatturaPA XML (
POST /api/v1/einvoicing/fatture/import) becomes a local draft that an admin can transmit withPOST /api/v1/einvoicing/fatture/{id}/send.
Admin surfaces in the same page: Inviate (sent, with drafts and credit
notes), Ricevute (received, with unread badge), Nuova (composer, including
TD04/TD05 credit notes linked to the original), Aziende (counterparties),
Pagamenti and Commerce (order surfaces). Offline and collective invoices
are generated through POST /api/v1/einvoicing/fatture/offline/booking/{id},
.../offline/subscription/{id} and POST .../collective; GET .../export
downloads a CSV for a date range.
Error states
| Error code | Meaning |
|---|---|
errors.eInvoicing.notConfigured | No usable provider credentials |
errors.eInvoicing.authenticationFailed | Gateway rejected the credentials |
errors.eInvoicing.gatewayUnavailable | Transport failure reaching the gateway |
errors.eInvoicing.gatewayRejected | Gateway refused the request |
errors.eInvoicing.structuredInvoiceInvalid | FatturaPA payload failed validation |
errors.eInvoicing.sendFailed | Deterministic send failure (safe to retry after correction) |
errors.eInvoicing.emissionOutcomeUnknown | Lost/unknown outcome — reconcile remotely before retrying |
errors.eInvoicing.resendNotAllowed | Row is not in a resendable state |
errors.eInvoicing.invoiceImportInvalid | Imported XML is not a valid FatturaPA document |
errors.eInvoicing.invoiceNumberAlreadyUsed | Reserved number collision |
errors.eInvoicing.creditNoteSourceUnavailable / ...CreationFailed | Credit note cannot be created from the source invoice |
errors.eInvoicing.permissionDenied | Caller is not Admin (or lacks the finance e-invoicing view policy) |
Code reference
| File | Purpose |
|---|---|
Flo.BE/Controllers/PaymentsController.cs | Readiness, booking checkout, provider webhook, admin orders and retry |
Flo.BE/Controllers/CommerceController.cs | Studio-plan offers/checkout, customer reconcile, admin commerce orders |
Flo.BE/Services/Payments/StripePaymentGateway.cs / PayPalPaymentGateway.cs | Provider adapters, webhook parsing and signature verification |
Flo.BE/Services/Payments/OnlinePaymentGatewayResolver.cs | Provider selection from tenant configuration |
Flo.BE/Services/Payments/PaymentReadinessService.cs | The three readiness checks |
Flo.BE/Services/Payments/BookingPaymentService.cs / CommerceOrderService.cs | Order state machines, holds, finalization, refunds, invoice jobs |
Flo.BE/Services/Payments/SubscriptionRefundService.cs | Subscription refunds + TD04 draft |
Flo.BE/Services/Payments/BookingPaymentBackgroundService.cs / CommerceOrderBackgroundService.cs | 30-second background cycles |
Flo.BE/BoundedContexts/EInvoicing/Controllers/FattureController.cs | Invoice mirror, composer, import, resend, sync, PDFs |
Flo.BE/BoundedContexts/EInvoicing/Controllers/EInvoicingSettingsController.cs | Settings CRUD and connection probe |
Flo.BE/BoundedContexts/EInvoicing/Services/EInvoicingSettingsService.cs | Encrypted credentials, seller defaults |
Flo.BE/BoundedContexts/EInvoicing/Services/FattureService.cs | Emit / import / resend / sync state machines |
Flo.BE/BoundedContexts/EInvoicing/Services/EInvoiceReconcileService.cs | 5-minute SDI reconcile loop |
Flo.BE/BoundedContexts/EInvoicing/Services/EmissionStates.cs | Confirmed vs deterministic SDI outcome sets |
Flo.BE/BoundedContexts/EInvoicing/Services/Gateway/ | FatturaApi and OpenAPI.it clients |
cli/src/commands/instance/payments.ts | flo instance payments operator command |