Pay-to-View: Web Integration Guide (Stripe)
Overview
Web pay-to-view uses Stripe Embedded Checkout. The flow is:
- Frontend calls the API to create a purchase → gets a Stripe
clientSecret - Frontend renders the Stripe embedded checkout using that
clientSecret - User completes payment in the Stripe checkout UI
- Stripe sends a webhook to the backend → purchase is marked as completed
- 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: falsewithhasAccess: 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:
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.
Event routing — The backend checks the event type and routes to the correct handler:
checkout.session.completed→ completes a pay-to-view purchasecharge.refunded→ marks the purchase as refunded
Purchase completion — For
checkout.session.completed:- Reads
payToViewPurchaseIdanduserIdfrom the session metadata - Finds the pending purchase in the database
- Sets
ExternalTransactionIdto the StripePaymentIntentId - Marks status as
Completed - Sets
ExpiresAtif the product hasAccessDurationDays - Grants the user access to the content
- Reads
Refund handling — For
charge.refunded:- Finds the purchase by the
PaymentIntentId - Marks status as
Refunded - Revokes access
- Finds the purchase by the
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 |