Payment providers
The Stripe and manual adapters, test mode, and what is not built.
Adapters live in features/integrations/payment: stripe and manual. A PaymentProvider record stores a name, a code starting pp_, whether it is installed, and the adapter name for each operation (create, capture, refund, status, link, webhook). The list page at /dashboard/platform/payment-providers shows each provider and whether its keys are present: configured, missing keys, or disabled.
Seeded providers are pp_stripe (Stripe, test) and pp_manual (check or wire).
Stripe test mode
Card payment on an invoice uses Stripe in test mode in development. Set NEXT_PUBLIC_STRIPE_KEY to a pk_test_ key and STRIPE_SECRET_KEY. The payment form does not load unless the publishable key starts with pk_test_.
The flow:
- The client opens
/invoice/<token>.payInvoicecreates a Stripe payment intent for the balance and saves a payment session, keyed so a repeat returns the same intent. - The browser loads Stripe.js and shows the payment element. Stripe test card
4242 4242 4242 4242works. - After payment Stripe returns the client to the invoice page with the payment intent in the URL.
confirmInvoicePaymentasks Stripe for the intent, checks that it succeeded and that the amount and currency match, then records the payment, updates the invoice, and posts a client funds receipt. Repeating it does not add a second payment.
Refunds
Refunds go through the same adapter to the original payment. See Approvals and refunds.
Manual
recordClientPayment records a check, wire, or cash payment taken outside the app. The adapter exists for symmetry but manual payments are not refunded through it.
Not built
- Stripe events arrive at
POST /api/integrations/payment/pp_stripe/webhook. The route verifies the signature, records each event inPaymentWebhookEvent, and settles the invoice onpayment_intent.succeeded. Refund events are recorded asfailedfor operator review; refunds are settled byrefundClientPayment. - Payment provider changes need
canManagePayments, and credentials are never readable through GraphQL. - Live Stripe keys are not part of this documentation.