---
title: Stripe Setup
description: "Stripe is the most fully supported gateway in RackHelm: it is the only one that handles checkout, paying an invoice from the client portal, saving a card, background renewal charges and a verified webhook. This article walks through the whole…"
url: "https://rackhelm.com/documentation/stripe-setup/"
updated: "2026-09-24"
category: "Billing & Payments"
---

# Stripe Setup

Stripe is the most fully supported gateway in RackHelm: it is the only one that handles checkout, paying an invoice from the client portal, saving a card, background renewal charges and a verified webhook. This article walks through the whole setup, including the webhook secret — which has no field on the admin form and needs a one-off command. It is written for operators.

## Before you start

- A Stripe account with API keys, and access to **Developers → Webhooks** in the Stripe dashboard.
- Access to **Billing → Payment Gateways** in RackHelm, which in a default install means a Super Admin account.
- Shell access to the RackHelm server for the webhook-secret step.
- The portal must be reachable over HTTPS from the public internet for Stripe to deliver webhooks.

## Step 1 — Enter your API keys

1. Open **Billing → Payment Gateways**.
2. Select **Add Gateway** and choose Stripe, or **Configure** on the Stripe row if you have already added it.
3. Paste your **Publishable Key** (`pk_test_…` or `pk_live_…`).
4. Paste your **Secret Key** (`sk_test_…` or `sk_live_…`).
5. Tick **Enable Gateway** and select **Save Changes**.

> **Warning:** The **Mode** dropdown does not change Stripe's behaviour. Stripe uses a single API host and the key decides the environment, so a live secret key takes real money even while the list shows "Sandbox". Treat the key prefix as the source of truth.

> **Note:** The Secret Key field is always rendered blank. Leaving it blank on a later save keeps the stored key; only type in it when you are genuinely rotating the key.

![The Configure Stripe dialog showing the Enable Gateway switch, Mode selector, Publishable Key field and a masked Secret Key field](/wp-content/uploads/rackhelm-docs/billing-gateway-stripe-config.png)
The Stripe configuration dialog. Note that there is no webhook-secret field here — that is set separately.

## Step 2 — Create the webhook endpoint in Stripe

The webhook is the backstop that settles an invoice when the customer closes the browser mid-payment. Without it, a payment that Stripe accepted but whose return trip never completed leaves the invoice open.

1. In the Stripe dashboard, open **Developers → Webhooks → Add endpoint**.
2. Set the endpoint URL to your portal's payment webhook:
   ```bash
   https://your-portal.example.com/webhooks/payment
   ```
3. Subscribe to the events RackHelm acts on. The handler processes exactly one:
   - `payment_intent.succeeded`

   Subscribing to anything else is harmless — unrecognised events are acknowledged and ignored — but adds noise.
4. Save the endpoint and copy its **Signing secret** (`whsec_…`).

> **Note:** Create a separate endpoint in each Stripe environment you use. Test-mode and live-mode endpoints have different signing secrets, and RackHelm stores only one.

## Step 3 — Store the signing secret

RackHelm reads the signing secret from the Stripe gateway's stored configuration under the key `webhook_secret`, but the admin form has no field for it. Set it once from the server, from the directory RackHelm is installed in:

```bash
php artisan tinker --execute="$g = AppModelsPaymentGateway::where('slug','stripe')->first(); $c = $g->config; $c['webhook_secret'] = 'whsec_REPLACE_ME'; $g->config = $c; $g->save(); echo 'saved';"
```

> **Warning:** Saving the Stripe dialog in the admin area replaces the gateway's whole stored configuration with what the form submitted, and the form has no webhook-secret field — so **the secret is wiped every time you save that dialog**. Re-run the command above after any change to the Stripe keys, and re-test delivery from the Stripe dashboard.

## Step 4 — Verify

