Integration guide

A Stripe Checkout integration that never trusts the browser.

A new SaaS product needs one server credential, one approved origin, registered SKUs, and a callback endpoint. The browser never chooses a price, user identity, or return URL.

Before the first request

Start the Stripe Checkout integration at the product boundary.

Yito Pay onboarding creates an app ID and a one-time server API key. The app record stores the product name, approved return origin, optional entitlement callback URL, and active status. Each hostname that displays Embedded Checkout is registered separately and every sellable SKU maps to a Stripe Price, payment mode, currency, and entitlement key.

Store the API key only in the product backend’s secret manager. Do not place it in a browser bundle, public environment variable, mobile application, analytics event, or support screenshot. Yito Pay stores only a keyed digest of the credential, so a lost key must be replaced rather than recovered.

  • Choose a stable app ID and customer identity.
  • Approve exact production and staging origins separately.
  • Create Stripe Prices before catalog registration.
  • Define the entitlement granted by each SKU.
  • Store the callback secret in the receiving backend.

Required product secrets

A small server-side configuration.

Product environment
YITO_PAY_BASE_URL=https://pay.yito.ai
YITO_PAY_APP_KEY=yp_live_••••••••
YITO_PAY_CALLBACK_SECRET=••••••••

The Stripe secret key and webhook signing secret remain inside Yito Pay. Individual products do not receive them.

Step 1

Create Checkout from the authenticated product server.

After confirming the signed-in user, generate a unique external order ID and call the Yito Pay API with the registered SKU. Reuse that same external order ID when retrying the request. A different user, SKU, or quantity under an existing ID is rejected visibly.

Server request
const response = await fetch(
  'https://pay.yito.ai/v1/checkout-sessions',
  {
    method: 'POST',
    headers: {
      'authorization': 'Bearer ' + process.env.YITO_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',
      quantity: 1
    })
  }
);

if (!response.ok) {
  throw new Error('Yito Pay returned ' + response.status);
}

const checkout = await response.json();

The response contains an order ID, publishable key, short-lived client secret, and recovery URL. Treat the client secret as sensitive session material and do not log it.

Step 2

Mount Embedded Checkout in the product modal.

Load the shared widget once, open it only after the server creates the order, and provide a container that behaves as an accessible dialog. The widget obtains the client secret from your own backend response and mounts Stripe directly in the product page—never through a nested Yito Pay iframe.

Browser integration
<script src="https://pay.yito.ai/sdk/v1/pay-widget.js"></script>
<div id="checkout" role="dialog" aria-modal="true"></div>
<script>
  const payment = await window.YitoPay.open({
    publishableKey: checkout.publishable_key,
    clientSecret: checkout.client_secret,
    statusUrl: '/api/billing/orders/' + checkout.order_id,
    onConfirmed: () => location.assign('/billing/success')
  });
</script>

The completion callback starts order polling; it does not grant access. If the embed cannot initialize, navigate to the returned recovery URL as a top-level page.

Step 3

Poll status for customer feedback.

The product backend can query GET /v1/orders/:orderId with its app key. Show a confirmed state only when the API returns paid or the product has processed a valid entitlement callback. A processing state should explain that payment confirmation is still underway instead of pretending the order failed.

Do not poll forever. After a short browser window, send the customer to a pending screen that can be refreshed safely. The verified webhook continues working even if the browser is gone.

Step 4

Verify the signed product callback.

Read the callback body as raw text, reject timestamps outside the allowed tolerance, calculate HMAC-SHA256 over timestamp.rawBody, and compare signatures in constant time. Store X-Yito-Event-Id before granting or revoking access.

Return a successful response only after the durable entitlement update is committed. A non-success response triggers a later delivery attempt with the same event ID.

Operational endpoints

Use server-only routes for billing actions.

Customer Portal sessions and refund requests require the app credential and validated product user or administrator context. They must not be exposed as unauthenticated browser calls. Health checks are public and reveal only service readiness.

Order status

GET /v1/orders/:orderId returns the app-scoped order state without exposing Stripe secrets.

Customer Portal

POST /v1/customer-portal-sessions creates a short-lived Stripe billing management link.

Refund request

POST /v1/refunds uses the internal order ID and a deterministic idempotency key.

Questions, answered

Frequently asked questions

Can a product send an amount instead of a SKU?

No. The registered SKU is the only pricing input. Yito Pay loads the Stripe Price, currency, payment mode, and entitlement from its server-side catalog.

Where should the Yito Pay app key be stored?

Store it in the product backend secret manager. Never send it to a browser or embed it in a client bundle. The browser should call the authenticated product backend first.

What should happen if Embedded Checkout cannot load?

Open the recovery URL returned with the checkout response as a top-level page. It uses the same order boundary without creating a nested iframe.

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.