> ## Documentation Index
> Fetch the complete documentation index at: https://primer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Use Stripe Billing with Primer

> Use Stripe Billing as your subscription engine alongside the advantages of using Primer for processing every subscription payment.

Stripe Billing is the system of record for plans, subscriptions, and invoices. Primer executes every payment, with vaulting, network tokens, routing across processors, and empowers you to control when, how and if any charge is retried.

The integration approach addresses the goals we hear most often from subscription merchants: keeping every retry informed by the decline response, protecting your standing with issuing banks and card schemes, and diversifying processing across providers, while Stripe Billing remains the subscription engine.

## Things to plan for

* Confirm with your Stripe account team that [third-party payment processing](https://docs.stripe.com/billing/subscriptions/third-party-payment-processing) (custom payment methods and payment records) is enabled for your account, and that your Stripe business entity country is supported.
* The collection surface for these payments is Primer Checkout. Stripe Checkout and Stripe's hosted invoice page do not apply to payments processed outside Stripe.
* Stripe's automatic recovery emails do not apply either; customer communications are yours, as covered below.
* Refunds are executed through Primer and reported to Stripe. Disputes are managed with the processor that handled the payment, supported by Primer's [Dispute Webhooks](/docs/disputes/manage-disputes).
* Only subscription invoices can be collected this way. One-off invoices can still be created in Stripe but not collected through Primer.
* Stripe Billing fees apply to billing volume including payments processed outside Stripe. Confirm the treatment on your plan with Stripe.

## How it works

When using Stripe Billing with Primer, the plan catalogue and live subscription state is kept in Stripe Billing and the payment processing layer is kept on Primer. Stripe supports this natively for payments processed outside Stripe, built on two objects:

* **Custom payment methods.** A Stripe object that references a payment method held outside Stripe. The Primer token reference is stored in its metadata, and it works with automatically charged subscriptions, so Stripe continues to drive the billing cycle and signals when to collect.
* **Payment records.** How each Primer-processed outcome is reported back to Stripe, keeping invoices and subscription states accurate.

### Prerequisites

* A Primer account with at least one processor connected and a Workflow configured. See [Connect a processor](/docs/get-started/connect-a-processor).
* A Primer API key with the `client_tokens:write` and `transactions:authorize` scopes. See [Authentication](/docs/api-reference/get-started/authentication).
* A Stripe account with Stripe Billing and [third-party payment processing](https://docs.stripe.com/billing/subscriptions/third-party-payment-processing) enabled. Request access from your Stripe account team, and confirm that your business country is [supported](https://docs.stripe.com/billing/subscriptions/third-party-payment-processing#supported-countries).

### Configure Stripe

In the Stripe Dashboard:

1. Create a [custom payment method type](https://dashboard.stripe.com/settings/custom_payment_methods) to represent payment methods processed by Primer. Store its ID in your configuration, because custom payment method types can't be retrieved through the Stripe API.
2. Register a webhook endpoint and subscribe it to `invoice.payment_attempt_required`, `invoice.updated`, and `invoice.payment_failed`.
3. Configure your retry schedule, either in your [revenue recovery retry settings](https://dashboard.stripe.com/revenue-recovery/retries) or with [Billing automations](https://docs.stripe.com/billing/automations). See [Renewals and decline-aware retries](#renewals-and-decline-aware-retries).

### Sign-Up and first payment

```mermaid theme={"dark"}
sequenceDiagram
    participant C as Customer
    participant M as Merchant backend
    participant S as Stripe Billing
    participant P as Primer
    participant X as PSP
    C->>M: Selects a plan
    M->>S: Create customer + subscription (payment_behavior: default_incomplete)
    S-->>M: Subscription incomplete + open invoice (amount includes tax and proration)
    M->>P: Create client session (amount = invoice due, vault on success)
    C->>P: Pays via Primer Checkout (CIT)
    P->>X: Authorise + capture
    X-->>P: Approved
    P-->>M: Success + vaulted token
    M->>S: Create custom payment method, set as default (Primer token reference in metadata)
    M->>S: Report payment, attach to invoice
    Note over S: Invoice paid, subscription active
```

*Figure 1. Sign-Up and first payment: Stripe raises the invoice, Primer collects, the result is reported back.*

The customer and subscription are created first because the chargeable amount comes from Stripe's invoice (tax, proration). The customer then pays through Primer Checkout, the payment method is vaulted, and a custom payment method carrying the Primer token reference is attached to the customer and set as the subscription default. Reporting the payment marks the invoice paid and activates the subscription.

#### Create the subscription in Stripe

Create the customer and subscription in Stripe as you do today, with `payment_behavior` set to `default_incomplete`. Expand `latest_invoice` to get the amount to charge, which includes tax, discounts, and proration.

```bash Create the subscription theme={"dark"}
curl https://api.stripe.com/v1/subscriptions \
  -u "<STRIPE_SECRET_KEY>:" \
  -d customer="<STRIPE_CUSTOMER_ID>" \
  -d "items[0][price]"="<STRIPE_PRICE_ID>" \
  -d payment_behavior=default_incomplete \
  -d "expand[0]"=latest_invoice
```

The subscription is now `incomplete`, with an open invoice. Keep the subscription ID, the invoice ID, and the invoice's `amount_due` and `currency` for the next step.

<Warning>
  Report the first payment within 23 hours of creating the subscription. Otherwise, the subscription moves to `incomplete_expired` and you need to create it again.
</Warning>

#### Collect the first payment with Primer

Create a client session for the invoice amount with `vaultOnSuccess` enabled, then render Primer Checkout with the returned client token.

```bash Create a client session theme={"dark"}
curl -X POST 'https://api.sandbox.primer.io/client-session' \
  --header 'X-Api-Key: <PRIMER_API_KEY>' \
  --header 'X-Api-Version: 2.4' \
  --header 'Content-Type: application/json' \
  --data '{
    "orderId": "<STRIPE_INVOICE_ID>",
    "customerId": "<YOUR_CUSTOMER_ID>",
    "amount": <INVOICE_AMOUNT_DUE>,
    "currencyCode": "USD",
    "paymentMethod": {
      "paymentType": "FIRST_PAYMENT",
      "vaultOnSuccess": true
    },
    "metadata": {
      "stripeSubscriptionId": "<STRIPE_SUBSCRIPTION_ID>",
      "stripeInvoiceId": "<STRIPE_INVOICE_ID>"
    }
  }'
```

<Note>
  Stripe and Primer both express amounts in minor units, so you can pass `amount_due` unchanged. Stripe returns currency codes in lowercase, and Primer expects uppercase ISO 4217 codes.
</Note>

#### Link the payment method and report the payment

When the first payment succeeds, retrieve the customer's vaulted payment method token from Primer with `GET /payment-instruments`. Then, in Stripe:

1. Create a custom payment method that stores the Primer token in its metadata.
2. Attach it to the customer and set it as the subscription's default payment method.
3. Report the payment and attach the payment record to the invoice.

```javascript Link and report the first payment theme={"dark"}
// primerPayment: the successful Primer payment for the first invoice
// primerToken: the vaulted payment method token from GET /payment-instruments
const paymentMethod = await stripe.paymentMethods.create({
  type: 'custom',
  custom: { type: process.env.STRIPE_CUSTOM_PAYMENT_METHOD_TYPE_ID },
  metadata: { primer_payment_method_token: primerToken },
});

await stripe.paymentMethods.attach(paymentMethod.id, { customer: stripeCustomerId });
await stripe.subscriptions.update(subscriptionId, {
  default_payment_method: paymentMethod.id,
});

const now = Math.floor(Date.now() / 1000);
const paymentRecord = await stripe.paymentRecords.reportPayment({
  amount_requested: { value: invoice.amount_due, currency: invoice.currency },
  payment_method_details: { payment_method: paymentMethod.id },
  customer_details: { customer: stripeCustomerId },
  initiated_at: now,
  customer_presence: 'on_session',
  processor_details: { type: 'custom', custom: { payment_reference: primerPayment.id } },
  outcome: 'guaranteed',
  guaranteed: { guaranteed_at: now },
});

await stripe.invoices.attachPayment(invoice.id, { payment_record: paymentRecord.id });
```

Attaching the payment record marks the invoice as paid and moves the subscription to `active`.

<Note>
  **Good to know**

  * The first payment is initiated by your backend with no webhook prompt. Renewal webhooks begin from the first renewal onward.
  * Report the first payment within 23 hours of creating the subscription, or the subscription expires as incomplete.
  * If the first payment fails, there will be an open invoice for the customer, and you should retry charging the customer.
  * For every invoice, Stripe creates a default PaymentIntent and cancels it when you report a payment record. The canceled PaymentIntent appears next to your reported payment and doesn't affect the invoice or subscription status. Webhooks events for these events can be ignored entirely
</Note>

### Renewals and decline-aware retries

```mermaid theme={"dark"}
sequenceDiagram
    participant S as Stripe Billing
    participant M as Merchant backend
    participant P as Primer
    participant X as PSPs
    S->>M: Webhook: invoice.payment_attempt_required
    M->>P: MIT payment with vaulted token (paymentType: SUBSCRIPTION)
    P->>X: Route per workflow (network tokens, fallback)
    X-->>P: Result
    P-->>M: Approved or declined, with decline code
    alt Approved
        M->>S: Report payment: invoice paid
    else Declined
        M->>S: Report attempt failed
        Note over S: Retry policy schedules the next attempt
        loop Each scheduled retry
            S->>M: Webhook: invoice.payment_attempt_required
            Note over M: Apply your retry logic
            M->>P: MIT payment with vaulted token (paymentType: SUBSCRIPTION)
            P-->>M: Approved or declined
            M->>S: Report attempt against the same payment record
        end
        Note over S: Schedule exhausted, final state
    end
```

*Figure 2. Renewals: every attempt is executed by Primer and every retry is gated by your decline classification.*

Retry scheduling uses Stripe Billing Automations, configured in your Dashboard. Because the decline detail comes from the processor through Primer, your backend supplies that context: before reporting a failed attempt, it stamps the invoice metadata with the decline classification derived from Primer's decline code.

<Note>
  **Why this matters for retries**

  Stripe Billing schedules each retry, and your backend decides whether to execute it, informed by the decline code Primer returns. Hard declines can be stopped after a single attempt; soft declines follow the retry policy you configure in Stripe.
</Note>

* **Hard declines** (stolen card, closed account): the automation cancels the subscription or marks the invoice as `uncollectible`.
* **Soft declines** (insufficient funds, temporary issuer errors): the automation attaches your retry policy, for example day 2, day 5, day 8. Each scheduled attempt arrives as a webhook, and execution still sits with you and Primer, so later attempts can be routed to a different processor.

Attempts beyond the schedule are also possible at any time: charge through Primer and report the attempt against the same payment record, which keeps one consolidated attempt history per invoice.

You can also build your own retry logic on your backend that reads these metadata values, instead of or alongside the Dashboard automations. You have full flexibility on when these retries are done with your own logic.

<Note>
  **Key design point**

  Stripe's Smart Retries is not currently supported for custom payment methods (a Stripe limitation), so retries follow a prescribed policy, set globally or on an automation (the automation takes precedence where it applies). Combined with the decline classification above, issuers and schemes only see attempts with a realistic chance of succeeding.
</Note>

#### Handle renewals

When Stripe finalises a renewal invoice, it sends `invoice.payment_attempt_required`. Your webhook handler creates a merchant-initiated payment through Primer with the vaulted token and `paymentType` set to `SUBSCRIPTION`, then reports the outcome to Stripe.

```javascript Handle invoice.payment_attempt_required theme={"dark"}
async function collectInvoice(invoice) {
  if (invoice.amount_remaining <= 0) return;

  // The subscription's default payment method is the custom payment method created at signup
  const paymentMethod = await stripe.paymentMethods.retrieve(defaultPaymentMethodId);

  const response = await fetch('https://api.sandbox.primer.io/payments', {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.PRIMER_API_KEY,
      'X-Api-Version': '2.4',
      'X-Idempotency-Key': `${invoice.id}-${invoice.attempt_count}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      orderId: invoice.id,
      amount: invoice.amount_remaining,
      currencyCode: invoice.currency.toUpperCase(),
      paymentMethodToken: paymentMethod.metadata.primer_payment_method_token,
      paymentMethod: { paymentType: 'SUBSCRIPTION' },
    }),
  });
  const payment = await response.json();

  await reportOutcome(invoice, paymentMethod, payment);
}
```

<Note>
  If Primer returns the payment as `PENDING`, wait for the `PAYMENT.STATUS` webhook with the final status before you report the outcome to Stripe. See [Configure webhooks](/docs/api-reference/get-started/configure-webhooks).
</Note>

Primer runs the payment through your Workflow, which selects the processor and applies any fallbacks you've configured. Use the invoice ID and attempt count as the idempotency key so that a redelivered webhook doesn't create a second payment. See [Avoid duplicated payments](/docs/get-started/recurring-payments#avoid-duplicated-payments).

#### Report retries against the payment record

When you report a failed attempt, the invoice stays open and the subscription moves to `past_due`. Stripe then schedules the next attempt according to your retry settings, and sends a new `invoice.payment_attempt_required` event for each one.

Report every retry against the existing payment record with `reportPaymentAttempt`, rather than creating a new record with `reportPayment`. This keeps a single payment with a consolidated attempt history on the invoice.

```javascript Report the outcome of an attempt theme={"dark"}
async function reportOutcome(invoice, paymentMethod, payment) {
  const now = Math.floor(Date.now() / 1000);
  const succeeded = ['AUTHORIZED', 'SETTLING', 'SETTLED'].includes(payment.status);
  const outcome = succeeded
    ? { outcome: 'guaranteed', guaranteed: { guaranteed_at: now } }
    : { outcome: 'failed', failed: { failed_at: now } };

  const existing = await stripe.invoicePayments.list({
    invoice: invoice.id,
    payment: { type: 'payment_record' },
  });

  if (existing.data.length > 0) {
    await stripe.paymentRecords.reportPaymentAttempt(existing.data[0].payment.payment_record, {
      initiated_at: now,
      payment_method_details: { payment_method: paymentMethod.id },
      ...outcome,
    });
    return;
  }

  const paymentRecord = await stripe.paymentRecords.reportPayment({
    amount_requested: { value: invoice.amount_remaining, currency: invoice.currency },
    payment_method_details: { payment_method: paymentMethod.id },
    customer_details: { customer: invoice.customer },
    initiated_at: now,
    customer_presence: 'off_session',
    processor_details: { type: 'custom', custom: { payment_reference: payment.id } },
    ...outcome,
  });
  await stripe.invoices.attachPayment(invoice.id, { payment_record: paymentRecord.id });
}
```

#### Detect the final attempt

With standard retry settings, failures arrive as `invoice.payment_failed`. When automations control retries, `next_payment_attempt` is updated through `invoice.updated`, so handle both events. There is no dedicated event for an exhausted schedule so you should treat a failure-related invoice update with no remaining `next_payment_attempt`, together with the resulting subscription state, as the final signal.

### Dunning and payment method recovery

Failed-payment emails and card-update prompts are owned by your systems, driven by the same webhooks. When a stored credential is no longer usable after a hard decline, recovery is a customer-present journey: bring the customer back, take a fresh payment through Primer Checkout, vault the new token, and attach a new custom payment method in Stripe. Stripe's customer portal continues to work for plan changes and cancellations on these subscriptions. With automations enabled, read the next scheduled attempt time from `invoice.updated` events.

#### Update the customer's payment method

1. Bring the customer back to a payment page and collect the new payment method with Primer Checkout, with `vaultOnSuccess` enabled.
2. Create a new custom payment method with the new Primer token, and set it as the subscription's default payment method.
3. If an invoice is still open, charge it through Primer and report the attempt against its payment record.

<Warning>
  The Stripe customer portal lets customers switch to a Stripe-supported payment method. Renewals on a Stripe payment method are processed by Stripe, not Primer. To keep every payment on Primer, turn off payment method updates in your portal configuration and handle card updates in your own flow.
</Warning>

### Refunds

Refund the payment through Primer, with the API or the Dashboard. Then report the refund to the payment record in Stripe, and create a credit note to adjust the invoice. Both full and partial refunds are supported.

```javascript Report a refund to Stripe theme={"dark"}
const refund = await stripe.paymentRecords.reportRefund(paymentRecordId, {
  processor_details: { type: 'custom', custom: { refund_reference: primerRefundReference } },
  outcome: 'refunded',
  refunded: { refunded_at: Math.floor(Date.now() / 1000) },
});