1. In Stripe, use **Send test webhook** on the endpoint and pick `payment_intent.succeeded`. A correctly configured portal returns `200` with `{"status":"ok"}`.
2. `400` with `Webhook secret not configured` means step 3 has not taken effect.
3. `400` with `Invalid webhook` means the stored secret does not match the endpoint's signing secret — usually a test secret against a live endpoint or vice versa.
4. Place a small real order using a Stripe test card in test mode and confirm the invoice reaches `paid`, the service provisions, and the customer receives the "Payment Successful" email.

## What Stripe is used for, and how

| Flow | What RackHelm does |
| --- | --- |
| Checkout | Creates a SetupIntent so the browser can collect card details, then a PaymentIntent for the cart total in the store currency. The capture is serialised per order with a 120-second lock and carries an idempotency key derived from the order and the card, so a double-submit cannot charge twice. |
| Paying an invoice in the client portal | Creates a PaymentIntent for the invoice's stored `amount`, tagged with the invoice id in its metadata. An existing intent is reused only if its amount still matches and it is still payable; if a late fee has changed the total, the stale intent is discarded and a new one minted. |
| Saving a card | Creates or reuses a Stripe customer for the account, attaches the payment method, and confirms a SetupIntent with `off_session` usage. If the bank demands 3D Secure, the portal returns the customer to a challenge and completes the save afterwards. |
| Renewals | Creates an off-session, auto-confirming PaymentIntent against the saved method, attaching it to the Stripe customer first if it is not already. |
| Webhook | Resolves the invoice from the intent's `metadata.invoice_id`, checks that the amount actually received covers the invoice total, then settles it. Retried deliveries are harmless: an already-paid invoice is detected and no second receipt is sent. |

### Currency

Checkout charges in the store currency (the `currency` setting, defaulting to `GBP`). Invoice payments from the client portal and background renewal charges are created in **GBP** regardless of that setting. If you operate in another currency, test both paths before going live.

### Minimum amount

An invoice total below **£0.50** cannot be paid by card — the portal shows "Amount (£x) is too small to process via Stripe" instead of a payment form. The same floor applies when account credit covers most of an invoice: the split is adjusted so the card is charged at least 50p, with credit covering the rest.

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| Webhook returns 400 "Webhook secret not configured" | No `webhook_secret` stored, or it was wiped by saving the admin dialog. Re-run the command in step 3. |
| Webhook returns 400 "Invalid webhook" | The signing secret does not match, or a proxy is altering the request body — signature verification is computed over the raw payload. |
| Webhook returns 404 or 500 from the edge | Either the endpoint is behind authentication or a firewall, or there is no *active* Stripe gateway row — the handler looks one up and aborts with 404 when it cannot find it. `/webhooks/payment` is deliberately public and unauthenticated; its security comes from the Stripe signature. |
| Customer paid but the invoice is still open | Look for "Short payment on Invoice" in the application log — the intent collected less than the invoice total, most often because a late fee raised the total after the intent was minted. Reconcile by hand and mark the invoice paid. |
| "Could not initialize payment system" on the invoice page | The Stripe API call failed. Check the secret key is valid for the environment and look at the application log for the Stripe error message. |
| Card form never appears | The Publishable Key is blank, or the gateway is not active. The key is only published to the page when the gateway row is active. |
| Renewal charges fail with "User has no Stripe customer id" | The account never went through a Stripe flow. Ask the customer to add a card in the client portal, which creates the Stripe customer. |
| Renewal charges fail with "No saved payment method" | The customer has no default method, or their only method belongs to another gateway. The automatic charge run emails the customer in this case rather than failing silently. |
| "This payment method belongs to a different customer" | The card token was already attached to another Stripe customer. The customer must re-enter the card details. |
| Stripe elements are blocked in the browser console | A second Content Security Policy header is being sent by the web server, intersecting with RackHelm's. Remove it — RackHelm's own policy already allows `js.stripe.com`, `hooks.stripe.com` and `api.stripe.com`. |
