API reference

Billing

7 endpoints, served at https://orbitwrite.com/api/v1. Each one lists the credentials it accepts, its parameters and every response it can give.

Read a workspace's billing summary#

GET/api/v1/billing

Plan details (any member) plus the Stripe customer email and recent invoices (owner/admin only — null/empty for plain members). Stripe read failures degrade to defaults rather than erroring.

Authentication

A signed-in session only. The Orbitwrite app calls this; an API key is refused.

Query parameters

organizationIdstringRequired

At least 1 character.

Responses

200BillingSummary

The workspace's billing summary

Response attributes
canManagebooleanRequired
emailstring | nullRequired

Stripe customer email — null for plain members

invoicesBillingInvoice[]Required

Recent invoices — empty for plain members

Child attributes
idstringRequired
numberstring | nullRequired
statusstringRequired
amountnumberRequired
currencystringRequired
createdstring | nullRequired
hostedUrlstring | nullRequired
pdfUrlstring | nullRequired
planDetailsBillingPlanDetailsRequired
Child attributes
planstringRequired
pricenumber | nullRequired
currencystring | nullRequired
intervalstring | nullRequired
nextBillingDatestring | nullRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
501Error

Billing is not configured (STRIPE_SECRET_KEY unset)

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/billing
curl -X GET "https://orbitwrite.com/api/v1/billing?organizationId=organizationId" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "canManage": true,
  "email": "string",
  "invoices": [
    {
      "id": "string",
      "number": "string",
      "status": "string",
      "amount": 0,
      "currency": "string",
      "created": "string",
      "hostedUrl": "string",
      "pdfUrl": "string"
    }
  ],
  "planDetails": {
    "plan": "string",
    "price": 0,
    "currency": "string",
    "interval": "string",
    "nextBillingDate": "string"
  }
}

Update the workspace's billing email#

POST/api/v1/billing/email

Owner/admin only. Updates the Stripe customer's email.

Authentication

A signed-in session only. The Orbitwrite app calls this; an API key is refused.

Body application/json

organizationIdstringRequired

At least 1 character.

emailstringRequired

At least 1 character.

Responses

200BillingMutationResult

The mutation was applied

Response attributes
okbooleanRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
501Error

Billing is not configured (STRIPE_SECRET_KEY unset)

Response attributes
errorstringRequired
issuesany[]
502Error

Stripe rejected or failed the customer update

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/billing/email
curl -X POST "https://orbitwrite.com/api/v1/billing/email" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "organizationId": "string",
  "email": "string"
}'
Response
{
  "ok": true
}

Sync the local subscription row from Stripe#

POST/api/v1/billing/sync

Pulls the subscription's status/cancellation/period fields from Stripe into the local row. Best-effort: always returns ok, even when Stripe is unreachable or there is no subscription. Owner/admin only.

Authentication

A signed-in session only. The Orbitwrite app calls this; an API key is refused.

Body application/json

organizationIdstringRequired

At least 1 character.

Responses

200BillingMutationResult

The mutation was applied

Response attributes
okbooleanRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
501Error

Billing is not configured (STRIPE_SECRET_KEY unset)

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/billing/sync
curl -X POST "https://orbitwrite.com/api/v1/billing/sync" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "organizationId": "string"
}'
Response
{
  "ok": true
}

Plan limits, features and current usage#

GET/api/v1/entitlements

What the workspace's plan allows and how much of each allowance it has spent this period.

Use remaining to pace a batch: a null value is unlimited, and xPosts is the monthly X allowance and xLinkPosts the much smaller one for posts containing a link; xPostsPerDay is null on every listed plan. storageBytes is a running total rather than a per-period count.

A quota with a rate in overage doesn't block at zero remaining — going over is allowed and billed at that rate. On plans with overage.channels set, remaining.channels is the remaining included channels before per-channel billing starts, not a cap.

Authenticates with a workspace API key or a session cookie.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

organizationIdstring

Session callers only — an API key already determines the workspace.

At least 1 character.

Responses

200Entitlements

The workspace's entitlements

Response attributes
planstringRequired

Plan name. Never null — a workspace with no plan resolves to none, whose limits are all closed.

sourcestringRequired

