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 returnedclientSecretto mount Stripe’s Payment Elementrequires_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.