Guides
GuidePayment Providers

Payment Providers

Get paid the way that works where you and your members are. Every payment method is an optional, independent switch — turn on only what you need. Use just bank transfer if that’s all you want; add cards later; mix and match freely.

How it works: open your app’s Payments tab, flip on a method, and (for Stripe) paste your key. However a member pays, a successful payment unlocks the same paid access — so the rest of your app doesn’t care which provider they used.

Built-in methods

Stripe — cards & subscriptions

Visa, Mastercard and recurring subscriptions worldwide. Paste your Stripe secret key and you’re live; we finish the webhook setup for you so payments unlock access automatically.

Bank transfer / cash — manual

No processor at all. Show your bank details or a QR, the member pays you directly, and you approve it — access unlocks on approval. Perfect when card processors aren’t available or you just want to keep it simple.

Test vs Live

Stripe has its own Test / Live button. Test mode takes fake card payments so you can try the whole checkout-to-access flow safely; Live takes real money. Switching Stripe to Live doesn’t touch anything else.

Bank transfer / cash has no processor to sandbox — it’s simply on or off, and you’re the one confirming each payment is real before you approve it.

Need a provider that isn’t listed?

You are not limited to the built-in methods. Want Razorpay, Khalti, PayPal, Paytm, or a local processor? Serenities can connect any gateway through a small backend function that grants access through the same entitlement ledger Stripe and manual payments use. You don’t have to wait for us to add it as a built-in.

The easiest path: just ask the AI assistant in your app to set it up, for example —

"Add Razorpay as a payment option and unlock the ‘pro’ plan when a payment succeeds."

It stores your gateway key securely as a project environment variable (never in page code), opens that gateway’s own checkout, verifies each payment with the gateway’s API, and unlocks paid access — exactly like Stripe.

Bottom line: the built-in methods are the fast path, not a fence. If a gateway exists, your app can take money through it.

Prefer to wire it yourself? (for builders)

The AI does exactly what a builder would do by hand. If you’d rather build it, here’s the whole pattern. It rests on one idea: access is entitlement-based, so nothing cares how a payment was collected — a verified payment grants an entitlement, and your access rules check the entitlement, exactly as they would for Stripe or manual payments.

  1. Put the gateway’s keys in project environment variables (e.g. RAZORPAY_TEST_KEY_ID /RAZORPAY_TEST_KEY_SECRET, and aRAZORPAY_MODE = test/live toggle) — never hardcode a key in a function. Read them with ctx.env.get('NAME').
  2. On your checkout page, call a backend function to create the order (the secret stays server-side), then open the gateway’s own checkout widget (their JS SDK).
  3. The gateway’s success callback (or webhook) hits your project’s backend function endpoint — give the function a public access rule so it’s reachable with no login. Treat its input as untrusted — verify the payment (check the signature, or refetch from the gateway’s API).
  4. On a verified payment, grant one billing period of access. A replay guard makes sure the same payment can’t unlock twice.
Runtime note: backend functions run on Deno, so use Web APIs —crypto.subtle (Web Crypto),btoa, crypto.getRandomValues — not Node’s crypto module.

The verification backend function — the security-critical part (real Razorpay pattern, Deno):

// Backend function "verifyRazorpayPayment" — params is UNTRUSTED input
const { razorpay_order_id, razorpay_payment_id, razorpay_signature, appUserId } = params;
if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature || !appUserId)
  throw new Error('Missing required fields');

// Keys come from ENV VARS, never hardcoded. ctx.env.get returns { value }.
const mode = await ctx.env.get('RAZORPAY_MODE') || 'test';
const keySecret = await ctx.env.get(mode === 'live' ? 'RAZORPAY_LIVE_KEY_SECRET' : 'RAZORPAY_TEST_KEY_SECRET');
if (!keySecret) throw new Error('Razorpay ' + mode + ' key secret not configured');

// 1) Verify the signature with Web Crypto (Deno): HMAC-SHA256(order|payment), hex.
const enc = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey('raw', enc.encode(keySecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const sigBuf = await crypto.subtle.sign('HMAC', cryptoKey, enc.encode(razorpay_order_id + '|' + razorpay_payment_id));
const expected = Array.from(new Uint8Array(sigBuf)).map((b) => b.toString(16).padStart(2, '0')).join('');
if (expected !== razorpay_signature) throw new Error('Payment verification failed');

// 2) Replay guard — one grant per gateway payment id.
const key = 'pro';
const got = await ctx.entitlements.get(appUserId, key);
const existing = got && got.entitlement;
if (existing?.source?.paymentId === razorpay_payment_id) return { granted: false, reason: 'already processed' };

// 3) One period per verified charge; a renewal extends from the current expiry.
const PERIOD_MS = 30 * 86400000;
const base = existing?.expiresAt && new Date(existing.expiresAt) > new Date()
  ? new Date(existing.expiresAt).getTime() : Date.now();
const expiresAt = new Date(base + PERIOD_MS).toISOString();
existing
  ? await ctx.entitlements.extend(appUserId, key, expiresAt, { source: { provider: 'razorpay', paymentId: razorpay_payment_id } })
  : await ctx.entitlements.grant(appUserId, key, { expiresAt, source: { provider: 'razorpay', paymentId: razorpay_payment_id } });
return { granted: true, expiresAt };

Why this is safe: access closes automatically when the entitlement lapses, so a missed renewal fails closed even if a callback never arrives. Gate any content with the same rule you’d use for Stripe: { "type": "has_entitlement", "key": "pro" }.

Recurring subscriptions map the same way: each renewal charge → verify → extend one period; a failed payment → suspend with a grace period; a cancellation → revoke. The full, annotated version lives in the in-app AI code guide (payments topic).