Where the plan comes from: a paid or trialing subscription; a grant, a plan held with no one billing it (workspaces that were on the retired free plan were moved onto Standard this way); or none.

  • "subscription"
  • "grant"
  • "none"
statusstring | nullRequired

Stripe subscription status, or null when the plan is a grant or there is none.

trialEndsAtstring | nullRequired

When the free trial ends, while the subscription is trialing. Null otherwise.

unrestrictedbooleanRequired

Billing is not enforced for this deployment (dev / self-hosted): every limit is open and every feature on.

periodobjectRequired
Child attributes
keystringRequired
startstringRequired
endstringRequired
featuresstring[]Required

Gated capabilities this plan unlocks.

limitsPlanLimitsRequired
Child attributes
membersinteger | nullRequired
includedChannelsinteger | nullRequired
channelsPerPlatforminteger | nullRequired
postsPerMonthinteger | nullRequired
xPostsPerDayinteger | nullRequired
xPostsPerMonthinteger | nullRequired
xLinkPostsPerMonthinteger | nullRequired
aiAssistsPerMonthinteger | nullRequired
autoEngagementPerMonthinteger | nullRequired
storageBytesinteger | nullRequired
analyticsHistoryDaysinteger | nullRequired
usagePlanUsageRequired
Child attributes
postsintegerRequired
xPostsintegerRequired
xPostsTodayintegerRequired
xLinkPostsintegerRequired
aiAssistsintegerRequired
autoEngagementintegerRequired
storageBytesintegerRequired
membersintegerRequired
channelsintegerRequired
remainingPlanRemainingRequired
Child attributes
membersinteger | nullRequired
channelsinteger | nullRequired
postsinteger | nullRequired
xPostsPerDayinteger | nullRequired
xPostsinteger | nullRequired
xLinkPostsinteger | nullRequired
aiAssistsinteger | nullRequired
autoEngagementinteger | nullRequired
storageBytesinteger | nullRequired
issueBillingIssue | nullRequired

Set when the workspace has a subscription that isn't entitling — a failed payment, an unfinished checkout, or a cancellation. Null on a workspace that simply never subscribed. plan is none either way; this is what distinguishes 'never chose a plan' from 'card expired'.

Child attributes
kindstringRequired
  • "payment_failed"
  • "incomplete"
  • "canceled"
planstringRequired
statusstringRequired
periodEndstring | nullRequired
pausedobjectRequired

Members and channels past the plan's limits. Nothing over a limit is removed: a paused member can read but not create, schedule, publish or reply; a paused channel stays connected and its posts are held until it is active again. Choose which stay active with POST /seats/activate.

Child attributes
memberIdsstring[]Required
channelIdsstring[]Required
overageobjectRequired

Price in USD cents for exceeding a quota. Null means the quota blocks instead of billing — which is the case for every quota on Standard.

Child attributes
xPostsinteger | nullRequired
xLinkPostsinteger | nullRequired
channelsinteger | nullRequired

Monthly (not per-unit) price for each channel above limits.includedChannels. When set, the included count is a billing threshold, not a cap — connecting more is allowed and billed.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
GET /api/v1/entitlements
curl -X GET "https://orbitwrite.com/api/v1/entitlements" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY"
Response
{
  "plan": "string",
  "source": "subscription",
  "status": "string",
  "trialEndsAt": "string",
  "unrestricted": true,
  "period": {
    "key": "string",
    "start": "string",
    "end": "string"
  },
  "features": [
    "string"
  ],
  "limits": {
    "members": 0,
    "includedChannels": 0,
    "channelsPerPlatform": 0,
    "postsPerMonth": 0,
    "xPostsPerDay": 0,
    "xPostsPerMonth": 0,
    "xLinkPostsPerMonth": 0,
    "aiAssistsPerMonth": 0,
    "autoEngagementPerMonth": 0,
    "storageBytes": 0,
    "analyticsHistoryDays": 0
  },
  "usage": {
    "posts": 0,
    "xPosts": 0,
    "xPostsToday": 0,
    "xLinkPosts": 0,
    "aiAssists": 0,
    "autoEngagement": 0,
    "storageBytes": 0,
    "members": 0,
    "channels": 0
  },
  "remaining": {
    "members": 0,
    "channels": 0,
    "posts": 0,
    "xPostsPerDay": 0,
    "xPosts": 0,
    "xLinkPosts": 0,
    "aiAssists": 0,
    "autoEngagement": 0,
    "storageBytes": 0
  },
  "issue": {
    "kind": "payment_failed",
    "plan": "string",
    "status": "string",
    "periodEnd": "string"
  },
  "paused": {
    "memberIds": [
      "string"
    ],
    "channelIds": [
      "string"
    ]
  },
  "overage": {
    "xPosts": 0,
    "xLinkPosts": 0,
    "channels": 0
  }
}

