Documentation

Subscriptions & Billing

Deploy, cancel, reactivate, change plans, and manage payments via the OrbitKit API.

OrbitKit uses a seat-based subscription model. Your account has one subscription, and each deployed app uses one seat. Deploying an app automatically creates or adds a seat to your subscription. OrbitKit uses Stripe for payment processing with support for monthly ($5/mo per seat) and yearly ($50/yr per seat) plans.

Endpoints

Method Path Description
POST /api/apps/:appId/deploy Deploy (adds a seat if already subscribed; 402 if not)
POST /api/apps/:appId/create-subscription Start a subscription (embedded Payment/Setup Element)
POST /api/apps/:appId/checkout-session Start a subscription (Stripe-hosted Checkout)
POST /api/apps/:appId/verify-checkout Finalize a Stripe Checkout subscription
POST /api/apps/:appId/verify-subscription Verify after card confirmation / 3DS
POST /api/cancel-subscription Cancel at period end
POST /api/reactivate-subscription Undo pending cancellation
POST /api/change-plan Switch monthly/yearly
POST /api/setup-intent Create SetupIntent for payment method
PUT /api/payment-method Update default payment method
POST /api/billing-portal Open Stripe Billing Portal
GET /api/invoices List recent invoices

Deploy (with auto-subscribe)

POST /api/apps/:appId/deploy

Deploys an app’s site. Behavior depends on subscription state:

  • App already subscribed (seat active): just re-deploys, returns the deploy result.
  • Account has a usable subscription, app is new: adds a seat (increments quantity), then deploys.
  • No usable subscription: returns 402 SUBSCRIPTION_REQUIRED. Deploy does not create a subscription or return a client secret. Start one first via create-subscription (embedded) or checkout-session (hosted), complete payment, then call deploy again.

A “usable” subscription is one with status active, past_due, or trialing (first-app free trial).

Full example

curl -X POST https://api.orbitkit.io/api/apps/-NtestApp123/deploy \
  -H "Authorization: Bearer $TOKEN"
var request = URLRequest(url: URL(string: "https://api.orbitkit.io/api/apps/\(appId)/deploy")!)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
let result = try JSONDecoder().decode(DeployResponse.self, from: data)
const res = await fetch(`https://api.orbitkit.io/api/apps/${appId}/deploy`, {
  method: "POST",
  headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();

if (res.status === 402) {
  // No usable subscription — start one via create-subscription /
  // checkout-session, then call deploy again.
} else {
  // Deploy succeeded — data contains url, deployedAt, pages, etc.
}

Response (deploy success)

{
  "url": "https://sites.orbitkit.io/my-app",
  "deployedAt": 1712345678000,
  "pages": {
    "privacyPolicy": "https://sites.orbitkit.io/my-app/privacy"
  }
}

Errors

Code Status When
SUBSCRIPTION_REQUIRED 402 No usable subscription — subscribe first, then deploy
DEPLOY_FAILED 500 Deployment failed

Create subscription (embedded)

POST /api/apps/:appId/create-subscription

Body: { "plan": "monthly" | "yearly" }. Starts the account’s subscription using Stripe’s embedded Payment/Setup Element. Returns a client secret to confirm on the client.

The response includes a mode field — you MUST branch on it:

mode Meaning Client action
"payment" A normal first charge — clientSecret is a PaymentIntent stripe.confirmPayment(...)
"setup" First-app free trial — nothing due today; clientSecret is a SetupIntent stripe.confirmSetup(...)
{
  "clientSecret": "seti_xxx_secret_yyy",
  "subscriptionId": "sub_1234",
  "mode": "setup"
}

After the client confirms, call verify-subscription.


Checkout session (hosted)

POST /api/apps/:appId/checkout-session

Body: { "plan": "monthly" | "yearly" }. Returns { "url": "https://checkout.stripe.com/..." } — redirect the user there. Trial eligibility is applied automatically (first-app free trial; card collected). On return, call verify-checkout with the checkout_session_id from the success URL.

POST /api/apps/:appId/verify-checkout

Body: { "sessionId": "cs_..." }. Finalizes the subscription (accepts trialing as success) and deploys the site. Returns the deploy result.


Verify subscription

POST /api/apps/:appId/verify-subscription

Call after the embedded card/3DS/SetupIntent confirmation. Confirms the subscription is usable (active or trialing, normalized to active) and deploys the site.

Response (success)

{
  "status": "active",
  "url": "https://sites.orbitkit.io/my-app",
  "pages": { "privacyPolicy": "https://sites.orbitkit.io/my-app/privacy" }
}

If the subscription isn’t usable yet, returns { "status": "<stripe status>" } (e.g. "incomplete", "past_due") with no deploy fields — retry after the client finishes confirmation.


Cancel subscription

POST /api/cancel-subscription

Cancels the entire account subscription at the end of the current billing period. All sites remain active until the period ends.

Response

{
  "status": "active",
  "cancelAtPeriodEnd": true,
  "currentPeriodEnd": 1712345678
}

Errors

Code Status When
NO_ACTIVE_SUBSCRIPTION 400 No active subscription to cancel
ALREADY_CANCELING 400 Already scheduled for cancellation

Reactivate subscription

POST /api/reactivate-subscription

Removes the pending cancellation. Only valid when cancelAtPeriodEnd is true.

Response

{
  "status": "active",
  "cancelAtPeriodEnd": false
}

Errors

Code Status When
NO_ACTIVE_SUBSCRIPTION 400 No active subscription
NOT_CANCELING 400 Subscription is not scheduled for cancellation

Change plan

POST /api/change-plan

Switches between monthly and yearly plans for the entire subscription. Stripe prorates the charge automatically.

Request body

Field Type Required Description
plan string Yes "monthly" or "yearly"

Response

{
  "planType": "yearly",
  "currentPeriodEnd": 1743881678
}

Errors

Code Status When
NO_ACTIVE_SUBSCRIPTION 400 No active subscription to change
SUBSCRIPTION_CANCELING 400 Reactivate first before changing plan
SAME_PLAN 400 Already on the requested plan

Create setup intent

POST /api/setup-intent

Creates a Stripe SetupIntent for adding or updating a payment method. Use the returned clientSecret with Stripe.js to mount the Payment Element.

Response 201 Created

{
  "clientSecret": "seti_xxx_secret_yyy"
}

Errors

Code Status When
NO_STRIPE_CUSTOMER 400 Stripe customer not set up

Update payment method

PUT /api/payment-method

Sets a new default payment method after the user completes the SetupIntent flow.

Request body

Field Type Required Description
paymentMethodId string Yes Stripe payment method ID

Response

{
  "paymentMethodId": "pm_1234"
}

Billing portal

POST /api/billing-portal

Returns a Stripe Billing Portal URL where the user can view invoices and manage payment methods.

Response

{
  "url": "https://billing.stripe.com/p/session/..."
}

Errors

Code Status When
NO_STRIPE_CUSTOMER 400 No Stripe customer record

List invoices

GET /api/invoices

Returns up to 24 recent Stripe invoices.

Response

{
  "invoices": [
    {
      "id": "in_1234",
      "number": "INV-0001",
      "amount": 500,
      "currency": "usd",
      "status": "paid",
      "created": 1712345678,
      "pdfUrl": "https://pay.stripe.com/invoice/...",
      "hostedUrl": "https://invoice.stripe.com/...",
      "description": "OrbitKit Monthly"
    }
  ]
}

Returns {"invoices": []} if the user has no billing history.