Skip to content
CADCAMFUN
Docs

Documentation

Billing

Subscription information, Stripe checkout/portal integration, trial maintenance, and signed webhook ingestion.

Playground

Send these requests to your own running sidecar. Values stay in this browser.

Not connected
How this works

The sidecar listens on a loopback port chosen at startup and prints it in its ready line; use that URL and the CADCAMFUN_TOKEN it was started with. Requests go straight from this page to that address.

The sidecar sends CORS headers only to the Tauri origins and to http://localhost or http://127.0.0.1 on any port. From the hosted docs the browser blocks the response, so run the docs locally (bun run dev in packages/web) to use the playground, or copy the request as curl. To experiment without touching real data, start the sidecar with CADCAMFUN_DATA_DIR pointed at a throwaway directory — see the API walkthrough.

Operations

15 endpoints in the billing group, generated from the sidecar's own OpenAPI document. Protected operations take Authorization: Bearer $CADCAMFUN_TOKEN; see authentication. Behaviour and limits are in the subsystem guide.

get /api/subscription/info billing.info
get /api/subscriptions billing.current
post /api/subscriptions billing.subscribe
post /api/subscriptions/portal billing.portal
get /api/better-auth-stripe/subscription billing.baGet
post /api/better-auth-stripe/subscription billing.baCheckout
put /api/better-auth-stripe/subscription billing.baUpdate
delete /api/better-auth-stripe/subscription billing.baCancel
post /api/better-auth-stripe/billing-portal billing.baPortal
post /api/better-auth-stripe/feature-access billing.baFeature
post /api/better-auth-stripe/sync billing.baSync
get /api/cron/update-expired-trials billing.cronGet
post /api/cron/update-expired-trials billing.cronPost
get /api/health/stripe billing.stripeHealth
post /api/webhooks/stripe billing.stripeWebhook
Billing operations in 3D Drag to orbit · scroll to zoom · right-drag to pan
One block per operation (15), stacked per resource section of this page and coloured by method: green GET, orange POST, blue PUT, violet PATCH, red DELETE. Generated from the OpenAPI document at build time.

/api/subscription/info

get /api/subscription/info # billing.info

Requires Authorization: Bearer <token>.

Parameters
NameInTypeRequired
userId query string | null optional
Responses
200 object application/json
{
  id: string
  plan: string
  status: string
  currentPeriodEnd: string | null
  cancelAtPeriodEnd: boolean | null
  stripeCustomerId: string | null
  stripeSubscriptionId: string | null
}
400 BillingBadRequestEncoded application/json
BillingBadRequestEncoded
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
403 BillingForbiddenEncoded application/json
BillingForbiddenEncoded
404 BillingNotFoundEncoded application/json
BillingNotFoundEncoded
500 BillingFailureEncoded application/json
BillingFailureEncoded
Example response captured · 400
GET /api/subscription/info
{
  "_tag": "BillingBadRequest",
  "error": "User ID is required"
}
SDK example
billing.info
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// BillingInfoInput
const result = await client.billing.info({})
curl
GET /api/subscription/info
curl --request GET "${CADCAMFUN_URL}/api/subscription/info" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}"
Try it

/api/subscriptions

get /api/subscriptions # billing.current

Requires Authorization: Bearer <token>.

Responses
200 object application/json
{
  plan: string
  status: string
  periodEnd: string | null
  cancelAtPeriodEnd: boolean
  isOnTrial?: boolean | null
  trialEnd?: string | null
}
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
500 InternalErrorEncoded application/json
InternalErrorEncoded
Example response captured · 200
GET /api/subscriptions
{
  "plan": "FREE",
  "status": "active",
  "periodEnd": null,
  "cancelAtPeriodEnd": false
}
SDK example
billing.current
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// no input
const result = await client.billing.current()
curl
GET /api/subscriptions
curl --request GET "${CADCAMFUN_URL}/api/subscriptions" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}"
Try it
post /api/subscriptions # billing.subscribe

Requires Authorization: Bearer <token>.

Request body · required · application/json
{
  priceId?: string | null
  startTrial?: boolean | null
}
Responses
200 object application/json
{
  url?: string | null
  success?: boolean | null
  message?: string | null
  redirectUrl?: string | null
}
400 BadRequestEncoded application/json
BadRequestEncoded
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
404 NotFoundEncoded application/json
NotFoundEncoded
500 InternalErrorEncoded application/json
InternalErrorEncoded
SDK example
billing.subscribe
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// BillingSubscribeInput
const result = await client.billing.subscribe({
  priceId: "",
  startTrial: false
})
curl
POST /api/subscriptions
curl --request POST "${CADCAMFUN_URL}/api/subscriptions" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}" \
  --header "Content-Type: application/json" \
  --data-binary @request.json
