# Pay-to-View: Web Integration Guide (Stripe) ## Overview Web pay-to-view uses **Stripe Embedded Checkout**. The flow is: 1. Frontend calls the API to create a purchase → gets a Stripe `clientSecret` 2. Frontend renders the Stripe embedded checkout using that `clientSecret` 3. User completes payment in the Stripe checkout UI 4. Stripe sends a webhook to the backend → purchase is marked as completed 5. Frontend can poll or check access status --- ## Step-by-Step Flow ### Step 1: Create a Purchase **Endpoint:** `POST /client/v1/pay-to-view/purchases` **Auth:** User token required #### Request: ```json { "payToViewProductId": "encoded_product_id", "entityId": "encoded_entity_id", "returnUrl": "https://yoursite.com/purchase/complete" } ``` | Field | Description | |-------|------------| | `payToViewProductId` | The encoded ID of the pay-to-view product | | `entityId` | The encoded ID of the entity (video, channel, or content) | | `returnUrl` | URL to redirect after checkout completes | #### Response: ```json { "success": true, "purchaseId": "ptv_abc123", "clientSecret": "cs_test_...", "stripeCustomerId": "cus_..." } ``` | Field | Description | |-------|------------| | `success` | Whether the purchase was created | | `hasAccess` | If `true`, user already has access (no need to pay) | | `purchaseId` | Internal purchase ID for tracking | | `clientSecret` | **Use this to mount Stripe Embedded Checkout** | | `stripeCustomerId` | The Stripe customer ID (auto-created if first purchase) | > **Note:** If the user already has an active purchase for this entity, the API returns `success: false` with `hasAccess: true`. No need to show checkout. > **Note:** If there's a recent pending purchase (< 5 min old) with an open Stripe session, the API reuses it instead of creating a new one. ### Step 2: Mount Stripe Embedded Checkout Use the `clientSecret` from the response to render Stripe's embedded checkout: ```javascript import { loadStripe } from '@stripe/stripe-js'; const stripe = await loadStripe('pk_test_your_publishable_key'); const checkout = await stripe.initEmbeddedCheckout({ clientSecret: response.clientSecret, }); // Mount the checkout form into a DOM element checkout.mount('#checkout-container'); ``` The user completes the payment directly in the embedded Stripe form. No redirect happens — the `returnUrl` is used only if `redirectOnCompletion` is triggered (currently set to `"never"`). ### Step 3: Webhook Validates & Completes the Purchase After the user pays, Stripe sends a `checkout.session.completed` event to: ``` POST /client/v1/webhooks/stripe ``` #### How webhook validation works: 1. **Signature verification** — Stripe signs every webhook with a secret. The backend verifies it using: ``` EventUtility.ConstructEvent(rawBody, stripeSignatureHeader, webhookSecret) ``` This ensures the webhook is genuinely from Stripe and hasn't been tampered with. 2. **Event routing** — The backend checks the event type and routes to the correct handler: - `checkout.session.completed` → completes a pay-to-view purchase - `charge.refunded` → marks the purchase as refunded 3. **Purchase completion** — For `checkout.session.completed`: - Reads `payToViewPurchaseId` and `userId` from the session metadata - Finds the pending purchase in the database - Sets `ExternalTransactionId` to the Stripe `PaymentIntentId` - Marks status as `Completed` - Sets `ExpiresAt` if the product has `AccessDurationDays` - Grants the user access to the content 4. **Refund handling** — For `charge.refunded`: - Finds the purchase by the `PaymentIntentId` - Marks status as `Refunded` - Revokes access --- ## Purchase Status Flow ``` Pending → Completed → (optionally) Refunded ``` | Status | Meaning | |--------|---------| | `Pending` | Purchase created, waiting for Stripe payment | | `Completed` | Payment confirmed via webhook, access granted | | `Refunded` | Refund processed via webhook, access revoked | --- ## Stripe Metadata Stored on Checkout Session These are attached to the Stripe session and returned in the webhook: | Key | Value | |-----|-------| | `payToViewPurchaseId` | Internal purchase ID | | `payToViewProductId` | Internal product ID | | `userId` | The authenticated user's ID | | `entityType` | Type of entity (1=Video, 2=Channel, 3=Content) | | `entityId` | The encoded entity ID | --- ## Checking User Access **Endpoint:** `GET /client/v1/pay-to-view/purchases?page=1&pageSize=10` **Auth:** User token required Returns the user's purchase history. Check if a purchase with `status: Completed` and valid `expiresAt` exists for the entity. --- ## Admin Refund **Endpoint:** `POST /admin/v1/pay-to-view/purchases/{id}/refund` **Auth:** Admin token required Refunds the purchase via Stripe and marks it as refunded in the database. --- ## Summary | Step | Who | What | |------|-----|------| | 1 | Frontend | `POST /client/v1/pay-to-view/purchases` with product + entity ID | | 2 | Frontend | Mount Stripe Embedded Checkout using `clientSecret` | | 3 | User | Completes payment in Stripe UI | | 4 | Stripe | Sends `checkout.session.completed` webhook to backend | | 5 | Backend | Validates signature, marks purchase completed, grants access | | 6 | Frontend | User now has access to the content |