Payments API
Subscriptions and billing via Stripe
1 min readSubscription and billing management powered by Stripe. Create checkout sessions for new subscriptions, manage existing subscription plans, handle plan upgrades and downgrades, and access billing portal for payment method updates. Retrieve current subscription status and billing history.
Available Endpoints
| Method | Endpoint | Description |
|---|---|---|
POST | /api/user/organizations/{organizationId}/payments/checkout | Create checkout session |
POST | /api/user/organizations/{organizationId}/payments/credits/checkout | Create credit pack checkout session |
POST | /api/user/organizations/{organizationId}/payments/verify | Verify checkout completion |
GET | /api/user/organizations/{organizationId}/payments/subscription | Get current subscription |
PATCH | /api/user/organizations/{organizationId}/payments/subscription | Update subscription (set-to-value) |
GET | /api/user/organizations/{organizationId}/payments/subscription/usage | Get subscription usage counts |
GET | /api/user/organizations/{organizationId}/payments/portal | Create billing portal session |
Endpoints
/api/user/organizations/{organizationId}/payments/checkoutCreate checkout session
Create a Stripe Checkout session that subscribes the organization to a tier. Optionally bundles slot packs as a second subscription line item.
subscription:write, organization:manage-billingRequest
curl -X POST "http://localhost:3030/api/user/organizations/{organizationId}/payments/checkout" -H "Authorization: Bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"tierId":"plus","interval":"monthly","seatCount":5,"slotPacks":0,"successUrl":"https://app.example.com/success","cancelUrl":"https://app.example.com/cancel"}'const response = await fetch("http://localhost:3030/api/user/organizations/{organizationId}/payments/checkout", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
"tierId": "plus",
"interval": "monthly",
"seatCount": 5,
"slotPacks": 0,
"successUrl": "https://app.example.com/success",
"cancelUrl": "https://app.example.com/cancel"
}),
});
const data = await response.json();
console.log(data);Path Parameters
| Name | Type | Description |
|---|---|---|
organizationIdrequired | string | Organization ID550e8400-e29b-41d4-a716-446655440000 |
Body Parameters
| Name | Type | Description |
|---|---|---|
tierIdrequired | TierId | Tier to subscribe toplus |
intervalrequired | BillingInterval | Billing intervalmonthly |
seatCountrequired | number | Number of seats (tier item quantity)5 |
slotPacks | number | Optional slot packs to include in the same checkout (added as a second subscription line item).0 |
successUrlrequired | string | Redirect URL after successful paymenthttps://app.example.com/success |
cancelUrlrequired | string | Redirect URL if payment is cancelledhttps://app.example.com/cancel |
Response 200
Checkout session created successfully
{
"success": true,
"status": 200,
"code": "OK",
"message": "Operation completed successfully",
"data": {
"sessionId": "cs_test_a1b2c3d4e5f6",
"url": "https://checkout.stripe.com/c/pay/cs_test_..."
}
}Error Responses
401— Unauthorized - Invalid or missing authentication
{
"success": false,
"status": 401,
"code": "UNAUTHORIZED",
"message": "Authentication required"
}403— Forbidden - Insufficient permissions
{
"success": false,
"status": 403,
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action"
}/api/user/organizations/{organizationId}/payments/credits/checkoutCreate credit pack checkout session
Create a checkout session for purchasing organization credit packs.
subscription:write, organization:manage-billingRequest
curl -X POST "http://localhost:3030/api/user/organizations/{organizationId}/payments/credits/checkout" -H "Authorization: Bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"packId":"credits_100","successUrl":"https://app.example.com/billing?credits=purchased","cancelUrl":"https://app.example.com/billing"}'const response = await fetch("http://localhost:3030/api/user/organizations/{organizationId}/payments/credits/checkout", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
"packId": "credits_100",
"successUrl": "https://app.example.com/billing?credits=purchased",
"cancelUrl": "https://app.example.com/billing"
}),
});
const data = await response.json();
console.log(data);Path Parameters
| Name | Type | Description |
|---|---|---|
organizationIdrequired | string | Organization ID550e8400-e29b-41d4-a716-446655440000 |
Body Parameters
| Name | Type | Description |
|---|---|---|
packIdrequired | CreditPackId | Credit pack ID to purchasecredits_100 |
successUrlrequired | string | Redirect URL after successful paymenthttps://app.example.com/billing?credits=purchased |
cancelUrlrequired | string | Redirect URL if payment is cancelledhttps://app.example.com/billing |
Response 200
Checkout session created successfully
{
"success": true,
"status": 200,
"code": "OK",
"message": "Operation completed successfully",
"data": {
"sessionId": "cs_test_a1b2c3d4e5f6",
"url": "https://checkout.stripe.com/c/pay/cs_test_..."
}
}Error Responses
401— Unauthorized - Invalid or missing authentication
{
"success": false,
"status": 401,
"code": "UNAUTHORIZED",
"message": "Authentication required"
}403— Forbidden - Insufficient permissions
{
"success": false,
"status": 403,
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action"
}/api/user/organizations/{organizationId}/payments/verifyVerify checkout completion
Verify that a checkout session completed successfully. Call this after the user returns from the payment provider.
subscription:writeRequest
curl -X POST "http://localhost:3030/api/user/organizations/{organizationId}/payments/verify" -H "Authorization: Bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"sessionId":"cs_test_a1b2c3d4e5f6"}'const response = await fetch("http://localhost:3030/api/user/organizations/{organizationId}/payments/verify", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
"sessionId": "cs_test_a1b2c3d4e5f6"
}),
});
const data = await response.json();
console.log(data);Path Parameters
| Name | Type | Description |
|---|---|---|
organizationIdrequired | string | Organization ID550e8400-e29b-41d4-a716-446655440000 |
Body Parameters
| Name | Type | Description |
|---|---|---|
sessionIdrequired | string | Checkout Session ID from payment providercs_test_a1b2c3d4e5f6 |
Response 200
Payment verification result
{
"success": true,
"status": 200,
"code": "OK",
"message": "Operation completed successfully",
"data": {
"status": "complete",
"customerId": "cus_abc123"
}
}Error Responses
401— Unauthorized - Invalid or missing authentication
{
"success": false,
"status": 401,
"code": "UNAUTHORIZED",
"message": "Authentication required"
}403— Forbidden - Insufficient permissions
{
"success": false,
"status": 403,
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action"
}/api/user/organizations/{organizationId}/payments/subscriptionGet current subscription
Get the organization's current subscription status and details.
subscription:read, organization:readRequest
curl -X GET "http://localhost:3030/api/user/organizations/{organizationId}/payments/subscription" -H "Authorization: Bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json"const response = await fetch("http://localhost:3030/api/user/organizations/{organizationId}/payments/subscription", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json",
},
});
const data = await response.json();
console.log(data);Path Parameters
| Name | Type | Description |
|---|---|---|
organizationIdrequired | string | Organization ID550e8400-e29b-41d4-a716-446655440000 |
Response 200
Subscription details retrieved
{
"success": true,
"status": 200,
"code": "OK",
"message": "Operation completed successfully",
"data": {
"status": "active",
"interval": "monthly",
"currentPeriodEnd": "2024-12-31T23:59:59.000Z",
"cancelAt": null,
"tierId": {},
"seatCount": 5,
"slotPacks": 2
}
}Error Responses
401— Unauthorized - Invalid or missing authentication
{
"success": false,
"status": 401,
"code": "UNAUTHORIZED",
"message": "Authentication required"
}403— Forbidden - Insufficient permissions
{
"success": false,
"status": 403,
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action"
}/api/user/organizations/{organizationId}/payments/subscriptionUpdate subscription (set-to-value)
Apply any combination of tier, interval, seat count, and slot pack count changes to the active subscription in a single Stripe `subscriptions.update` call. Stripe prorates the in-period delta. The webhook reconciles caps on the org row.
subscription:write, organization:manage-billingRequest
curl -X PATCH "http://localhost:3030/api/user/organizations/{organizationId}/payments/subscription" -H "Authorization: Bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json" -d '{"tierId":"plus","interval":"monthly","seatCount":0,"slotPacks":0}'const response = await fetch("http://localhost:3030/api/user/organizations/{organizationId}/payments/subscription", {
method: "PATCH",
headers: {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
"tierId": "plus",
"interval": "monthly",
"seatCount": 0,
"slotPacks": 0
}),
});
const data = await response.json();
console.log(data);Path Parameters
| Name | Type | Description |
|---|---|---|
organizationIdrequired | string | Organization ID550e8400-e29b-41d4-a716-446655440000 |
Body Parameters
| Name | Type | Description |
|---|---|---|
tierId | TierId | Tier to subscribe toplus |
interval | BillingInterval | Billing intervalmonthly |
seatCount | number | |
slotPacks | number |
Response 200
Subscription updated successfully
{
"success": true,
"status": 200,
"code": "OK",
"message": "Operation completed successfully",
"data": {
"status": "active",
"interval": "monthly",
"currentPeriodEnd": "2024-12-31T23:59:59.000Z",
"cancelAt": null,
"tierId": {},
"seatCount": 5,
"slotPacks": 2
}
}Error Responses
401— Unauthorized - Invalid or missing authentication
{
"success": false,
"status": 401,
"code": "UNAUTHORIZED",
"message": "Authentication required"
}403— Forbidden - Insufficient permissions
{
"success": false,
"status": 403,
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action"
}/api/user/organizations/{organizationId}/payments/subscription/usageGet subscription usage counts
Returns the live counts the server uses as floors when downsizing the subscription (active members, active projects).
subscription:read, organization:readRequest
curl -X GET "http://localhost:3030/api/user/organizations/{organizationId}/payments/subscription/usage" -H "Authorization: Bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json"const response = await fetch("http://localhost:3030/api/user/organizations/{organizationId}/payments/subscription/usage", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json",
},
});
const data = await response.json();
console.log(data);Path Parameters
| Name | Type | Description |
|---|---|---|
organizationIdrequired | string | Organization ID550e8400-e29b-41d4-a716-446655440000 |
Response 200
Usage counts retrieved
{
"success": true,
"status": 200,
"code": "OK",
"message": "Operation completed successfully",
"data": {
"activeMembers": 4,
"activeProjects": 12
}
}Error Responses
401— Unauthorized - Invalid or missing authentication
{
"success": false,
"status": 401,
"code": "UNAUTHORIZED",
"message": "Authentication required"
}403— Forbidden - Insufficient permissions
{
"success": false,
"status": 403,
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action"
}/api/user/organizations/{organizationId}/payments/portalCreate billing portal session
Create a Stripe Billing Portal session URL for managing payment methods, viewing invoices, or cancelling the subscription.
subscription:write, organization:manage-billingRequest
curl -X GET "http://localhost:3030/api/user/organizations/{organizationId}/payments/portal" -H "Authorization: Bearer YOUR_ACCESS_TOKEN" -H "Content-Type: application/json"const response = await fetch("http://localhost:3030/api/user/organizations/{organizationId}/payments/portal", {
method: "GET",
headers: {
"Authorization": "Bearer YOUR_ACCESS_TOKEN",
"Content-Type": "application/json",
},
});
const data = await response.json();
console.log(data);Path Parameters
| Name | Type | Description |
|---|---|---|
organizationIdrequired | string | Organization ID550e8400-e29b-41d4-a716-446655440000 |
Query Parameters
| Name | Type | Description |
|---|---|---|
returnUrlrequired | string | URL to return to after closing the billing portalhttps://app.example.com/billing |
Response 200
Portal session created successfully
{
"success": true,
"status": 200,
"code": "OK",
"message": "Operation completed successfully",
"data": {
"url": "https://billing.stripe.com/p/session/test_..."
}
}Error Responses
401— Unauthorized - Invalid or missing authentication
{
"success": false,
"status": 401,
"code": "UNAUTHORIZED",
"message": "Authentication required"
}403— Forbidden - Insufficient permissions
{
"success": false,
"status": 403,
"code": "FORBIDDEN",
"message": "You do not have permission to perform this action"
}