Try it

/api/subscriptions/portal

post /api/subscriptions/portal # billing.portal

Requires Authorization: Bearer <token>.

Request body · required · application/json
{
  returnUrl?: string | null
}
Responses
200 object application/json
{
  url: string
}
400 BadRequestEncoded application/json
BadRequestEncoded
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
404 NotFoundEncoded application/json
NotFoundEncoded
500 InternalErrorEncoded application/json
InternalErrorEncoded
SDK example
billing.portal
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// BillingPortalInput
const result = await client.billing.portal({
  returnUrl: ""
})
curl
POST /api/subscriptions/portal
curl --request POST "${CADCAMFUN_URL}/api/subscriptions/portal" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}" \
  --header "Content-Type: application/json" \
  --data-binary @request.json
Try it

/api/better-auth-stripe/subscription

get /api/better-auth-stripe/subscription # billing.baGet

Requires Authorization: Bearer <token>.

Responses
200 object application/json
{
  id?: string | null
  plan: string
  status: string
  currentPeriodEnd: string | null
  cancelAtPeriodEnd: boolean
  stripeCustomerId?: string | null
  stripeSubscriptionId?: string | null
  source: "better-auth"
}
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
500 BillingFailureEncoded application/json
BillingFailureEncoded
Example response captured · 200
GET /api/better-auth-stripe/subscription
{
  "id": "c942817d-0162-4291-84ff-77321d387acc",
  "plan": "FREE",
  "status": "active",
  "currentPeriodEnd": null,
  "cancelAtPeriodEnd": false,
  "stripeCustomerId": null,
  "stripeSubscriptionId": null,
  "source": "better-auth"
}
SDK example
billing.baGet
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// no input
const result = await client.billing.baGet()
curl
GET /api/better-auth-stripe/subscription
curl --request GET "${CADCAMFUN_URL}/api/better-auth-stripe/subscription" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}"
Try it
post /api/better-auth-stripe/subscription # billing.baCheckout

Requires Authorization: Bearer <token>.

Request body · required · application/json
{
  plan?: string | null
  successUrl?: string | null
  cancelUrl?: string | null
  trialDays?: number | "Infinity" | "-Infinity" | "NaN" | null
}
Responses
200 object application/json
{
  url: string
  source: "better-auth"
}
400 BillingBadRequestEncoded application/json
BillingBadRequestEncoded
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
500 BillingFailureEncoded application/json
BillingFailureEncoded
SDK example
billing.baCheckout
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// BillingBaCheckoutInput
const result = await client.billing.baCheckout({
  plan: "",
  successUrl: "",
  cancelUrl: "",
  trialDays: 0
})
curl
POST /api/better-auth-stripe/subscription
curl --request POST "${CADCAMFUN_URL}/api/better-auth-stripe/subscription" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}" \
  --header "Content-Type: application/json" \
  --data-binary @request.json
Try it
put /api/better-auth-stripe/subscription # billing.baUpdate

Requires Authorization: Bearer <token>.

Request body · required · application/json
{
  newPlan?: string | null
}
Responses
200 object application/json
{
  success: true
  message: string
  source: "better-auth"
}
400 BillingBadRequestEncoded application/json
BillingBadRequestEncoded
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
500 BillingFailureEncoded application/json
BillingFailureEncoded
SDK example
billing.baUpdate
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// BillingBaUpdateInput
const result = await client.billing.baUpdate({
  newPlan: ""
})
curl
PUT /api/better-auth-stripe/subscription
curl --request PUT "${CADCAMFUN_URL}/api/better-auth-stripe/subscription" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}" \
  --header "Content-Type: application/json" \
  --data-binary @request.json
Try it
delete /api/better-auth-stripe/subscription # billing.baCancel

Requires Authorization: Bearer <token>.

Responses
200 object application/json
{
  success: true
  message: string
  source: "better-auth"
}
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
500 BillingFailureEncoded application/json
BillingFailureEncoded
SDK example
billing.baCancel
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// no input
const result = await client.billing.baCancel()
curl
DELETE /api/better-auth-stripe/subscription
curl --request DELETE "${CADCAMFUN_URL}/api/better-auth-stripe/subscription" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}"
Try it

/api/better-auth-stripe/billing-portal

