| [ Web Proxy ] |
| Viewing: https://docs.stripe.com/billing/subscriptions/third-party-payment-processing | [Back] [Original] |
To process payments for your Billing subscriptions and invoices with a third-party processor, you have the following options:
Use Stripe Billing to manage subscription payments made with off-Stripe payment processors. With this integration, you can:
Stripe-Account header to perform operations on behalf of connected accountsIf you set up this integration, you no longer need to mark invoices as paid out-of-band or rely on the send_invoice method to accept off-Stripe payments.
When integrating with a third-party payment processor, youre responsible for complying with applicable legal requirements, including your agreement with your PSP, applicable laws, and so on.
Additionally, youre responsible for limiting your integration to supported locations.
Make sure that the following limitations dont conflict with your use case:
always_invoice.You can integrate your custom payment page or Payment Element with Stripe Billing to process payments off-Stripe. This payment flow relies on two Stripe concepts that enable off-Stripe payments:
charge_automatically collection method allow you to receive webhooks for future subscription charges, and to apply off-Stripe payments to associated invoices.To use Stripe Billing with off-Stripe payment processors, you must:
To set up your off-Stripe subscriptions:
In the Dashboard, create the custom payment method type your customers will pay with. Custom payment method types allow you to specify branding for the custom payment methods you define for each customer. For example, if youre using SamplePay as another processor, you might want to create a SamplePayCard to represent cards that you process with SamplePay.
Go to Custom payment method types in the Dashboard. At the end of these steps, youll have one or more custom payment method types defined that you can offer your customers when they checkout.
Make sure your custom payment method aligns with our marks policy on the usage of display name and logo.
SamplePayCard, which you can then use to set up a custom payment method. Collect customer information (email address, billing address, and shipping address) and the price your customer selected. Create a new Customer or customer-configured Account with that information, then use it and the selected price to create a Subscription.
curl https://api.stripe.com/v1/subscriptions \ -u "sk_test_wU7nrJCZspk1NPDxiQgAF05q:" \ -d "customer_account=" \ -d "items[0][price]={{CUSTOMER_ACCOUNT_ID}}" \ -d payment_behavior=default_incomplete \ -d "payment_settings[save_default_payment_method]=on_subscription" \ -d "expand[0]=latest_invoice"{{PRICE_ID}}
At this point the subscription is incomplete with an open invoice.
Redirect the customer to your payment form to collect payment. Collect payment details for the payment method selected, and send a request to your server to initiate a payment session:
const response = await fetch('/create_payment_session', { method: 'POST', data: { // If needed, include an identifier for the processor selected by the customer } }); // Redirect customer to processor to complete payment window.location.href = response.redirectUrl;
Create a server-side handler to start a new payment session on your third-party processor. Then, send a return URL to redirect your customer to where they can complete payment.
app.post('/create_payment_session', async (req, res) => {return { redirectUrl: paymentSession.redirectUrl }; });const paymentSession = processorSdk.createPaymentSession({ amount: invoice.amount_due, currency: invoice.currency, return_url: '/payment_session_completed' });
When Stripe creates an invoice for a charge_automatically subscription, it automatically creates a default PaymentIntent object to attempt payment collection. Because youre processing payments through your own processor and reporting the outcome with a PaymentRecord, Stripe cancels this default PaymentIntent. As a result, you see a Canceled payment entry next to your reported payment on each invoice. This is expected behavior and doesnt affect the invoice or subscription status.
After the customer completes their checkout session on the payment processor, create a record of payment on Stripe:
// Don't put any keys in code. See https://docs.stripe.com/keys-best-practices. const stripe = new Stripe('sk_test_wU7nrJCZspk1NPDxiQgAF05q', { apiVersion: '2026-07-29.dahlia' }); app.post('/payment_session_completed', async (req, res) => { // `paymentReference` refers to the response object provided by your third-party processor // to indicate completion of payment. The exact contents vary based on the third-party processor. const paymentMethod = await stripe.paymentMethods.create({ type: 'custom', custom: { // Set to the ID of the custom payment method type created in Step 1 type:, }, metadata: { // Store any information specific to your third-party processor // that's needed to perform off-session payments {{PROCESSOR_AGREEMENT_ID_KEY}}: '{{PROCESSOR_AGREEMENT_ID}}', }'{{CUSTOM_PAYMENT_METHOD_TYPE_ID}}'
When setting up your custom payment method and integration, dont save any sensitive payment credentials with Stripe, including PANs.
Attaching a payment record to the invoice marks it as paid and transitions the associated subscription to active. You must report a payment within 23 hours of creating the subscription, or the subscription transitions to incomplete_expired and you need to recreate it.
To process future payments for a charge_automatically subscription, configure a handler to receive webhook events. Specifically, youll handle the invoice.payment_attempt_required webhook, which Stripe sends when a new invoice is finalized and requires payment through your custom payment method:
Most integrations dont need to set auto_advance: false on an invoice. However, if your integration requires it for any reason, Stripe wont attempt payment on the invoice while auto_advance is set to false, and the invoice.payment_attempt_required webhook wont be sent. To resume normal payment collection and trigger the webhook, update the invoice to set auto_advance back to true.
// Don't put any keys in code. See https://docs.stripe.com/keys-best-practices. const stripe = new Stripe('sk_test_wU7nrJCZspk1NPDxiQgAF05q', { apiVersion: '2026-07-29.dahlia' }); app.post('/webhook', async (request, response) => { const payload = request.body; const sig = request.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(payload, sig, endpointSecret); } catch (err) { return response.status(400).send(`Webhook Error: ${err.message}`); } switch (event.type) { case 'invoice.payment_attempt_required':
When a failed payment is reported, the invoice remains open. If the Subscription was created in an incomplete status, it remains incomplete until successful payment is reported, or it expires.
If the Subscription was previously active, it transitions to past_due. If retries are configured, then the Subscription moves into dunning and we send subsequent invoice.payment_attempt_required webhook events when retries are attempted.
You can configure retries using custom retry schedules or automations. We dont support Smart Retries.
For custom retry schedules, if you exhaust all retries, the invoice and subscription might transition depending on your configured retry settings. You can also manually cancel the subscription earlier if you cant collect payment.
When retrying a failed payment, report the new attempt against the existing PaymentRecord object using report_payment_attempt or report_payment_attempt_guaranteed. Dont create a new PaymentRecord with report_payment. Creating a separate PaymentRecord for each attempt results in multiple distinct payment entries on the invoice (for example, one Failed and one Succeeded), rather than a single payment with a consolidated history of attempts.
To handle payment retries, extend the handler implemented in the Configure webhook handler section:
async function processOffSessionPayment(invoice) { const customPaymentMethod = await stripe.paymentMethods.retrieve(invoice.defaultPaymentMethod); // No changes needed, collect payment as before from the third-party processor const paymentResult = await processorSdk.collectPayment({ amount: invoice.amount_remaining, agreement_id: customPaymentMethod.metadata['{{PROCESSOR_AGREEMENT_ID_KEY}}'] }); // Query for any existing payment records, which indicate a prior payment attempt const invoicePayments = await stripe.invoicePayments.list({ invoice: invoice.id, payment: { type: 'payment_record' } }); if (invoicePayments.data.length) {
You can report canceled payments to Stripe when using the asynchronous payment flow. Use this option when a payment is initiated but later canceled.
To report a canceled payment, first create a payment record indicating the payment is initiated (as shown in Record payment).
When the payment is canceled, report the cancellation to Stripe:
// Don't put any keys in code. See https://docs.stripe.com/keys-best-practices. const stripe = new Stripe('sk_test_wU7nrJCZspk1NPDxiQgAF05q', { apiVersion: '2026-07-29.dahlia' }); app.post('/payment_session_canceled', async (req, res) => { await stripe.paymentRecords.reportPaymentAttemptCanceled(paymentRecord.id, { canceled_at: Date.now() }); });
When your third-party processor processes a refund, you can report it to Stripe to maintain accurate payment records. You can report both full and partial refunds, but only after the original payment is successfully recorded in Stripe.
To report a refund, you need to provide the refund reference from your third-party processor, which must be unique for each Payment Record. For a partial refund, specify the amount parameter.
To maintain accurate accounting data, log refunds on issued invoices by using Credit Notes to adjust the invoice amounts.
// Don't put any keys in code. See https://docs.stripe.com/keys-best-practices. const stripe = new Stripe('sk_test_wU7nrJCZspk1NPDxiQgAF05q', { apiVersion: '2026-07-29.dahlia' }); app.post('/payment_refunded', async (req, res) => { const paymentRecordRefund = await stripe.paymentRecords.reportRefund(paymentRecord.id, { // `refundReference` refers to the response object provided by your third-party processor // when the refund is issued. The exact contents vary based on the third-party processor. processor_details: { type: 'custom', custom: { refund_reference: refundReference.id } }, outcome: 'refunded', refunded: { refunded_at: Date.now() } }); const invoicePayments = await stripe.invoicePayments.list({ payment: { type: 'payment_record', payment_record: paymentRecordId } }); // Create a credit note to reflect the refund on the invoice await stripe.creditNotes.create({ invoice: invoicePayments.data[0].invoice, refunds: [{ type: 'payment_record_refund', // amount_refunded is an optional field. If not provided, the credit note is created for the full amount of the PaymentRecord refund. amount_refunded: paymentRecordRefund.refund_details[0].amount_refunded.value, payment_record_refund: { payment_record: paymentRecordRefund.id, refund_group: refundReference.id } } ] }); });
Customers can use the customer portal to manage their own subscriptions that use custom payment methods, which reduces their need for interaction with your support team. The customer portal supports the following functionality for subscriptions with custom payment methods:
Billing volume from third-party processors is considered part of your total billing volume, which includes transactions both on and off Stripe that use Stripe Billing functionality. The standard fee structure applies based on your billing contract, either pay-as-you-go or a subscription plan. For more information, see Billing pricing and how Stripe charges for Billing.
In addition, one-off invoices that are paid through a third-party payment processor are monetized under Invoicing pricing. Invoices marked as paid out-of-band arent charged as usual.
| Web Proxy Viewer | New URL | Original Page |