Skip to main content

Payments and E-invoicing Setup

Flo exposes two independent money-facing surfaces, both operated per tenant:

SurfaceConfiguration lives inGate
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​

FlagEffect
enable_online_paymentsEnables online checkout. The admin batch save rejects it unless enable_bookings and enable_einvoicing are also on (errors.payment.featureDependency).
enable_bookingsBooking flow; the booking checkout checks it first.
enable_einvoicingE-invoicing bounded context. With the flag off, every /api/v1/einvoicing/* endpoint answers 404 (the feature is hidden, not merely disabled).
enable_subscriptionsStudio-plan offers and commerce checkout; the offers endpoint returns an empty list without it.
enable_financeFinance sidebar section (payments, refunds, collections/reports).
  • Booking checkout (POST /api/v1/payments/booking-checkout) requires enable_bookings, enable_einvoicing and enable_online_payments; with any of them off it returns the translated disabled error.
  • Studio-plan checkout (POST /api/v1/commerce/studio-plan-checkout) requires enable_subscriptions and enable_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-flags enables enable_online_payments and enable_einvoicing in 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​

VariableValue
PAYMENT__PROVIDERstripe (default) or paypal
PAYMENT__STRIPE__SECRETKEYStripe secret key (sk_live_ / sk_test_)
PAYMENT__STRIPE__WEBHOOKSECRETStripe webhook signing secret (whsec_)

Stripe readiness requires both the secret key and the webhook secret to be set.

PayPal​

VariableValue
PAYMENT__PROVIDERpaypal
PAYMENT__PAYPAL__ENVIRONMENTsandbox or live; empty means live. Any other value is rejected instead of silently targeting live
PAYMENT__PAYPAL__CLIENTID / PAYMENT__PAYPAL__CLIENTSECRETOAuth credentials
PAYMENT__PAYPAL__WEBHOOKIDPayPal webhook id used for signature verification

PayPal readiness requires client id, client secret and webhook id.

Shared keys​

VariableValue
PAYMENT__SUCCESSURL / PAYMENT__CANCELURLRedirects after checkout (default: frontend URL)
PAYMENT__INVOICESELLER__NAMESeller legal name for emitted invoices
PAYMENT__INVOICESELLER__VATNUMBERSeller VAT number
PAYMENT__INVOICESELLER__FISCALCODESeller fiscal code
PAYMENT__INVOICESELLER__REGIMEFISCALEFiscal regime code (e.g. RF01)
PAYMENT__INVOICESELLER__ADDRESS / __STREETNUMBERStreet address
PAYMENT__INVOICESELLER__POSTALCODE / __CITY / __PROVINCE / __COUNTRYPostal code, city, province code, country (must be IT)
PAYMENT__INVOICESELLER__EMAILSeller 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):

KeyDefaultEffect
Payment:HoldMinutes30Checkout hold duration
Payment:MaxActiveHoldsPerUser3Maximum concurrent active holds per user
Payment:PaidReviewHoldMinutes1440Hold kept for an order awaiting operator review
Payment:InvoiceSeries / Payment:InvoiceTimeZoneFLO / —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​

ProviderEndpoint
StripePOST /api/v1/payments/stripe/webhook
PayPalPOST /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 the whsec_ secret, read from the Stripe-Signature header; signatures older than 5 minutes are rejected. Supported events: checkout.session.completed (only when payment_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-signature with PAYMENT__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​

SurfaceEndpoint / command
LivenessGET /health
Full readinessGET /health/ready
Commerce readiness (safe JSON)GET /health/commerce
Admin readiness APIGET /api/v1/payments/admin/readiness (Admin)
E-invoicing settings + probeGET/PUT /api/v1/einvoicing/settings, POST /api/v1/einvoicing/settings/test-connection (Admin)
Operator CLIflo instance health <id> (Commerce readiness check), flo instance payments <id>

The readiness checks are:

Check keyPasses when
paymentProviderThe configured provider's credentials are present (stripe or paypal)
einvoicingCredentialsProvider credentials are stored and decryptable
invoiceSellerThe 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 ClientSubscription for 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}/review with ConfirmPayment, FinalizeBooking, Release or RefundPayment; POST /api/v1/commerce/admin/orders/{id}/review with ConfirmPayment, FinalizeFulfillment, Release or RefundPayment. Order lists/details are exposed under /api/v1/payments/admin/orders and /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:

ServiceCycle
BookingPaymentBackgroundServiceExpire pending orders, finalize paid orders, process the invoice queue
CommerceOrderBackgroundServiceSame, 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:

ActionEndpoint
Retry a booking invoicePOST /api/v1/payments/orders/{id}/invoice/retry
Retry a commerce invoicePOST /api/v1/commerce/admin/orders/{id}/invoice/retry
Reconcile one mirrored invoicePOST /api/v1/einvoicing/fatture/{id}/sync
Reconcile the whole mirrorPOST /api/v1/einvoicing/fatture/sync

E-invoicing (SDI)​

Provider model​

ProviderCredentialsNotes
FatturaApiUsername + passwordGateway token cached per (environment, username); cache invalidated on credential changes
OpenApiAPI token (bearer)OpenAPI.it Invoice API
LocalnoneMirror 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):

ProviderDefaultOverride
FatturaApihttps://fattura-elettronica-api.it/ws2.0/{test|prod}EInvoicing:BaseUrl
OpenApihttps://test.invoice.openapi.com / https://invoice.openapi.comEInvoicing: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 only passwordConfigured / 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​

  1. A finalized paid order queues exactly one invoice job (Booking or CommerceOrder source) with an immutable seller snapshot and a reserved invoice number.

  2. 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.

  3. Outcomes are classified centrally:

    ClassSDI statesHandling
    ConfirmedINVI, PREN, CONS, NONC, ACCE, DECOJob Submitted, email queued
    Deterministic refusalERRO, RIFIJob Failed, retryable by an admin after correction
    Unknown / lost responseanything elseJob NeedsReview, never retried automatically
  4. 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; a NeedsReview job without a remote id needs an explicit providerOutcomeConfirmed flag before it is reopened.

  5. Manual resend (POST /api/v1/einvoicing/fatture/{id}/resend) is allowed only for ERRO/RIFI or NEEDS_REVIEW rows and starts a new idempotent attempt.

  6. 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 with POST /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 codeMeaning
errors.eInvoicing.notConfiguredNo usable provider credentials
errors.eInvoicing.authenticationFailedGateway rejected the credentials
errors.eInvoicing.gatewayUnavailableTransport failure reaching the gateway
errors.eInvoicing.gatewayRejectedGateway refused the request
errors.eInvoicing.structuredInvoiceInvalidFatturaPA payload failed validation
errors.eInvoicing.sendFailedDeterministic send failure (safe to retry after correction)
errors.eInvoicing.emissionOutcomeUnknownLost/unknown outcome — reconcile remotely before retrying
errors.eInvoicing.resendNotAllowedRow is not in a resendable state
errors.eInvoicing.invoiceImportInvalidImported XML is not a valid FatturaPA document
errors.eInvoicing.invoiceNumberAlreadyUsedReserved number collision
errors.eInvoicing.creditNoteSourceUnavailable / ...CreationFailedCredit note cannot be created from the source invoice
errors.eInvoicing.permissionDeniedCaller is not Admin (or lacks the finance e-invoicing view policy)

Code reference​

FilePurpose
Flo.BE/Controllers/PaymentsController.csReadiness, booking checkout, provider webhook, admin orders and retry
Flo.BE/Controllers/CommerceController.csStudio-plan offers/checkout, customer reconcile, admin commerce orders
Flo.BE/Services/Payments/StripePaymentGateway.cs / PayPalPaymentGateway.csProvider adapters, webhook parsing and signature verification
Flo.BE/Services/Payments/OnlinePaymentGatewayResolver.csProvider selection from tenant configuration
Flo.BE/Services/Payments/PaymentReadinessService.csThe three readiness checks
Flo.BE/Services/Payments/BookingPaymentService.cs / CommerceOrderService.csOrder state machines, holds, finalization, refunds, invoice jobs
Flo.BE/Services/Payments/SubscriptionRefundService.csSubscription refunds + TD04 draft
Flo.BE/Services/Payments/BookingPaymentBackgroundService.cs / CommerceOrderBackgroundService.cs30-second background cycles
Flo.BE/BoundedContexts/EInvoicing/Controllers/FattureController.csInvoice mirror, composer, import, resend, sync, PDFs
Flo.BE/BoundedContexts/EInvoicing/Controllers/EInvoicingSettingsController.csSettings CRUD and connection probe
Flo.BE/BoundedContexts/EInvoicing/Services/EInvoicingSettingsService.csEncrypted credentials, seller defaults
Flo.BE/BoundedContexts/EInvoicing/Services/FattureService.csEmit / import / resend / sync state machines
Flo.BE/BoundedContexts/EInvoicing/Services/EInvoiceReconcileService.cs5-minute SDI reconcile loop
Flo.BE/BoundedContexts/EInvoicing/Services/EmissionStates.csConfirmed vs deterministic SDI outcome sets
Flo.BE/BoundedContexts/EInvoicing/Services/Gateway/FatturaApi and OpenAPI.it clients
cli/src/commands/instance/payments.tsflo instance payments operator command