# Payments and subscriptions

Audoryx provides a production-oriented, provider-hosted subscription checkout layer for **Extended** installations. The application never renders raw card fields and never receives PAN or CVC data.

Supported drivers:

| Driver | Checkout API | Recurring object | Signature verification |
|---|---|---|---|
| Stripe | Checkout Sessions in `subscription` mode | Stripe Billing Subscription/Price | Local HMAC verification of the raw request body and `Stripe-Signature` |
| PayPal | Subscriptions REST API approval URL | PayPal Catalog Product/Plan/Subscription | PayPal `verify-webhook-signature` REST endpoint |
| Paddle | Transactions API and returned Checkout URL | Recurring Paddle Price/Subscription | Local HMAC verification of the raw request body and `Paddle-Signature` |
| Razorpay | Subscription Link | Razorpay Plan/Subscription | HMAC-SHA256 over the exact raw body and `X-Razorpay-Signature` |
| Paystack | Transaction Initialize hosted URL | Paystack Plan/Subscription | HMAC-SHA512 over the exact raw body and `x-paystack-signature` |
| Flutterwave | Standard hosted checkout | Payment Plan/Subscription | Base64 HMAC-SHA256, with verified legacy secret-hash support |
| Mercado Pago | Preapproval `init_point` | Preapproval Plan/Subscription | `x-signature` manifest validation plus authenticated resource retrieval |
| Lemon Squeezy | Checkouts JSON:API | Variant/Subscription | HMAC-SHA256 over the exact raw body and `X-Signature` |
| Square | Online Checkout Payment Link | Subscription Plan Variation | Base64 HMAC-SHA256 over the exact notification URL plus raw body |
| Mollie | First mandate payment | Customer/Mandate/Subscription | Authenticated payment retrieval; the recurring subscription is created only after the first paid mandate |

## Enable SaaS billing

1. Confirm that the installation is legally entitled to operate in Extended/SaaS mode.
2. Confirm that the EKR certificate shown under **Admin → Licenses** grants the
   Extended tier and billing features. Local environment values cannot unlock them.
3. Open **Admin → Payments**.
4. Configure the provider credentials. Secrets are encrypted using `APP_KEY`, are write-only, and are never returned to the browser.
5. Copy the displayed webhook URL into the provider dashboard.
6. Open **Admin → Plans** and add the provider's recurring catalog identifier for each monthly/yearly price.
7. Enable the gateway only after the signing secret/webhook ID is configured.

The customer-facing plan page is `/app/billing`. The Studio **Upgrade plan** action links to this page only in Extended mode.

## Provider setup

### Stripe

- Create recurring **Prices**, not legacy Plan objects.
- Configure the monthly/yearly `price_...` IDs on each Audoryx plan.
- Add `https://your-domain.example/billing/webhooks/stripe` as a webhook endpoint.
- Subscribe at minimum to:
  - `checkout.session.completed`
  - `customer.subscription.created`
  - `customer.subscription.updated`
  - `customer.subscription.deleted`
  - `invoice.paid`
  - `invoice.payment_failed`
  - `charge.refunded`
- Save the endpoint's `whsec_...` signing secret.

Audoryx sends `Stripe-Version: 2026-02-25.clover` by default and uses Checkout Sessions with `mode=subscription`. It does not use the Charges API or collect card data.

### PayPal

- Create a Catalog Product and one recurring Plan for each monthly/yearly option.
- Configure the `P-...` Plan IDs on each Audoryx plan.
- Add `https://your-domain.example/billing/webhooks/paypal` in the PayPal developer dashboard.
- Subscribe to subscription lifecycle events, `PAYMENT.SALE.COMPLETED`, refunds, reversals, and payment failures.
- Save the REST app Client ID, Client Secret, and PayPal Webhook ID.

Audoryx obtains an OAuth access token, creates the subscription with `PayPal-Request-Id`, and redirects the buyer to PayPal's approval URL. Incoming events are sent back to PayPal's official verification endpoint before processing.

