POST
/payment
Create a payment

Send your payment creation payload here. Include an order identifier, user id, and customer email so the row can be matched later by the webhook.

Expected request fields

FieldNotes
orderIdRequired. Used as orders.order_id.
amountRequired. Sent to the payment service.
currencyRequired. Example: ZAR.
customer.emailRequired. Passed through to the payment service.
userIdRequired. Stored in payment metadata.

Flow

  1. The API forwards the request to the configured payments service.
  2. The API stores a pending order row in the orders table.
  3. The webhook updates the row when payment status changes.

Webhook

Register this URL in the payment provider dashboard:

/payment/webhook

Webhook payload shape

{
  "event": "charge.success",
  "data": {
    "id": 6181112847,
    "status": "success",
    "reference": "pay-...-ORDER-94-87",
    "currency": "ZAR",
    "metadata": {
      "tenantId": "tenant-uuid",
      "orderId": "ORDER-94",
      "paymentId": "87",
      "userId": "41",
      "customerEmail": "cairnswm@gmail.com"
    }
  }
}

Database schema

The orders and payment_webhook tables must be created before using the payment routes. See .github/payment-tables.sql.

POST
/admin/spaces/{spaceId}/subscription/checkout
Space billing checkout (Mock Data Service)

Adapts the payment flow above to Space plan upgrades (user-specification.md ยง12). Requires X-Tenant-ID and Authorization: Bearer <token> (or AUTH_MODE=mock), and the caller must be the Space's owner or admin. Body: { "planCode": "starter" | "growth" | "pro" } - "free" needs no purchase and "corporate" is manually assigned (sales-negotiated), so neither is available through this endpoint (422).

Plan catalogue

PlanPrice/moMax ProjectsMax template recordsMax API keys
Free$012002
Starter$232,0005
Growth$51010,00015
Pro$102550,00040
Corporate$99unlimitedunlimitedunlimited

A NULL limit column means unlimited, per the same convention every existing limit check already uses.

Flow

  1. Checkout encodes (spaceId, planCode) into a structured orderId (space-{spaceId}-{planCode}-{timestamp}) rather than changing the orders table's existing (tenant_id, order_id) key shape, and creates the order via the same flow as POST /payment above.
  2. The existing POST /payment/webhook parses that shape back out on a successful payment and inserts a new space_subscriptions row for the Space - the same table Space creation itself seeds a free row into.
  3. Existing limit-enforcement code (Projects, API keys, template records) already reads "the current plan" as the most recent active row for a Space, so no enforcement code needed to change - only the numbers behind it became real.
GET
/admin/spaces/{spaceId}/subscriptions
List available subscriptions (Mock Data Service)

Returns the full plan catalogue, marking which plan is currently active for the given Space, so a client can render a plan-comparison/upgrade screen without hardcoding the price list. Requires X-Tenant-ID and Authorization: Bearer <token> (or AUTH_MODE=mock). Unlike checkout, any Space-level role (owner, admin, member, or viewer) may call this - it's read-only and discloses no billing secrets.

Response shape

{
  "plans": [
    {
      "code": "free",
      "name": "Free",
      "monthlyPrice": 0,
      "maxProjects": 1,
      "maxTemplateRecords": 200,
      "maxApiKeys": 2,
      "selfServe": false,
      "active": true
    },
    {
      "code": "starter",
      "name": "Starter",
      "monthlyPrice": 2,
      "maxProjects": 3,
      "maxTemplateRecords": 2000,
      "maxApiKeys": 5,
      "selfServe": true,
      "active": false
    }
  ]
}

A null limit field means unlimited. Exactly one plan has active: true - the most recent active-status space_subscriptions row for this Space, the same "current plan" every existing limit check already reads.