await stripe.creditNotes.create({
  invoice: invoiceId,
  refunds: [{
    type: 'payment_record_refund',
    payment_record_refund: { payment_record: paymentRecordId, refund_group: primerRefundReference },
  }],
});
```

Disputes are handled with the processor that processed the payment, and can't be managed in Stripe.

### Who owns what

| Stripe Billing | Your team | Primer |
| - | - | - |
| Plans, subscriptions, invoices, tax and proration, lifecycle states, retry scheduling via Automations | Webhook handling, payment creation, decline classification, reporting back, dunning communications, recovery journeys | Payment execution, vault and network tokens, recurring credential context, routing and fallbacks, decline codes, integration support |

## Integration checklist

| Step | Where | What to build |
| - | - | - |
| 1 | Stripe Dashboard | Create the custom payment method type and store its identifier in configuration. |
| 2 | Stripe Dashboard | Configure the two payment-failure automations (hard decline cancels, soft decline applies your retry policy). |
| 3 | Stripe Dashboard | Register your webhook endpoint and subscribe to `invoice.payment_attempt_required` and `invoice.updated`. |
| 4 | Your backend | Your backend: create the customer and subscription with an incomplete first invoice. |
| 5 | Your frontend and backend | Create a Primer client session for the invoice amount and take the first payment through Primer Checkout with vaulting enabled. |
| 6 | Your backend | Create the custom payment method with the Primer token reference, attach it, and set it as the subscription default. |
| 7 | Your backend | Report the first payment and attach it to the invoice within 23 hours. |
| 8 | Your backend | Handle the renewal webhook from Stripe and create merchant-initiated payments through Primer with `paymentType` `SUBSCRIPTION`. |
| 9 | Your backend | Classify declines, stamp the invoice metadata, and report attempts against the payment record. |
| 10 | Your backend | Execute refunds through Primer and report them to Stripe with a credit note. |
| 11 | Your backend | Handle failed-payment emails to customers and the card-update journey through Primer Checkout. |

## Testing in sandbox

Stripe test clocks cover sign up and renewals. For testing successful subsequent payments, you need to advance the test clock and ensure that the payment will succeed. On dunning attempts, where a subsequent payment fails and triggers retry Automation on Stripe's end, for the most production-representative validation, run a subscription without a test clock on a short custom retry schedule and let it elapse in real time.

<Tip>
  When you advance a test clock directly past `invoice.next_payment_attempt`, the retry webhook may not be sent immediately. Advance the clock again by a small increment, such as one minute, to trigger the pending attempt.
</Tip>

## Reference documentation

* [Stripe, integrate with third-party payment processors](https://docs.stripe.com/billing/subscriptions/third-party-payment-processing)
* [Stripe, Billing automations](https://docs.stripe.com/billing/automations)
* [Primer documentation (payments API, client sessions, vaulting, webhooks)](https://primer.io/docs)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.