### Paddle Billing

- Create recurring Paddle Prices and configure each `pri_...` ID on the matching Audoryx plan.
- Configure a default payment link/approved checkout domain in Paddle, or save its approved checkout page URL in Audoryx.
- Add `https://your-domain.example/billing/webhooks/paddle` as a notification destination.
- Subscribe to `transaction.*` and `subscription.*` lifecycle events.
- Save the API key and webhook endpoint secret.

Audoryx creates an automatically collected transaction and redirects to the `checkout.url` returned by Paddle. A recurring transaction creates/updates the Paddle subscription.

### Extended gateway catalog

For the remaining drivers, configure these catalog identifiers on every Audoryx plan:

| Gateway | Monthly/yearly catalog value |
|---|---|
| Razorpay | Plan ID |
| Paystack | Plan code |
| Flutterwave | Payment Plan ID |
| Mercado Pago | Preapproval Plan ID |
| Lemon Squeezy | Variant ID; also configure the Store ID on the gateway |
| Square | Subscription Plan Variation ID; also configure Location ID and the exact webhook URL |
| Mollie | A stable internal billing profile key; Audoryx uses the server-side plan amount and interval |

All seven use provider-hosted checkout. Razorpay, Paystack, Flutterwave, Lemon Squeezy, and Square validate signed webhook content locally. Mercado Pago and Mollie additionally retrieve the canonical provider resource with the private API credential before changing local billing state. Mollie creates the recurring subscription only after its first mandate payment is confirmed as paid.

## Checkout API

Authenticated, verified users can call:

```http
POST /app/billing/checkout
Idempotency-Key: billing-client-generated-unique-key
Content-Type: application/json

{
  "plan_id": "01...",
  "gateway": "stripe",
  "billing_interval": "monthly"
}
```

The idempotency key must be 16–160 URL-safe characters. Replaying the same key with the same user, gateway, plan, and interval returns the existing checkout URL. Reusing it with different parameters is rejected.

The response contains only the local transaction ID, provider-hosted checkout URL, and status. The frontend must navigate to that URL; it must not ask the user for card data.

## Return URLs are not payment proof

Success and cancel routes use a random per-transaction return token. A success redirect records only that the customer returned. It **does not** activate a subscription or grant credits. Audoryx changes billing state only after a verified provider webhook.

Successful renewal/payment events grant plan credits through the existing idempotent credit ledger:

- Stripe: `invoice.paid`
- PayPal: `PAYMENT.SALE.COMPLETED`
- Paddle: completed transaction

The ledger key includes the provider and external payment transaction, so webhook retries cannot grant credits twice. A yearly payment grants twelve months of the plan's configured monthly credits.

## Webhook reliability and security

- Webhook endpoints are intentionally exempt from CSRF; every event must pass provider signature verification instead.
- Verification uses the **exact raw HTTP request body**.
- Stripe and Paddle timestamps default to a five-minute tolerance (`AUDORYX_PAYMENT_WEBHOOK_TOLERANCE=300`).
- Provider event IDs and SHA-256 payload hashes are stored. Identical retries return success without reprocessing; an event ID reused with different content is rejected.
- Processing failures return HTTP 500 and retain a sanitized error so the provider can retry.
- Webhooks continue to reconcile existing subscriptions even if a gateway is later disabled.
- Full webhook payloads are not intended as an analytics store. Keep financial reporting in the provider dashboard and implement a compliant retention policy for local transaction metadata.

### Account-deletion retention

Completed financial transaction rows are retained for reconciliation after an account is deleted. Their account and subscription foreign keys are cleared, and the checkout URL, original return token, client idempotency key, and provider metadata are erased. Only non-personal reconciliation fields such as gateway, external transaction ID, type, status, amount, currency, billing interval, and timestamps remain.

Campaign-delivery audit rows likewise retain delivery status and timing, while the recipient email and any provider error text are redacted before the user record is removed.

## Environment fallback

