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
| Field | Notes |
|---|---|
orderId | Required. Used as orders.order_id. |
amount | Required. Sent to the payment service. |
currency | Required. Example: ZAR. |
customer.email | Required. Passed through to the payment service. |
userId | Required. Stored in payment metadata. |
Flow
- The API forwards the request to the configured payments service.
- The API stores a pending order row in the
orderstable. - 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.
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
| Plan | Price/mo | Max Projects | Max template records | Max API keys |
|---|---|---|---|---|
| Free | $0 | 1 | 200 | 2 |
| Starter | $2 | 3 | 2,000 | 5 |
| Growth | $5 | 10 | 10,000 | 15 |
| Pro | $10 | 25 | 50,000 | 40 |
| Corporate | $99 | unlimited | unlimited | unlimited |
A NULL limit column means unlimited, per the same convention every existing limit check already uses.
Flow
- Checkout encodes
(spaceId, planCode)into a structuredorderId(space-{spaceId}-{planCode}-{timestamp}) rather than changing theorderstable's existing(tenant_id, order_id)key shape, and creates the order via the same flow asPOST /paymentabove. - The existing
POST /payment/webhookparses that shape back out on a successful payment and inserts a newspace_subscriptionsrow for the Space - the same table Space creation itself seeds afreerow into. - Existing limit-enforcement code (Projects, API keys, template records) already reads "the current plan" as the most recent
activerow for a Space, so no enforcement code needed to change - only the numbers behind it became real.
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.