Payments and the shop
Everything on Forumulate that takes money takes it through your own Stripe account. There is no Forumulate payment account in the middle and never will be: a buyer pays you directly, Stripe pays out to you, and we only record what was bought so the right reader gets the right seat, post or order. We take no cut.
One connection serves every paid feature in the organization:
Feature | What is charged | Guide |
|---|---|---|
Blog memberships and paid posts | a recurring subscription (or a gift) | |
Event tickets | a ticket per seat | |
Courses | a one-off course purchase | |
Paid forms | a fixed or chosen amount with a form response | |
Community paid membership | a recurring membership or a one-off pass | |
The shop | products from your catalog, through a cart or "Buy now" | below |
Card details are only ever typed into Stripe Checkout, the payment page Stripe hosts. No card number reaches a page we serve, so your pages stay out of PCI scope.
Which plans can take payments
Forms are on every plan, the free plan included: every question type, branching, scoring and integrations. Taking payments is on paid plans only (Starter and up): a paid form, and the shop — its products, cart and checkout.
The reason is fraud, not money. Free accounts that accept payments are a common abuse vector (stolen cards tested against a throwaway storefront), and since payments go through your own Stripe account and we take no cut, the plan is the brake we can put on it.
What this means in practice:
On the free plan the studio shows an upgrade prompt where a form's payment switch and the shop's Add product button would be.
The API refuses a new payment with
402and the codeplan.required, naming the plan that includes it indetails.requiredPlan(starter) and the feature indetails.feature(commerce.payments). It is refused where a payment is enabled or started: creating a form with a payment, adding a payment to a form, creating a product, and starting a checkout (a cart, or a paid form's submit). A buyer who meets the refusal is told the site cannot take payments right now and that nothing was charged.Switching a form's payment off is always allowed, on any plan.
What already existed keeps working. Every paid form and every product that existed on an organization without a paid plan when this rule arrived (20 October 2026) is grandfathered: it keeps taking payments on any plan, and you can still edit and re-price it. Only new paid forms and new products need a paid plan. An organization that leaves a paid plan keeps its grandfathered items, but anything it set up while on the paid plan stops taking payments until it upgrades again.
Until these are done, nothing paid can be bought: the buy buttons are disabled or say so, and the API answers 409 with "This organization does not take card payments yet — connect Stripe in Settings → Payments."
A Stripe account of your own. Test mode works for everything below.
A secret key. In Stripe, under Developers → API keys, copy a secret key (
sk_test_…/sk_live_…). A restricted key (rk_…) is enough and is the safer choice. It needs write access to Checkout Sessions and Refunds. Blog memberships and community paid memberships also read and cancel Subscriptions, so give the key that access if you sell them.Enter it in the console: Settings → Payments, then Save. Press Test to have Stripe confirm the key works. The panel then shows whether the key is live or in test mode.
Turn sales on. The switch in the same panel (labelled Sell paid tickets) is the
enabledflag, and it covers every paid feature, not only tickets. Saving a first key turns it on. With it off, nothing new can be bought, but refunds and the webhook keep working.The webhook. The Payments panel shows your organization's webhook address,
https://forumulate.com/api/public/payments/stripe/{orgId}/webhook. In Stripe, under Developers → Webhooks, add an endpoint at that address and select these events:Event
Used by
checkout.session.completedeverything
checkout.session.expiredtickets, shop, paid forms (the order is marked abandoned)
charge.refundedeverything (a refund made in the Stripe dashboard is mirrored)
customer.subscription.updated,customer.subscription.deletedblog and community memberships (renewals, cancellations)
invoice.paid,invoice.payment_failed,checkout.session.async_payment_succeededcommunity paid membership
Copy the endpoint's signing secret (
whsec_…) and paste it into the Payments panel as the Webhook signing secret.Stripe Tax (optional, shop and paid forms only): if you want tax added at checkout, activate Stripe Tax in your Stripe dashboard first, then turn on Add tax with Stripe Tax in Settings → Shop. If you turn it on without setting Stripe Tax up, Stripe refuses to start the checkout.
One webhook for everything. Your organization has one Stripe connection, so it has one webhook. Each checkout we create carries a metadata.kind (event_ticket, course, shop, form_payment, blog_membership…), and the webhook hands every event to the feature that owns it. You never add a second endpoint for a second feature.
Without a signing secret, webhook events are ignored. An event we cannot verify is acknowledged and dropped, in every environment, because the webhook books paid seats. Purchases still complete without it: when Stripe sends the buyer back, the return page asks Stripe directly, with your key, whether the session was paid, and settles it. What you lose without the webhook is everything that happens when the buyer does not come back: a closed tab after paying, abandoned checkouts, refunds made in Stripe, subscription renewals and cancellations. Set the secret.
The keys are write-only
The secret key and the signing secret are encrypted at rest and never shown again, to anyone, including you. GET /orgs/{orgId}/payments/stripe says whether each is set (secretSet, webhookSecretSet), the mode (live or test), when the key was last verified and the last error. To change a key, send a new one. Leaving a field out of a PUT keeps what is stored, and webhookSecret: null removes the signing secret.
curl -X PUT "https://forumulate.com/api/orgs/$ORG/payments/stripe" \
-H "Authorization: Bearer $FORUMULATE_TOKEN" -H "Content-Type: application/json" \
-d '{ "secretKey": "rk_test_…", "webhookSecret": "whsec_…", "enabled": true }'
curl -X POST "https://forumulate.com/api/orgs/$ORG/payments/stripe/test" \
-H "Authorization: Bearer $FORUMULATE_TOKEN"
# { "ok": true, "accountName": "Stripe (test mode)" }DELETE /orgs/{orgId}/payments/stripe disconnects Stripe. All four routes need org.manage_settings. Connecting, changing and disconnecting fire the payments.connected, payments.updated and payments.disconnected webhooks. They carry flags only (enabled, mode, keyChanged), never a key.
The shop
The shop sells products from your organization's catalog on any page, the way Webflow's ecommerce does: a product block shows cards with a price, readers add to a cart or buy one straight away, and pay on Stripe Checkout.
Products
Products live in Settings → Shop → Products (or POST /orgs/{orgId}/shop/products).
Field | Default | What it does |
|---|---|---|
| (required) | Up to 120 characters. |
| from the name | Lower-case letters, digits and hyphens. Unique in the organization (a clash answers |
| none | Up to 2,000 characters, shown on the card. |
| none | An uploaded picture ( |
| (required) | The price in the smallest unit ( |
|
| Three letters, lower case. |
|
| Units left. |
|
| Ask for a shipping address at checkout. |
|
| On sale. An inactive product is hidden from every page. |
|
| Its order in the catalog. |
|
| The space that sells it. |
A reader never sees a stock count, only "Sold out" when stock reaches zero.
The product block
In the editor, type / and choose Products (searching "shop", "buy" or "cart" finds it too). The block names products by id, in the order you pick them, or names none, which means "the catalog's active products, in catalog order".
Attribute | Default | What it does |
|---|---|---|
|
| Up to 50 products. Empty = the catalog. |
|
|
|
|
| Cards per row in a grid, 1–4. |
|
|
|
|
| |
|
| |
| "Add to cart" / "Buy now" | Up to 40 characters. Without it the words follow the page's language. |
| 50 | How many, when the block shows the catalog. |
The block carries no copy of the products. Prices, names and pictures are read from the catalog every time the page is rendered, so a price you change in Settings → Shop is the price on every page at once, and a page can never state a price of its own. In markdown the block is :::product{data="…"}::: (Markdown directives).
The cart is kept in the reader's browser, per organization, and shown as a floating button with a drawer. It is a convenience, not a record: when the reader checks out, the API prices every line again from the catalog and ignores whatever the browser thinks a product costs. One cart pays in one currency, so mixing currencies is refused with a 422.
Checkout
Checkout opens a Stripe Checkout page on your account. What it offers is set in Settings → Shop → Checkout:
Setting | Default | What it does |
|---|---|---|
| off | Accept promotion codes. Buyers can enter a code on the Stripe page. You create the codes in your own Stripe dashboard (Products → Coupons). |
| off | Add tax with Stripe Tax. Stripe works out sales tax or VAT. Needs Stripe Tax set up in Stripe first. |
| empty | Ships to: two-letter country codes, comma separated ( |
These are ordinary organization settings, so they can also be written with PUT /orgs/{orgId}/settings/{key}. They apply to paid forms too.
The return address rule. Stripe sends the buyer back to the page the cart sits on, and the API checks that page first. It must be https:// and must be one of your addresses: our own host, or a custom domain your organization has verified and activated. Anything else is refused with a 422 (returnUrl: ["foreign"]). Without this, a crafted request could turn your checkout into a redirect to anywhere. The reader script, for its part, only ever navigates to Stripe's own checkout host.
What happens after paying. The order is created as pending when checkout starts. It becomes paid exactly once, whichever arrives first, the webhook or the buyer's return to your page (POST /public/orgs/{orgId}/shop/confirm/{sessionId} asks Stripe and settles the order). That single transition takes the stock, fires shop.order.paid and clears the reader's cart. Stock is not reserved while the buyer is on Stripe. It is checked when checkout starts and taken when the money arrives. If the last unit sold in between, the order is refunded automatically: overselling is worse than a rare refund.
Orders and refunds
Settings → Shop → Orders lists every order with its items, total, buyer and status: Awaiting payment (pending), Paid, Refunded, or Abandoned (expired, the checkout timed out). A paid order has a Refund button, which refunds it in full through Stripe and puts tracked stock back. A refund you make in the Stripe dashboard is mirrored here through the webhook.
curl "https://forumulate.com/api/orgs/$ORG/shop/orders?status=paid" \
-H "Authorization: Bearer $FORUMULATE_TOKEN"
curl -X POST "https://forumulate.com/api/orgs/$ORG/shop/orders/$ORDER/refund" \
-H "Authorization: Bearer $FORUMULATE_TOKEN"Orders from paid forms appear in the same list with kind: "form", linked to the form and the response (formId, submissionId).
Webhooks (Webhooks): shop.order.paid (order id, kind, items, total, currency, buyer email, shipping address) and shop.order.refunded. A paid form also fires form.paid.
Who can do it. The catalog, the checkout switches, the orders and refunds all need org.manage_settings, the same capability as the Stripe connection: the money and what is sold against it are one permission.
The public side
GET /public/orgs/{orgId}/shop/products?ids=…&spaceId=… the catalog a block draws (active products only)
POST /public/orgs/{orgId}/shop/checkout { items: [{ productId, quantity }], returnUrl, email? } → { url, orderId }
POST /public/orgs/{orgId}/shop/confirm/{sessionId} the return page's check → { status, orderId }A checkout takes 1–50 lines, each 1–99 units. The routes are throttled per network (checkout at 10 a minute). Nothing on them accepts a price.
Paid forms
A form can take a payment with each response, Typeform- and Tally-style. Turn on Take a payment with each response in the form's editor (forms.payment): a fixed amount, or the amount from one of the form's own answers, bounded by a minimum and maximum. The answers are filed first as pending, the reader pays on Stripe Checkout, and only then is the response announced to you. Details in Forms.
Taking a payment needs a paid plan (above); the form itself does not.
In the API
Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.
Org Payments
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Get the stripe in an organization |
| |
| Replace the stripe in an organization |
| |
| Delete the stripe in an organization |
| |
| Test the stripe |
|
Public Shop
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List products in an organization (public reader) |
| |
| Create the checkout in an organization (public reader shop) |
| |
| Create the confirm |
|
Shop
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List products in an organization |
| |
| Create a product in an organization |
| |
| Update the product |
| |
| Delete the product |
| |
| List orders in an organization |
| |
| Create the refund in an organization |
|