Admin-stored encrypted credentials take precedence. Environment variables are useful for immutable deployments:

```dotenv
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=

PAYPAL_CLIENT_ID=
PAYPAL_CLIENT_SECRET=
PAYPAL_WEBHOOK_ID=

PADDLE_API_KEY=
PADDLE_WEBHOOK_SECRET=
PADDLE_CHECKOUT_PAGE_URL=

RAZORPAY_KEY_ID=
RAZORPAY_KEY_SECRET=
RAZORPAY_WEBHOOK_SECRET=

PAYSTACK_SECRET_KEY=

FLUTTERWAVE_SECRET_KEY=
FLUTTERWAVE_WEBHOOK_SECRET=

MERCADOPAGO_ACCESS_TOKEN=
MERCADOPAGO_WEBHOOK_SECRET=

LEMONSQUEEZY_API_KEY=
LEMONSQUEEZY_WEBHOOK_SECRET=
LEMONSQUEEZY_STORE_ID=

SQUARE_ACCESS_TOKEN=
SQUARE_WEBHOOK_SIGNATURE_KEY=
SQUARE_LOCATION_ID=
SQUARE_NOTIFICATION_URL=https://your-domain.example/billing/webhooks/square

MOLLIE_API_KEY=
```

Never commit live credentials, copy them into support tickets, or expose them through Inertia/API resources.

## Production checklist

- Use HTTPS for `APP_URL` and all provider return/checkout URLs.
- Set `APP_DEBUG=false`; protect `APP_KEY` and back it up securely.
- Test in each provider sandbox, then replace all sandbox catalog IDs with live IDs.
- Confirm webhook delivery and signature verification in the provider dashboard.
- Confirm one initial payment, renewal, failed payment, cancellation, and refund.
- Confirm repeated webhook delivery does not duplicate subscriptions, transactions, or credits.
- Configure taxes, invoices, refunds, disputes, dunning, privacy, and retention in line with the merchant's jurisdiction.
- Do not claim PCI scope is eliminated solely by hosted checkout; the operator must complete the provider's applicable compliance process.

Official references:

- Stripe Checkout Sessions and Billing: <https://docs.stripe.com/api/checkout/sessions/create>
- Stripe webhook signatures: <https://docs.stripe.com/webhooks/signature>
- PayPal Subscriptions: <https://developer.paypal.com/docs/subscriptions/>
- PayPal webhook verification: <https://developer.paypal.com/api/rest/webhooks/rest/>
- Paddle Transactions: <https://developer.paddle.com/api-reference/transactions/create-transaction/>
- Paddle webhook signatures: <https://developer.paddle.com/webhooks/about/signature-verification/>
- Razorpay subscriptions and webhook validation: <https://razorpay.com/docs/api/payments/subscriptions/> and <https://razorpay.com/docs/webhooks/validate-test/>
- Paystack subscriptions and webhooks: <https://paystack.com/docs/payments/subscriptions/> and <https://paystack.com/docs/payments/webhooks/>
- Flutterwave Standard and webhooks: <https://developer.flutterwave.com/v3.0/docs/flutterwave-standard-1> and <https://developer.flutterwave.com/docs/webhooks>
- Mercado Pago subscriptions and webhooks: <https://www.mercadopago.com.br/developers/en/docs/subscriptions/overview> and <https://www.mercadopago.com.br/developers/en/docs/your-integrations/notifications/webhooks>
- Lemon Squeezy checkouts and webhooks: <https://docs.lemonsqueezy.com/api/checkouts/create-checkout> and <https://docs.lemonsqueezy.com/guides/developer-guide/webhooks>
- Square subscription checkout and webhook validation: <https://developer.squareup.com/docs/checkout-api/subscription-plan-checkout> and <https://developer.squareup.com/docs/webhooks/step3validate>
- Mollie recurring payments and classic webhooks: <https://docs.mollie.com/docs/recurring-payments> and <https://docs.mollie.com/reference/webhooks>
