The rule for payments: the client asks, the server decides. Prices, what was bought and whether it's paid are set in cloud code and confirmed by Stripe's webhook, never taken from the client.
1. Keys
In the Stripe dashboard copy your secret key, then (after step 3) the webhook's signing secret. Set both as environment variables of your app (Runtime › Environment Variables), e.g. STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET. Add Stripe's library to your repository's package.json:
npm install stripe
2. Start a checkout from cloud code
// cloud/payments.js (require it from cloud/main.js)
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
Parse.Cloud.define('createCheckout', async (request) => {
const { user } = request;
if (!user) throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Log in first.');
// the price comes from your Stripe catalog, not from the client
const order = new Parse.Object('Order', { user, status: 'pending', priceId: 'price_123' });
order.setACL(new Parse.ACL(user)); // the buyer reads it; only the server writes
await order.save(null, { useMasterKey: true });
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: order.get('priceId'), quantity: 1 }],
client_reference_id: order.id,
success_url: 'https://example.com/thanks',
cancel_url: 'https://example.com/cart',
});
return { url: session.url };
});
The client sends the user to Stripe:
const { url } = await Parse.Cloud.run('createCheckout');
window.location.href = url;
3. Receive Stripe's webhook
Your app's own routes (cloud/app.js) answer at its public URL, so the webhook goes to https://pg-app-<id>.scalabl.cloud/webhooks/stripe (or your custom domain). Add that endpoint in Stripe for the checkout.session.completed event.
// cloud/app.js
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
// the raw body: Stripe signs the exact bytes it sent
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET
);
} catch (e) {
return res.status(400).send('Invalid signature');
}
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
const order = await new Parse.Query('Order').get(session.client_reference_id, { useMasterKey: true });
if (order.get('status') !== 'paid') { // Stripe may send an event more than once
order.set({ status: 'paid', stripeSession: session.id });
await order.save(null, { useMasterKey: true });
}
}
res.sendStatus(200);
});
module.exports = app;
cloud/main.js keeps module.exports.app = require('./app');. Push to deploy.
4. Lock the Order class
In Core › Browser › Order › Security, give clients no Create, Update or Delete: orders are created and marked paid only by your cloud code with the master key. Users read their own orders through the ACL set in step 2.
Test it
Use Stripe's test keys and test cards first. The Stripe CLI can send events to your app (stripe trigger checkout.session.completed), and Core › Logs shows your webhook's errors.