Keep a member or channel active#

POST/api/v1/seats/activate

When the plan covers fewer members or channels than the workspace has, the rest are paused, never removed. This moves one member (memberId, needs member:update) or channel (accountId, needs account:update) to the front of the queue for a seat; the most recently activated wins. It changes nothing while the workspace has room. Read the result from paused on GET /entitlements.

Authenticates with a workspace API key or a session cookie.

Authentication

An API key or OAuth access token as a bearer token, or a signed-in session.

Query parameters

organizationIdstring

Session callers only — an API key already determines the workspace.

At least 1 character.

Body application/json

memberIdstringRequired

At least 1 character.

Responses

204

Recorded

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/seats/activate
curl -X POST "https://orbitwrite.com/api/v1/seats/activate" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "memberId": "string"
}'
Response
{
  "error": "string",
  "issues": []
}

Look at a plan invite before accepting it#

POST/api/v1/plan-invites/preview

What the invite offers and which of the caller's own workspaces it can be put on. Changes nothing. An unknown, expired, revoked or used-up link is a 404, whichever it was.

Authentication

A signed-in session only. The Orbitwrite app calls this; an API key is refused.

Body application/json

tokenstringRequired

Between 1 and 200 characters.

Responses

200PlanInvitePreview

The offer, and where it can go

Response attributes
planstringRequired
expiresAtstringRequired

When the link stops working. ISO 8601.

redemptionobject | any | nullRequired

The caller's own earlier acceptance of this invite. held means it is waiting for them to have a workspace.

Child attributes
organizationIdstring | nullRequired
heldbooleanRequired
workspacesobject[]Required
Child attributes
idstringRequired
namestringRequired
slugstringRequired
eligiblebooleanRequired

False when the workspace already holds this plan or a higher one.

subscribedbooleanRequired

The workspace has a subscription, which outranks a granted plan: accepting here takes effect only if that subscription ends.

400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
429Error

Rate limit exceeded

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/plan-invites/preview
curl -X POST "https://orbitwrite.com/api/v1/plan-invites/preview" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "token": "string"
}'
Response
{
  "plan": "string",
  "expiresAt": "string",
  "redemption": {
    "organizationId": "string",
    "held": true
  },
  "workspaces": [
    {
      "id": "string",
      "name": "string",
      "slug": "string",
      "eligible": true,
      "subscribed": true
    }
  ]
}

Accept a plan invite#

POST/api/v1/plan-invites/redeem

Puts the invite's plan on organizationId, a workspace the caller owns. A caller who owns no workspace omits it: the acceptance is held and applied to the first workspace they create. Accepting twice spends one redemption.

Authentication

A signed-in session only. The Orbitwrite app calls this; an API key is refused.

Body application/json

tokenstringRequired

Between 1 and 200 characters.

organizationIdstring | null

At least 1 character.

Responses

200PlanInviteRedemption

The invite was accepted

Response attributes
planstringRequired
statusstringRequired
  • "applied"
  • "held"
organizationIdstring | nullRequired
400Error

Invalid request

Response attributes
errorstringRequired
issuesany[]
401Error

Not signed in

Response attributes
errorstringRequired
issuesany[]
403Error

Not a member of the organization

Response attributes
errorstringRequired
issuesany[]
404Error

The resource does not exist

Response attributes
errorstringRequired
issuesany[]
409Error

Conflicts with an existing resource

Response attributes
errorstringRequired
issuesany[]
429Error

Rate limit exceeded

Response attributes
errorstringRequired
issuesany[]
POST /api/v1/plan-invites/redeem
curl -X POST "https://orbitwrite.com/api/v1/plan-invites/redeem" \
  -H "Authorization: Bearer $ORBITWRITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "token": "string"
}'
Response
{
  "plan": "string",
  "status": "applied",
  "organizationId": "string"
}