post /api/better-auth-stripe/billing-portal # billing.baPortal

Requires Authorization: Bearer <token>.

Request body · required · application/json
{
  returnUrl?: string | null
}
Responses
200 object application/json
{
  url: string
  source: "better-auth"
}
400 BillingBadRequestEncoded application/json
BillingBadRequestEncoded
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
500 BillingFailureEncoded application/json
BillingFailureEncoded
SDK example
billing.baPortal
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// BillingBaPortalInput
const result = await client.billing.baPortal({
  returnUrl: ""
})
curl
POST /api/better-auth-stripe/billing-portal
curl --request POST "${CADCAMFUN_URL}/api/better-auth-stripe/billing-portal" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}" \
  --header "Content-Type: application/json" \
  --data-binary @request.json
Try it

/api/better-auth-stripe/feature-access

post /api/better-auth-stripe/feature-access # billing.baFeature

Requires Authorization: Bearer <token>.

Request body · required · application/json
{
  feature?: string | null
  features?: unknown | null
}
Responses
200 object application/json
{
  feature?: string | null
  hasAccess?: boolean | null
  source: "better-auth"
  features?: Array<{
    feature: string
    hasAccess: boolean
    error?: string | null
  }> | null
}
400 BillingBadRequestEncoded application/json
BillingBadRequestEncoded
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
500 BillingFailureEncoded application/json
BillingFailureEncoded
SDK example
billing.baFeature
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// BillingBaFeatureInput
const result = await client.billing.baFeature({
  feature: "",
  features: null
})
curl
POST /api/better-auth-stripe/feature-access
curl --request POST "${CADCAMFUN_URL}/api/better-auth-stripe/feature-access" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}" \
  --header "Content-Type: application/json" \
  --data-binary @request.json
Try it

/api/better-auth-stripe/sync

post /api/better-auth-stripe/sync # billing.baSync

Requires Authorization: Bearer <token>.

Responses
200 object application/json
{
  success: true
  message: string
  subscription: {
    id?: string | null
    plan: string
    status: string
    currentPeriodEnd: string | null
    cancelAtPeriodEnd: boolean
    stripeCustomerId?: string | null
    stripeSubscriptionId?: string | null
    source: "better-auth"
  } | null
  source: "better-auth"
}
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
500 BillingFailureEncoded application/json
BillingFailureEncoded
SDK example
billing.baSync
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// no input
const result = await client.billing.baSync()
curl
POST /api/better-auth-stripe/sync
curl --request POST "${CADCAMFUN_URL}/api/better-auth-stripe/sync" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}"
Try it

/api/cron/update-expired-trials

get /api/cron/update-expired-trials # billing.cronGet

Requires Authorization: Bearer <token>.

Parameters
NameInTypeRequired
secret query string | null optional
Responses
200 object application/json
{
  success: true
  updatedCount: number | "Infinity" | "-Infinity" | "NaN"
  message: string
  timestamp: string
}
401 CronUnauthorizedEncoded | UnauthorizedEncoded application/json
CronUnauthorizedEncoded | UnauthorizedEncoded
500 CronFailureEncoded application/json
CronFailureEncoded
Example response captured · 200
GET /api/cron/update-expired-trials
{
  "success": true,
  "updatedCount": 0,
  "message": "Successfully updated 0 expired trials",
  "timestamp": "2026-10-15T04:52:23.475Z"
}
SDK example
billing.cronGet
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// BillingCronGetInput
const result = await client.billing.cronGet({})
curl
GET /api/cron/update-expired-trials
curl --request GET "${CADCAMFUN_URL}/api/cron/update-expired-trials" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}"
Try it
post /api/cron/update-expired-trials # billing.cronPost

Requires Authorization: Bearer <token>.

Parameters
NameInTypeRequired
secret query string | null optional
Responses
200 object application/json
{
  success: true
  updatedCount: number | "Infinity" | "-Infinity" | "NaN"
  message: string
  timestamp: string
}
401 CronUnauthorizedEncoded | UnauthorizedEncoded application/json
CronUnauthorizedEncoded | UnauthorizedEncoded
500 CronFailureEncoded application/json
CronFailureEncoded
SDK example
billing.cronPost
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// BillingCronPostInput
const result = await client.billing.cronPost({})
curl
POST /api/cron/update-expired-trials
curl --request POST "${CADCAMFUN_URL}/api/cron/update-expired-trials" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}"
Try it

/api/health/stripe

get /api/health/stripe # billing.stripeHealth

Requires Authorization: Bearer <token>.

