Skip to main content
This guide wires billing into your own product. Your app’s Upgrade button creates a quote through the /v1 API, sends the customer to Custral’s hosted quote page, and learns the outcome by webhook. The customer’s card or bank details go to Stripe’s page, the subscription runs on your workspace’s Stripe account, and the quote, invoices and payments land on the customer’s record in Custral. No endpoint charges a card, bills an invoice or changes a live subscription. Money moves only when the customer accepts or pays.

Before you start

1

Set up payments

An admin completes Settings → Payments: a Products object, a Customers object, Stripe connected, and payment defaults. Connect a Stripe test-mode account while you build, so Stripe’s test cards work and nobody is charged.
2

Pick a customer key

On the Developers tab of Settings → Payments, set Customer key to the field on your Customers object that holds your app’s own customer id (an Account ID, say). Then you can name customers by your id instead of a Custral record id.
3

Allow your return domain

Add your app’s host (for example app.example.com) to Allowed return domains. A returnUrl on any other host is refused.
4

Create an API key

In Settings → Applications, create a secret key with the quotes:write and billing:read scopes. Add billing:portal if you’ll open billing pages (step 6).
5

Register a webhook endpoint

Subscribe it to the quote and billing events you need. See the webhooks quickstart.

1. Create the quote

When the customer clicks Upgrade, your server creates a quote with POST /v1/quotes:
  • customer is your app’s id (matched through the customer key) or a Custral record id (rec_…).
  • createCustomer creates the customer record when none matches, with your id as its key. Without it, an unknown customer is refused with customer_not_found.
  • products names catalog products by record id or exact name. unitPrice sets a custom price for this deal, in major units (49.5 is $49.50).
  • send: true marks the quote sent in the same call, so its url opens right away. Sending emails nobody. You hand the customer the link yourself.
  • A returnUrl must be https (http only for localhost), and its host must be in Allowed return domains. A refused URL is checked before anything is written, so no quote is left behind.
Discounts, way to pay and expiry are optional; see the full parameter list.

2. Send the customer to the hosted page

Redirect to quote.url. The page is branded with your workspace and needs no sign-in. The customer reviews the lines and what they’ll be charged when, types their name and email, and accepts.
  • Autopay quotes hand over to Stripe’s hosted page, where the customer adds a card or bank account. The quote is accepted only once it’s saved.
  • Send invoices quotes are accepted straight away, and Stripe emails each invoice with a pay link.

3. The customer comes back

After accepting, the page shows “Quote accepted” with a Continue button. It takes the customer to your returnUrl with two parameters added:
Any query string or fragment already on your returnUrl is kept. Use the return to show a page (“Thanks, you’re on Pro”), not to grant access: a customer can close the tab before they click Continue.

4. Hear the outcome by webhook

Grant access when the webhook arrives. Custral starts the subscription on Stripe right after acceptance, and the events follow:
Delivery is at-least-once and events don’t arrive in a guaranteed order, so make listeners idempotent. Events carry the Custral recordId, not your own id; keep the mapping, or read billing back by your id as below.

5. Read billing back

Whenever your app needs the current state, read it live from Stripe with GET /v1/customers/{customer}/billing:
It returns the customer’s quotes, subscriptions (with MRR, discount and way to pay), invoices (with each one’s hostedUrl pay link) and upcoming instalments. For the workspace as a whole, custral.billing.invoices({period: "30d"}) returns receivables with totals per currency, and custral.billing.metrics() returns MRR, ARR and net revenue retention.

6. Give customers a billing page

Once a customer is paying, they need somewhere to see their plan, change their card, read invoices, switch plans and cancel. Custral hosts that page; your server opens it for one customer at a time. Add billing:portal to your key, then open a session when the customer clicks Billing in your app:
The session returns url, token and expiresAt. It lasts 15 minutes, so open one per visit rather than storing it. The page shows what you turned on in Revenue › Billing page (or with PATCH /v1/billing/page), and its Back link goes to returnUrl.

Or draw it inside your app

Pass the token to CustralBilling.mount from @custral/js instead of redirecting:
The frame grows to fit the page, so your app shows no scrollbar inside it. When the customer adds a card or chooses a plan, Stripe’s page opens at the top level and sends them back to the page that mounted the widget, which confirms the change. onTokenExpired is optional. With it, the widget swaps in the fresh token and reloads the frame; without it, the page says the session has ended.

What the customer can do there

Your server can cancel or keep a plan too, without the page:

SDK methods at a glance

Common refusals

See the Quote object for the full list. To set up Stripe, the catalog and the billing page itself from code, see Set up billing from code.