---
title: Accept payments with Stripe
nav: Payments (Stripe)
group: Build your app
updated: 2026-11-13
description: Take payments in your SashiDo app with Stripe Checkout. Create the checkout session in cloud code, receive Stripe's webhook on your app's own route, verify its signature, and mark orders paid where clients can't.
keywords: parse server stripe, sashido stripe, stripe checkout parse server, stripe webhook parse cloud code, payments backend parse
---

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](/en/docs/developers/api#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`:

```bash
npm install stripe
```

## 2. Start a checkout from cloud code

```javascript
// 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:

```javascript
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](/en/docs/developers/hosting)). Add that endpoint in Stripe for the `checkout.session.completed` event.

```javascript
// 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.
