# Add Stripe Checkout to a Django App

Yes. An agent adds a Stripe Checkout endpoint and a signature-checked webhook to your Django app, tests it in Stripe test mode and opens a pull request.

[Run this on Rush](https://cloud.getrush.ai/run/add-stripe-checkout/django/), in a Project that has your repository.

## What the agent does

The agent clones the app into its own cloud sandbox and reads how it is put together before adding payments to it. In a Django project like wagtail/bakerydemo, `requirements/base.txt` asks for `Django>=6.0,<6.1` and `wagtail>=8.0,<8.1`, `.python-version` says `3.12`, and the code is split into apps under `bakerydemo/` (`base`, `blog`, `breads`, `locations`, `recipes`, `search`) listed in `INSTALLED_APPS` in `bakerydemo/settings/base.py`. `bakerydemo/urls.py` mounts the Django admin, the Wagtail admin, `search/`, the API routes and finally `wagtail_urls` as a catch-all, so any new URL has to be added above that last line or Wagtail's page router will answer it. The repository's own `AGENTS.md` asks for tightly scoped changes and a pull request that says how to test them.

It adds a small `bakerydemo/payments` app instead of spreading payment code through the bread pages. `views.py` has a `create_checkout_session` view that calls `stripe.checkout.Session.create` with `mode="payment"`, one line item from a price id in settings, and `success_url` and `cancel_url` built with `request.build_absolute_uri`, then redirects to the session's `url`. A `success` view renders a template that reads the `session_id` query parameter but never treats it as proof of payment. A `webhook` view, marked `@csrf_exempt` and accepting only `POST`, reads the raw `request.body` and the `Stripe-Signature` header, calls `stripe.Webhook.construct_event` with the endpoint secret, answers `400` on a bad signature, and on `checkout.session.completed` records the session id, amount, currency and customer email in a `Payment` model, keyed on the session id so a retried event does not create a second row.

Keys stay out of the code. `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` and `STRIPE_PRICE_ID` are read from the environment in `settings/base.py`, matching how the file already reads `DATABASE_URL` and the CSP settings, and `.env.example` gets the three names with no values. The agent adds `stripe` to `requirements/base.txt` with a version range like its neighbours, and makes the migration for `Payment`, because CI runs `./manage.py makemigrations --check --noinput` and fails when one is missing.

Then it tests in two ways. Unit tests in `bakerydemo/payments/tests/` post signed and unsigned payloads to the webhook, signing them with a test secret the same way Stripe does, and assert the `400`, the recorded `Payment` and that a duplicate event records nothing new. With a Stripe test-mode key available, the agent runs `./manage.py runserver`, starts `stripe listen --forward-to localhost:8000/payments/webhook/`, uses the signing secret it prints, and runs `stripe trigger checkout.session.completed` so a real event from Stripe reaches the handler. It runs CI's commands, `./manage.py check`, the migration check and `DJANGO_SETTINGS_MODULE=bakerydemo.settings.test ./manage.py test`, plus `make lint-server`.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Add Stripe Checkout to this Django app for one fixed-price product. Read the settings, urls.py and AGENTS.md first and follow the project's patterns. Add an endpoint that creates a Checkout Session and redirects to it, a success page, and a webhook endpoint that verifies the Stripe-Signature header with stripe.Webhook.construct_event, returns 400 on a bad signature, and records checkout.session.completed once per session id in a model. Read STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET and STRIPE_PRICE_ID from the environment and add the names to .env.example. Add the stripe package and the migration. Write tests that sign webhook payloads with a test secret, and if a Stripe test-mode key is available, run the app with stripe listen forwarding to the webhook and stripe trigger checkout.session.completed. Run manage.py check, makemigrations --check, the test suite and the linters, and open a pull request with the test output and how to test it.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request that adds `bakerydemo/payments` (models, views, URLs, a template, a migration and tests), one `include` in `bakerydemo/urls.py` above the Wagtail catch-all, the app in `INSTALLED_APPS`, three settings read from the environment, `stripe` in `requirements/base.txt` and the three names in `.env.example`. The description says how to test it: set the three variables, run `stripe listen`, buy with Stripe's test card, and see the `Payment` row. It quotes the test output and, when a test-mode key was available, the `stripe trigger` and `stripe listen` lines showing the event delivered and answered.

The checks are the repository's `ci.yml`: lint with ruff, curlylint and djhtml, then `manage.py check`, the migration check and the test suite. The session's transcript keeps every command from the sandbox and its output.

Before merging, a reviewer checks that the success page grants nothing on its own, since anyone can visit a success URL, and that only the webhook marks an order paid. They confirm the webhook reads the raw body before anything parses it, which signature checking depends on, and that no key appears in the diff.

## Run it on every client that needs payments

An agency or a product that adds payments for many clients can send this request once per client repository, with that repository in `resources` and the client's product and currency in the prompt. Each run happens in its own sandbox, so one client's code and Stripe keys never meet another's.

With a webhook URL on the API key, Rush posts `run.completed` or `run.failed` with the `execution_id` and `session_url` when each turn ends. Your handler reads the session for the pull request and sends it to the client for review.

## Why run it on Rush?

Payment code is the code you most want proven before it merges, and a webhook can only be proven by receiving a real signed event. In a Rush sandbox the agent runs the Django server, the Stripe CLI and the tests together on its own machine, so `stripe trigger` sends a real test-mode event to a real handler without anyone opening a tunnel from their laptop.

Every command and its output is kept in the transcript, including the signature failures and the duplicate-event test, and the repository's CI runs on the pull request. Test-mode keys reach the run as Project secrets, so they are never pasted into the prompt or committed.

## Frequently asked questions

### How does the agent get a Stripe test key?

Save `STRIPE_SECRET_KEY` as a Project secret with `POST /o/{org}/secrets` and start the session in that Project with `POST /o/{org}/p/{project}/sessions`; the agent reads it as an environment variable. Without a key the agent still writes and runs the signed-payload tests, and says the live Stripe CLI check was not run.

### Why verify the webhook signature?

Anyone can POST to a public URL. `stripe.Webhook.construct_event` checks the `Stripe-Signature` header against your endpoint secret, so only events Stripe signed are recorded, and a bad one gets a 400.

### What if Stripe sends the same event twice?

Stripe retries deliveries, so the handler records each Checkout Session once, keyed on its id. A test sends the same signed event twice and asserts a single row.

### Does it work with a Django app that is not Wagtail?

Yes. The payment app, the settings and the tests are plain Django. Wagtail only changes where the URL include goes, which in bakerydemo is above the `wagtail_urls` catch-all.
