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 (auto-subscribes if needed)
POST /api/apps/:appId/verify-subscription Verify after 3DS auth
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. If the app doesn’t have a subscription seat, one is added automatically:

  • First deploy: Creates a new subscription with quantity=1
  • Subsequent deploys of new apps: Adds a seat (increments quantity)
  • Redeployment of already-subscribed app: Just deploys (no subscription change)

The response varies depending on subscription state:

  • Deploy result — subscription active, site deployed successfully
  • requires_payment_method — no card on file; use the returned clientSecret to mount Stripe’s Payment Element
  • requires_action — 3DS/SCA authentication required

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 (data.status === "requires_payment_method") {
  // Mount Stripe Payment Element with data.clientSecret
} else {
  // Deploy succeeded — data contains URL, pages, etc.
}

Response (deploy success)

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

Response 200 (requires payment method)

{
  "status": "requires_payment_method",
  "subscriptionId": "sub_1234",
  "clientSecret": "pi_xxx_secret_yyy"
}

Errors

Code Status When
CARD_ERROR 400 Card declined or invalid
DEPLOY_FAILED 500 Deployment failed

Verify subscription after 3DS

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

Call after the user completes 3DS/SCA authentication to confirm the payment succeeded and activate the subscription.

Response

{
  "status": "active"
}

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.