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 viacreate-subscription(embedded) orcheckout-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.