Payments
Let your app take real payments.
An app takes payments into its owner's Stripe account — no Stripe key in the code. Ask the agent ("add a checkout for the courses") and it builds the payment flow with totalum.payments (SDK). These calls are the account owner's.
- Try it in the preview right away. The preview always takes Stripe test cards, with no setup: pay with
4242 4242 4242 4242, any future date and any CVC. No real money moves. - Publish, then connect your Stripe to receive real payments — one click in the project's Payments window, or Create Payments Onboarding Link: Stripe asks for your business and bank details on its own page and the money goes to your Stripe account.
- Switch the published site to real payments when you are ready (Set Payments Mode). The preview stays in test mode.
Payment events arrive by themselves. Renewals, cancellations, failed payments and refunds reach the app's own /api/webhooks/stripe route with nothing to set up, on the site where the checkout was made: test payments made in the preview go to the preview, test payments made on the published site (before it takes real payments) and real payments go to the published site (on its custom domain if it has one). They are signed by Totalum, and the app checks them with totalum.webhooks.verify (SDK). To also send them to another address (your own server, Zapier…), set the optional project webhook (in the platform: Payments → Advanced).
Checkout opens in a new tab. Stripe's payment page cannot run inside the platform's preview frame, so the app opens Checkout and the customer portal in a new tab unless you ask otherwise.
Already have a Stripe account and prefer your own keys? Add your secret key as a project secret named STRIPE_SECRET_KEY and ask the agent to use it ("take payments with my own Stripe key"). The agent then calls Stripe from the app's server only, never from the browser. To receive Stripe's events, also add your endpoint's signing secret as STRIPE_WEBHOOK_SECRET and add https://<your site>/api/webhooks/stripe as an endpoint in your Stripe Dashboard: with your own keys that route checks Stripe's signature instead of Totalum's. Whether it charges test or real cards is up to the key (sk_test_… or sk_live_…).
#Get Payments Status
/api/v1/vcaas/projects/:projectId/payments/statusapi-keyFreeYour connected Stripe account and whether it can take payments.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | "live" (default: your Stripe account) or "test" (the preview's test account) |
Response fields
| Field | Type | Description |
|---|---|---|
data.enabled | boolean | A Stripe account is connected |
data.mode | string | "connect" |
data.accountId | string | null | The Stripe account id |
data.livemode | boolean | true for the live account |
data.chargesEnabled | boolean | It can take payments |
data.payoutsEnabled | boolean | It can receive payouts |
data.detailsSubmitted | boolean | Onboarding is complete |
data.requirements | object | { currentlyDue, pastDue, eventuallyDue }: what Stripe still asks for |
data.liveEnabled | boolean | The published site charges in live mode |
data.disconnectedAt | string | null | undefined | When the owner disconnected the account from Stripe |
Example request
curl -H "api-key: tlm_sk_your_key" \
"https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/payments/status?mode=live"const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/payments/status?mode=live", {
headers: {
"api-key": process.env.TOTALUM_API_KEY,
},
});
const { errors, data } = await res.json();{ "errors": null, "data": { "enabled": true, "mode": "connect", "accountId": "acct_1Q2w3E4r", "livemode": true, "chargesEnabled": true, "payoutsEnabled": true, "detailsSubmitted": true, "requirements": { "currentlyDue": [], "pastDue": [], "eventuallyDue": [] }, "liveEnabled": false, "disconnectedAt": null } }#Create Payments Onboarding Link
/api/v1/vcaas/projects/:projectId/payments/onboarding-linkapi-keyFreeA single-use link to Stripe's onboarding, where the owner connects (or creates) their Stripe account to receive real payments. The first link creates the account and needs its country. Publish the project first: real payments go to the published site, so a project that was never published is refused with PAYMENTS_PUBLISH_FIRST. The preview needs no onboarding: it takes test cards with no setup.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | "live" (default and the only one: "test" is refused, the preview's test account needs no onboarding) |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
returnUrl | string | Yes | https:// page to come back to when done |
refreshUrl | string | Yes | https:// page that asks for a new link if this one expired |
type | string | No | "account_onboarding" (default) or "account_update" |
country | string | No | Two-letter country of the business: required for the first link (and after a disconnection), cannot change later |
Response fields
| Field | Type | Description |
|---|---|---|
data.url | string | The onboarding link |
data.expiresAt | string | When it expires |
Example request
curl -X POST \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"returnUrl":"https://yourapp.com/payments/done","refreshUrl":"https://yourapp.com/payments/retry","country":"ES"}' \
"https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/payments/onboarding-link"const res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/payments/onboarding-link", {
method: "POST",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"returnUrl": "https://yourapp.com/payments/done",
"refreshUrl": "https://yourapp.com/payments/retry",
"country": "ES"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "url": "https://connect.stripe.com/setup/e/acct_1Q2w3E4r/…", "expiresAt": "2026-10-08T13:10:00.000Z" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
OWNER_ONLY | 403 | Account owner only |
VALIDATION_ERROR | 400 | Missing or non-https URLs, or mode=test |
MISSING_INPUT | 400 | No country for a new account |
PAYMENTS_PUBLISH_FIRST | 409 | The project was never published: publish it, then connect Stripe |
#Set Payments Mode
/api/v1/vcaas/projects/:projectId/payments/modeapi-keyFreeSwitch the published site between test and real (live) payments (10 switches an hour). Live needs your connected Stripe account to have completed onboarding. The preview always stays in test mode.
Path parameters
| Parameter | Type | Description |
|---|---|---|
projectId | string | The project id |
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | Yes | "test" or "live" |
Response fields
| Field | Type | Description |
|---|---|---|
data.mode | string | The mode now |
Example request
curl -X PUT \
-H "api-key: tlm_sk_your_key" \
-H "Content-Type: application/json" \
-d '{"mode":"live"}' \
https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/payments/modeconst res = await fetch("https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/payments/mode", {
method: "PUT",
headers: {
"api-key": process.env.TOTALUM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
"mode": "live"
}),
});
const { errors, data } = await res.json();{ "errors": null, "data": { "mode": "live" } }Error codes
| Code | HTTP | Meaning |
|---|---|---|
PAYMENTS_LIVE_ONBOARDING_REQUIRED | 403 | Finish connecting your Stripe account first |
OWNER_ONLY | 403 | Account owner only |
RATE_LIMITED | 429 | More than 10 switches in an hour |
