md-platform

paytoviewweb.md
View raw Back to list

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:

{
  "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:

{
  "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:

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