Integration guide

A same-page Stripe payment modal that never trusts the browser.

The shared widget opens a DOM modal inside each approved product—not a new browser window or automatic redirect—while app credentials, registered EUR SKUs, and signed callbacks keep financial terms off the client.

Before the first request

Register the product boundary.

Vidle Pay onboarding stores each approved product's exact HTTPS origin, return paths, callback URL, Mindepy merchant route, and sellable EUR SKUs. Each SKU maps to an active app-scoped Stripe Price, payment mode, billing interval, amount, and entitlement key.

The one-time app key belongs only in that product's backend secret manager. Never place it in a browser bundle, public environment variable, analytics event, mobile app, log, or screenshot.

  • Use a stable external user ID.
  • Create a unique external order ID.
  • Send only a registered SKU and quantity.
  • Keep the app key server-side.
  • Verify signed callbacks before changing access.

Product secrets

A small server-side configuration.

Product environment
VIDLE_PAY_BASE_URL=https://pay.vidle.org
VIDLE_PAY_APP_KEY=vp_live_••••••••
VIDLE_PAY_CALLBACK_SECRET=••••••••
VIDLE_PAY_CHECKOUT_PROVIDER=stripe

Stripe keys and webhook verification secrets remain inside Vidle Pay and are never shared with the product.

Step 1

Create Checkout from the authenticated server.

Confirm the signed-in user, generate a unique order ID, and call Vidle Pay with the registered SKU. Reuse the same external order ID only for an identical retry.

Server request
const response = await fetch(
  'https://pay.vidle.org/v1/checkout-sessions',
  {
    method: 'POST',
    headers: {
      authorization: 'Bearer ' + process.env.VIDLE_PAY_APP_KEY,
      'content-type': 'application/json',
      'idempotency-key': externalOrderId
    },
    body: JSON.stringify({
      external_order_id: externalOrderId,
      external_user_id: authenticatedUser.id,
      sku: 'pro_monthly',
      provider: 'stripe',
      quantity: 1
    })
  }
);

if (!response.ok) throw new Error('Vidle Pay returned ' + response.status);
const checkout = await response.json();

Do not log the returned Checkout client secret or recovery credential.

Step 2

Open a same-page modal, not a popup window.

Load the shared widget once. It creates a Shadow DOM modal and mounts Stripe's provider-owned Checkout directly over the product page. It does not call window.open() or navigate the customer away. Link may appear for eligible buyers without a separate Link SDK.

Browser integration
<script src="https://pay.vidle.org/sdk/v1/pay-widget.js"></script>
<script>
  await window.VidlePay.open({
    provider: 'stripe',
    publishableKey: checkout.publishable_key,
    clientSecret: checkout.client_secret,
    statusUrl: '/api/billing/orders/' + checkout.order_id,
    onConfirmed: () => location.assign('/billing/success'),
    onPending: () => showPendingPaymentMessage(),
    onError: (error) => reportCheckoutError(error)
  });
</script>

Register the app with managed_payments_enabled=false so Checkout returns an embeddable client secret. The SDK loads its modal CSS automatically and requires a same-origin status URL. Completion starts status polling; it is not payment proof. Offer the recovery URL only as an explicit top-level action when Stripe.js cannot initialize.

Step 3

Show durable order status.

Query GET /v1/orders/:orderId from the product backend with its app key. Show confirmed only for paid or after the signed entitlement callback is committed. Delayed methods should remain processing.

Do not poll forever. A pending screen can be refreshed safely while verified Stripe events continue without the browser.

Step 4

Verify the product callback.

Read the raw body, reject stale timestamps, calculate HMAC-SHA256 over timestamp.rawBody, and compare signatures in constant time. Store X-Vidle-Pay-Event-Id before changing access.

Return success only after the durable entitlement update commits. Retries preserve the same semantic event ID.

Order status

GET /v1/orders/:orderId reads only the authenticated app's order.

Subscription management

POST /v1/customer-portal-sessions opens Stripe Billing Portal for the stored customer.

Refund request

POST /v1/refunds uses a deterministic idempotency key and the order's immutable Stripe payment reference.

Questions, answered

Frequently asked questions

Can the product send an amount instead of a SKU?

No. A registered SKU is the only pricing input. Vidle Pay loads the trusted EUR Price, amount, mode, interval, and entitlement from its server catalog.

Where should the app key be stored?

Store it in the integrating product backend secret manager. Never send it to a browser or include it in a client-accessible build variable.

Does checkout open a browser popup or redirect?

No. The SDK creates a DOM modal over the current product page and mounts Stripe Embedded Checkout inside it, so ordinary popup blockers do not apply. A recovery-page navigation is shown only as an explicit fallback, while Stripe controls any required bank authentication.

Is PayPal available now?

No. The entire pay.vidle.org pool is Stripe-only; its control API rejects PayPal and no PayPal production credential or Queue is installed.

Build on a reliable payment boundary

Give every SaaS product one secure way to charge.

Start with the integration guide, then register the app, domain, and server credentials before enabling checkout.