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

# Subscriptions

> Sell recurring plans with Payviox: create a session server-side with a subscription object (interval, interval_count), the customer pays the first period on the payment page, then Stripe Billing on your own Stripe account charges every renewal. Covers Stripe prerequisites (card/Apple Pay/Google Pay, 5 webhook events on https://api.payviox.com/payment/stripe/webhook), webhooks (succeeded per cycle with order_id {order_id}-{cycle}, subscription_payment_failed, subscription_canceled), customer self-service management_url, cancel/resume API, limits and testing

Charge your customers a fixed amount every day, week, month or year. The customer subscribes once on the Payviox payment page; the following periods are charged automatically, and you're notified of each one by webhook.

## How it works

```mermaid theme={null}
sequenceDiagram
    participant Customer
    participant Server as Your server
    participant Payviox
    participant Stripe as Your Stripe account

    Server->>Payviox: POST /session with subscription (sk_)
    Payviox-->>Server: session_id
    Server->>Customer: openSession(session_id)
    Customer->>Payviox: Pays the first period (card, Apple Pay, Google Pay)
    Payviox->>Server: succeeded webhook (cycle 1, subscription.id)
    Payviox->>Stripe: Creates the subscription, first renewal at the end of the paid period
    loop Every period
        Stripe->>Stripe: Charges the saved card
        Stripe->>Payviox: invoice.paid
        Payviox->>Server: succeeded webhook (order_id-2, -3…)
    end
    Customer->>Payviox: Cancels from the management link
    Payviox->>Server: subscription_canceled (when the paid period ends)
```

1. **Your server** creates a payment session with a `subscription` object, using your secret key. `amount` is your price for one period.
2. **The customer** pays the first period on the Payviox payment page, with a card, Apple Pay or Google Pay. This payment goes through the usual fraud checks, the customer explicitly authorizes the recurring charge, and the card is saved for the renewals.
3. When the first payment succeeds, Payviox creates the subscription (`psub_...`) and sends you a `succeeded` webhook with a `subscription` block.
4. Payviox then sets up a **Stripe Billing subscription on your own Stripe account**, starting at the end of the paid period. Stripe charges each renewal automatically — the money lands on your Stripe account, as for your one-time Stripe payments.
5. Each renewal reaches you as a new `succeeded` webhook, with `order_id` = `{your order_id}-{cycle}`. Failed renewals and cancellations have their own events.
6. The customer can cancel at the end of the paid period from a self-service link; you can cancel or resume from your backend or from the dashboard.

## Before you start

### Connect Stripe with card or wallets

Subscriptions are charged with Stripe, on your account:

* In the [Payviox dashboard](https://dash.payviox.com), **Providers → Stripe** must be connected (publishable key, secret key and webhook signing secret) — the same setup as for one-time Stripe payments.
* At least one of these methods must be active: **card** (`stripe_credit`), **Apple Pay** (`stripe_apple_pay`), **Google Pay** (`stripe_google_pay`). They're the only methods offered on the payment page of a subscription.

<Note>
  Renewals are Stripe Billing subscriptions on your Stripe account, so Stripe's Billing pricing applies to them. Your Payviox fees apply to each renewal like any payment, at the rates in force when the customer subscribed. In your Stripe dashboard you'll see the customers, a product named after your business ("… subscription") and one Stripe subscription per Payviox subscription (metadata `payviox_subscription_id`).
</Note>

### Enable the Stripe renewal events

Renewals happen on Stripe, so Payviox learns about them through the Stripe webhook endpoint you set up for Payviox: `https://api.payviox.com/payment/stripe/webhook`. Add these five events to it:

| Stripe event | Used for |
| - | - |
| `invoice.paid` | A renewal was charged → you get `succeeded` |
| `invoice.payment_failed` | A renewal charge failed → you get `subscription_payment_failed` |
| `invoice.payment_action_required` | The bank asks the customer to authenticate a renewal → `subscription_payment_failed` with `authentication_required` |
| `customer.subscription.updated` | Status, billing period and scheduled cancellations stay in sync |
| `customer.subscription.deleted` | The subscription ended → you get `subscription_canceled` |

<Steps>
  <Step title="Open your webhook endpoints in Stripe">
    In the [Stripe Dashboard](https://dashboard.stripe.com/webhooks), go to **Developers → Webhooks**. Check the mode you're in: test mode and live mode have separate endpoints, so repeat these steps in each mode you use.
  </Step>

  <Step title="Open the Payviox endpoint">
    Click the endpoint whose URL is `https://api.payviox.com/payment/stripe/webhook` — the one you created when you connected Stripe to Payviox. It must be **enabled**: a disabled endpoint counts as missing.
  </Step>

  <Step title="Add the five events">
    Edit the endpoint's events (**Edit destination** or **Update details**, then **Select events**). Search for and tick `invoice.paid`, `invoice.payment_failed`, `invoice.payment_action_required`, `customer.subscription.updated` and `customer.subscription.deleted`.

    <Warning>
      Keep the `payment_intent.*` events already selected: one-time payments and the first payment of a subscription still rely on them.
    </Warning>
  </Step>

  <Step title="Save">
    Save the endpoint. Its signing secret doesn't change, so there's nothing to update in Payviox.
  </Step>
</Steps>

<Accordion title="No Payviox endpoint in Stripe yet?">
  Create one: **Add endpoint**, URL `https://api.payviox.com/payment/stripe/webhook`, select the `payment_intent.*` events plus the five events above, then copy its signing secret (`whsec_...`) into **Providers → Stripe** in the Payviox dashboard.
</Accordion>

<Check>
  Payviox checks your endpoint with your Stripe secret key when you create a subscription session. If events are missing, session creation fails with `subscription_setup_incomplete` and the list of `missing_events`, and the Subscriptions page of your dashboard shows a warning. The result is cached for up to an hour.
</Check>

<Warning>
  If your Stripe key isn't allowed to read webhook endpoints (restricted key), Payviox can't run this check and lets the session through. Make sure the five events are enabled anyway: without them, renewals are still charged by Stripe but you won't be notified.
</Warning>

### Turn on the subscription webhooks in Payviox

In the Payviox dashboard, **Developers → Webhooks**, select the opt-in events `subscription_payment_failed` and `subscription_canceled` next to `succeeded`. See [Webhook Integration](/webhooks/integration) if you don't receive Payviox webhooks yet.

## Create a subscription session

Subscriptions follow the [recommended integration](/quickstart/integration-methods): your backend creates the session with the **secret key**, the browser opens it with the SDK. A subscription session can't be created with the public key — the recurring price must never come from the browser.

<Steps>
  <Step title="Your server creates the session">
    Add `subscription` to a regular [Create Session](/api/endpoints/create-session#subscriptions) request. `amount` and `items` describe **one period**.

    <CodeGroup>
      ```javascript Node.js (Express) theme={null}
      const PLANS = {
        pro_monthly: { name: 'Pro plan', price: 999, interval: 'month', interval_count: 1 },
        pro_yearly: { name: 'Pro plan (yearly)', price: 9990, interval: 'year', interval_count: 1 },
      };

      app.post('/api/subscribe', requireLogin, async (req, res) => {
        const plan = PLANS[req.body.plan];
        if (!plan) return res.status(400).json({ error: 'unknown_plan' });

        const response = await fetch('https://api.payviox.com/session', {
          method: 'POST',
          headers: {
            Authorization: `Bearer ${process.env.PAYVIOX_SECRET_KEY}`, // sk_
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            amount: plan.price, // price of ONE period, in cents
            currency: 'USD',
            customer: req.user.id, // your ID: lets you find the subscription later
            email: req.user.email,
            order_id: `sub_${req.user.id}_${Date.now()}`, // renewals: `${order_id}-2`, `-3`…
            description: plan.name,
            ip: req.ip,
            metadata: { user_id: req.user.id, plan: req.body.plan },
            subscription: { interval: plan.interval, interval_count: plan.interval_count },
            items: [{ name: plan.name, quantity: 1, price: plan.price }],
          }),
        });

        const body = await response.json();
        if (!response.ok) {
          // e.g. 400 subscription_setup_incomplete: body.missing_events lists the Stripe events to enable
          console.error('Payviox session failed', response.status, body);
          return res.status(502).json({ error: 'checkout_unavailable' });
        }

        res.json({ session_id: body.session_id });
      });
      ```

      ```bash cURL theme={null}
      curl https://api.payviox.com/session \
        -H "Authorization: Bearer sk_live_xxxxxxxxxxxx" \
        -H "Content-Type: application/json" \
        -X POST \
        -d '{
          "amount": 999,
          "currency": "USD",
          "customer": "customer_abc123",
          "email": "john@example.com",
          "order_id": "sub_customer_abc123_1760351400",
          "description": "Pro plan",
          "ip": "203.0.113.42",
          "metadata": { "user_id": "usr_123", "plan": "pro_monthly" },
          "subscription": { "interval": "month", "interval_count": 1 },
          "items": [{ "name": "Pro plan", "quantity": 1, "price": 999 }]
        }'
      ```
    </CodeGroup>

    `interval` is `day`, `week`, `month` or `year`; `interval_count` (default `1`) multiplies it — `{ "interval": "month", "interval_count": 3 }` bills every quarter. A period can't exceed 3 years.

    <Tip>
      * Put your own IDs (user, plan) in `metadata`: it's sent with every webhook of the subscription, renewals included. The webhook `customer` field holds the payer's billing details, not your ID.
      * Renewals reuse your `order_id` with a suffix (`-2`, `-3`, …). Use a dedicated prefix for subscription order IDs (like `sub_` above) so they can't clash with your other orders.
    </Tip>
  </Step>

  <Step title="The browser opens it with the SDK">
    ```html theme={null}
    <script src="https://sdk.payviox.com/payviox.js"></script>
    <script>
      const payviox = new Payviox('pk_live_your_key');

      document.querySelector('#subscribe').addEventListener('click', async () => {
        const { session_id } = await fetch('/api/subscribe', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ plan: 'pro_monthly' }),
        }).then((r) => r.json());

        payviox.openSession(session_id); // redirects to the payment page
      });
    </script>
    ```

    No browser in your flow? Send the customer to `https://secure.payviox.com/{session_id}` yourself.
  </Step>
</Steps>

On the payment page, the customer sees what they pay today, the recurring price and the date of the first renewal, and confirms an authorization such as *"By subscribing, you authorize Acme to charge your card \$10.29 every month, starting November 13, 2026, until you cancel…"*. When the payment succeeds right away, the success screen shows their next payment date and a link to manage the subscription.

<Note>
  The customer is charged your `amount` plus the fees your account adds for the customer, exactly as for a one-time payment. The amount charged today can be slightly higher than the renewals: one-time add-on fees (such as fraud screening) only apply to the first payment. The renewal price is fixed at signup and returned as `unit_amount` on the [Subscription object](/api/endpoints/get-subscription#the-subscription-object).
</Note>

## Handle the webhooks

The customer coming back to your site isn't a confirmation — webhooks are. Keep one rule: **give access until the end of the last period you were paid for.**

| Event | Meaning | What to do |
| - | - | - |
| `succeeded` with `subscription.cycle = 1` | The first payment succeeded, the subscription is active | Store `subscription.id` on the customer, give access until `subscription.current_period_end` |
| `succeeded` with `subscription.cycle ≥ 2` | A renewal was charged (new session, `order_id` `{order_id}-{cycle}`) | Extend access until the new `subscription.current_period_end` |
| `subscription_payment_failed` | A renewal charge failed (`failure_reason`). Stripe retries automatically | Tell the customer; keep access for a grace period or suspend it — your call |
| `subscription_canceled` | The subscription ended, no further charges | Revoke access once the last paid period is over |

```javascript Node.js theme={null}
// After verifying the signature (see Webhook Integration)
switch (data.type) {
  case 'succeeded':
    if (!data.subscription) return fulfillOrder(data.order_id, data); // one-time payment

    await db.subscriptions.upsert({
      id: data.subscription.id,
      user_id: data.metadata.user_id,
      paid_until: data.subscription.current_period_end, // end of the period just paid
      management_url: data.subscription.management_url,
    });
    if (data.subscription.cycle === 1) {
      await sendWelcomeEmail(data.metadata.user_id, data.subscription.management_url);
    }
    break;

  case 'subscription_payment_failed':
    await sendPaymentIssueEmail(data.subscription.id, data.failure_reason);
    break;

  case 'subscription_canceled':
    // Access ends at paid_until (stored from the last `succeeded`)
    await db.subscriptions.update(data.subscription.id, { canceled: true });
    break;
}
```

* **Idempotency**: deduplicate on `order_id` + `type`, as for one-time payments. Each cycle has its own `order_id` (`order_12345`, `order_12345-2`, …), so renewals never collide.
* **Fraud review**: the first payment can be `pending_review` before `succeeded` — don't give access yet. Renewals aren't reviewed: they arrive directly as `succeeded`.
* **Failed renewals**: you get one `subscription_payment_failed` per cycle, even if Stripe retries several times. Its `subscription.current_period_end` is the end of the period that wasn't paid — don't extend access from it. If a retry succeeds, you receive `succeeded` with the same `order_id`. The retry schedule, and what happens when all retries fail, are set in your Stripe Billing settings (failed payments / revenue recovery): we recommend canceling the subscription, so you receive `subscription_canceled`. If Stripe leaves it unpaid instead, it stays `past_due` in Payviox and no further event is sent.
* **`authentication_required`**: the bank wants the customer to approve the renewal (3D Secure), which Stripe retries alone can't solve. Payviox doesn't contact the customer: enable Stripe's emails with a hosted confirmation link for payments that require 3D Secure in your Stripe Billing settings, or ask the customer to subscribe again.
* **Cancellation**: scheduling a cancellation (by you or the customer) sends no event — `subscription_canceled` arrives when the subscription actually ends. It's attached to the signup payment (`order_id` = your original one, `cycle` = `1`): identify the subscription with `subscription.id`. When you cancel immediately yourself, it's your call whether access ends now or at the end of the paid period.

See [Subscription events](/webhooks/integration#subscription-events) for the full payloads.

## Let customers manage their subscription

Every subscription has a `management_url` (in the webhook `subscription` block and on the [Subscription object](/api/endpoints/get-subscription)). It opens a Payviox page where the customer can:

* see the price, the next payment date and the card used;
* **cancel at the end of the paid period** — they keep what they paid for;
* **undo** their own cancellation before the period ends. If you scheduled the cancellation, the page tells them to contact you instead.

**Send it yourself: put `subscription.management_url` from the `succeeded` webhook in your confirmation email**, and show it in your account page. That's the reliable channel — Payviox doesn't email it, and the checkout success screen only shows it when the payment succeeds right away: if the signup payment goes to fraud review (`pending_review`), the subscription and its link only exist once the payment is `succeeded`, after the customer has left the page. Customers can't change their card, switch plans or cancel immediately from this page.

<Warning>
  Treat the link like a password: it doesn't expire, and anyone who has it can cancel the subscription.
</Warning>

## Cancel or resume from your backend

Use the [Subscriptions API](/api/endpoints/list-subscriptions) with your secret key, or the **Subscriptions** page of your dashboard (team members need the *Cancel and resume subscriptions* permission).

* **Cancel now** (`at_period_end: false`) also voids the subscription's unpaid Stripe invoices, so no pending retry can charge the customer afterwards. There's no automatic refund.
* **Resume** only works on a cancellation scheduled on your side (`cancel_requested_by` `api`, `manager`, or `null` for one scheduled from your Stripe dashboard). A cancellation scheduled by the customer (`payer`) can only be undone by the customer, from their management link; one scheduled by Payviox (`admin`, `system`) requires support. Both return `403`. The customer's cancellation withdraws their consent to be charged: renewing anyway would be an unauthorized charge, the typical cause of "canceled recurring payment" disputes.

```bash theme={null}
# Cancel at the end of the paid period (default) — can be undone
curl -X POST https://api.payviox.com/subscriptions/psub_8fKx2LmQp4ZrT7vW1nYc3aBd/cancel \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxx"

# Cancel now — no further charges, no automatic refund
curl -X POST https://api.payviox.com/subscriptions/psub_8fKx2LmQp4ZrT7vW1nYc3aBd/cancel \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "at_period_end": false }'

# Undo a scheduled cancellation
curl -X POST https://api.payviox.com/subscriptions/psub_8fKx2LmQp4ZrT7vW1nYc3aBd/resume \
  -H "Authorization: Bearer sk_live_xxxxxxxxxxxx"
```

<CardGroup cols={2}>
  <Card title="List Subscriptions" icon="list" href="/api/endpoints/list-subscriptions">
    Filter by `status` (`active`, `past_due`, `canceled`, `canceling`) or `customer`
  </Card>

  <Card title="Get Subscription" icon="magnifying-glass" href="/api/endpoints/get-subscription">
    Current status, next renewal, cancellation details
  </Card>

  <Card title="Cancel Subscription" icon="ban" href="/api/endpoints/cancel-subscription">
    At period end (default) or immediately
  </Card>

  <Card title="Resume Subscription" icon="rotate-left" href="/api/endpoints/resume-subscription">
    Undo a scheduled cancellation
  </Card>
</CardGroup>

<Tip>
  Manage subscriptions through Payviox, not directly in your Stripe dashboard. A subscription canceled on Stripe is synced (`canceled_by: "provider"`), but changing its price there isn't supported, and if you resume a customer's cancellation in Stripe, Payviox re-applies the cancellation.
</Tip>

## Limits

* **USD only**, like every Payviox session. Minimum `100` cents per period.
* **Card, Apple Pay and Google Pay through Stripe only.** Other providers and local methods aren't offered for subscriptions.
* **No free trial**: the first period is charged when the customer subscribes.
* **No plan catalog**: price and interval are set per session. To change a customer's price or interval, cancel at period end and have them subscribe again with a new session — there's no upgrade or proration.
* **Intervals up to 3 years**: 1095 days, 156 weeks, 36 months or 3 years per period.
* **The customer can't update their card.** If it expires or keeps being declined, renewals fail; once the subscription is canceled, invite the customer to subscribe again.
* **Fraud screening applies to the signup payment only**, not to renewals.

## Test your integration

1. Connect Stripe in Payviox with your Stripe **test-mode** keys and test-mode webhook signing secret, and enable the five events on the **test-mode** endpoint.
2. Create a subscription session with `{ "interval": "day", "interval_count": 1 }` so the first renewal happens one day later.
3. Pay with a Stripe test card:
   * `4242 4242 4242 4242` — signup and renewals succeed.
   * `4000 0027 6000 3184` — requires authentication on every charge: the signup succeeds after you complete the test authentication, and the renewal fails with `failure_reason: "authentication_required"`.
4. Check that you received `succeeded` with `subscription.cycle: 1`, then fetch the subscription with [Get Subscription](/api/endpoints/get-subscription): `provider_subscription_id` is filled within about a minute, and the subscription shows up in your Stripe dashboard (test mode).
5. Open the `management_url`, cancel, then undo — and check `cancel_at_period_end` with the API.

<Note>
  Stripe test clocks can't be used: they only apply to Stripe customers created on a test clock, and Payviox creates the Stripe customer itself. Use a daily interval to test renewals.
</Note>

## Troubleshooting

<AccordionGroup>
  <Accordion title="403 subscription_requires_secret_key">
    The session was created with your public key (for example with the SDK's `createSession()`). Create it from your server with your secret key, then open it with `openSession()`.
  </Accordion>

  <Accordion title="400 subscription_setup_incomplete">
    Your Stripe endpoint `https://api.payviox.com/payment/stripe/webhook` is missing, or lacks the events listed in `missing_events`. [Add them](#enable-the-stripe-renewal-events) in the right mode (test or live). The check is cached for up to an hour, so the error can persist briefly after you fix it.
  </Accordion>

  <Accordion title="400 No payment method that supports subscriptions">
    Card, Apple Pay and Google Pay (Stripe) are all inactive on your account, or your Stripe publishable or secret key is missing in **Providers → Stripe**. Activate at least one of these methods and complete the Stripe keys. If you pass `paymentMethodId`, it must be one of these methods.
  </Accordion>

  <Accordion title="403 when resuming a subscription">
    The cancellation was scheduled by the customer (`cancel_requested_by: "payer"`) — only they can undo it, from their management link — or by Payviox (`admin`, `system`) — contact support.
  </Accordion>

  <Accordion title="The subscription was canceled right after signup (setup_failed)">
    After the first payment, Payviox couldn't create the subscription on your Stripe account (for example, Stripe keys changed in between). The customer paid for the first period, then the subscription was canceled automatically (`canceled_by: "system"`, `cancellation_reason` `setup_failed:<code>`) and you received `subscription_canceled`. Contact [support@payviox.com](mailto:support@payviox.com) with the subscription ID.
  </Accordion>

  <Accordion title="Renewals are charged on Stripe but I get no webhook">
    Check that the five events are enabled on the Stripe endpoint of the right mode, that the webhook signing secret in **Providers → Stripe** is the one of that endpoint, and that `succeeded` is selected in your Payviox webhook settings.
  </Accordion>
</AccordionGroup>


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