# Totalum App Builder API — Payments

This file is the complete reference for this area. The short guide for agents is https://www.totalum.app/totalum-api.md.

- Base URL: `https://builder-api.totalum.app/api/v1/vcaas`
- Authentication: header `api-key: tlm_sk_…` on every request, from a server only.
- Response envelope: `{ "errors": null | { "errorCode", "errorMessage", "errorDetails?" }, "data": … }`.


---

# 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](https://www.totalum.app/docs/sdk/payments)). These calls are the account owner's.

1. **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.
2. **Publish, then connect your Stripe to receive real payments** — one click in the project's **Payments** window, or [Create Payments Onboarding Link](#create-payments-onboarding-link): Stripe asks for your business and bank details on its own page and the money goes to your Stripe account.
3. **Switch the published site to real payments** when you are ready ([Set Payments Mode](#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](https://www.totalum.app/docs/sdk/webhooks)). To also send them to another address (your own server, Zapier…), set the optional [project webhook](https://www.totalum.app/docs/api/webhooks#set-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](https://www.totalum.app/docs/api/secrets) 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

```endpoint
method: GET
path: /api/v1/vcaas/projects/:projectId/payments/status
cost: Free
```

Your 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**

```bash
curl -H "api-key: tlm_sk_your_key" \
  "https://builder-api.totalum.app/api/v1/vcaas/projects/barber-bookings/payments/status?mode=live"
```

:::success Success · 200 OK
```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

```endpoint
method: POST
path: /api/v1/vcaas/projects/:projectId/payments/onboarding-link
cost: Free
```

A 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**

```bash
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"
```

:::success Success · 200 OK
```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](https://www.totalum.app/docs/api/deployments#deploy-to-production), then connect Stripe |

## Set Payments Mode

```endpoint
method: PUT
path: /api/v1/vcaas/projects/:projectId/payments/mode
cost: Free
```

Switch 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**

```bash
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/mode
```

:::success Success · 200 OK
```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 |