Responses
200 object application/json
{
  status: "healthy"
  message: string
  timestamp: string
  service: "stripe"
}
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
503 StripeUnhealthyEncoded application/json
StripeUnhealthyEncoded
Example response captured · 503
GET /api/health/stripe
{
  "_tag": "StripeUnhealthy",
  "status": "unhealthy",
  "message": "Stripe is not configured on this installation",
  "timestamp": "2026-10-15T04:52:23.476Z",
  "service": "stripe"
}
SDK example
billing.stripeHealth
import { CadCam } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

// no input
const result = await client.billing.stripeHealth()
curl
GET /api/health/stripe
curl --request GET "${CADCAMFUN_URL}/api/health/stripe" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}"
Try it

/api/webhooks/stripe

post /api/webhooks/stripe # billing.stripeWebhook

Requires Authorization: Bearer <token>.

Request body · required · application/octet-stream
string
Responses
200 object application/json
{
  received: true
}
400 BadRequestEncoded application/json
BadRequestEncoded
401 UnauthorizedEncoded application/json
UnauthorizedEncoded
fetch example
billing.stripeWebhook
// Not generated in the Promise SDK (multipart, binary or raw body): call it with fetch.
const response = await fetch(`${process.env.CADCAMFUN_URL}/api/webhooks/stripe`, {
  method: "POST",
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})
curl
POST /api/webhooks/stripe
curl --request POST "${CADCAMFUN_URL}/api/webhooks/stripe" \
  --header "Authorization: Bearer ${CADCAMFUN_TOKEN}" \
  --header "Content-Type: application/octet-stream" \
  --data-binary @request-body
Try it

This operation takes application/octet-stream; use the curl or fetch example.

Schemas

Named schemas reachable from this group's parameters, bodies and responses. A ? marks an optional field; | null marks a field that may be present with a null value.

BadRequestEncoded
{
  _tag: "BadRequest"
  message: string
}
BillingBadRequestEncoded
{
  _tag: "BillingBadRequest"
  error: string
}
BillingFailureEncoded
{
  _tag: "BillingFailure"
  error: string
}
BillingForbiddenEncoded
{
  _tag: "BillingForbidden"
  error: string
}
BillingNotFoundEncoded
{
  _tag: "BillingNotFound"
  error: string
}
CronFailureEncoded
{
  _tag: "CronFailure"
  success: false
  error: string
  timestamp: string
}
CronUnauthorizedEncoded
{
  _tag: "CronUnauthorized"
  error: string
}
InternalErrorEncoded
{
  _tag: "InternalError"
  message: string
}
NotFoundEncoded
{
  _tag: "NotFound"
  message: string
}
StripeUnhealthyEncoded
{
  _tag: "StripeUnhealthy"
  status: "unhealthy"
  message: string
  timestamp: string
  service: "stripe"
}
UnauthorizedEncoded
{
  _tag: "Unauthorized"
  message: string
}

Calling this group

Operation ids have the form billing.<name>. The generated Promise SDK (@cadcamfun/api/sdk) exposes most of them as client.<namespace>.<method>(input), where input is one flat object of path parameters and body fields; each card's SDK example shows the exact accessor and input type. Multipart uploads, binary or JavaScript responses and raw webhooks are not generated; those cards show fetch instead.

Errors from the Promise SDK
import { CadCam, ClientError } from "@cadcamfun/api/sdk"

const client = CadCam.make({
  baseUrl: process.env.CADCAMFUN_URL!,
  headers: { authorization: `Bearer ${process.env.CADCAMFUN_TOKEN}` },
})

try {
  await client.projects.list()
} catch (error) {
  if (error instanceof ClientError) {
    // Transport | UnexpectedStatus | UnsupportedContentType | MalformedResponse | SseEventTooLarge
  } else {
    // a declared error body: switch on error._tag
  }
}

Troubleshooting

SymptomLikely causeFix
401 on every callToken from a previous launch, or missing Bearer prefixSend Authorization: Bearer <token> with the token the sidecar was started with.
400 before anything happensBody or parameter does not match the schemaCompare the payload with the request schema on the card.
403 or 404 on a record you can see elsewhereThe acting account differsSend the right x-session-token, or use the local profile that owns the record.
409Conflicting state, such as a duplicate name or a stale branch headRe-read the resource and retry with current data.
Try it says "blocked by CORS"The docs are served from a non-loopback originRun the docs locally or copy the request as curl.
Connection refusedThe sidecar restarted on a new portRead the port from the new ready line.