Studio Orders¶
Studio orders let you sell bulk course access to teams and studios. A buyer purchases N seats, receives a set of unique seat codes, and distributes them to their artists. Each artist redeems a code to unlock the course.
Authentication¶
All studio order endpoints require the webhook secret:
Exception: getStudioOrder also accepts a creator-scoped API key (see that endpoint).
Flow Diagrams¶
Purchase Flow¶
Buyer selects N seats on cglounge
|
v
Stripe checkout completes
|
v
cglounge webhook fires (checkout.session.completed)
|
v
cglounge calls POST /createStudioOrder
|
v
License server generates N seat codes, returns studioOrderId
|
v
cglounge stores studioOrderId on purchase record
|
v
cglounge emails all seat codes to buyer
Redemption Flow¶
Artist enters seat code on enrollment card
|
v
cglounge calls POST /validateSeatCode (real-time)
|
valid?
/ \
yes no --> show error
|
v
Artist clicks "Redeem"
|
v
cglounge creates purchase record (amount: 0)
|
v
cglounge calls POST /redeemSeatCode
|
success?
/ \
yes no --> delete orphaned purchase record
|
v
Artist redirected to /learn/{courseSlug}
Library View¶
Buyer opens library
|
v
cglounge calls GET /listStudioOrders?buyerUserId={uid}
|
v
Shows all studio orders (summary cards)
|
Buyer expands an order
|
v
cglounge calls GET /getStudioOrder?studioOrderId={id}
|
v
Shows all seat codes with redemption status
Endpoints¶
Create Studio Order¶
POST /createStudioOrder
Creates a studio order and generates N unique seat codes. Call this after a Stripe checkout.session.completed event with studioOrder: "true" in the session metadata.
Request¶
| Field | Type | Required | Description |
|---|---|---|---|
buyerEmail |
string | yes | Email of the studio buyer |
buyerUserId |
string | yes | cglounge Firebase UID of the buyer |
courseId |
string | yes | Firestore course document ID |
courseSlug |
string | yes | URL slug of the course |
courseTitle |
string | yes | Display title of the course |
creatorId |
string | yes | Firebase UID of the course creator |
seatCount |
integer | yes | Number of seats to generate (2 to 50) |
pricePerSeat |
integer | yes | Price per seat in cents |
totalAmount |
integer | yes | Total order amount in cents |
currency |
string | yes | ISO 4217 currency code (e.g. usd) |
externalOrderId |
string | no | Stripe checkout session ID |
purchaseId |
string | no | cglounge purchase document ID |
Response (201)¶
{
"success": true,
"studioOrderId": "so_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"seats": [
{ "code": "STUDIO-X7K9-M2P4", "status": "available" },
{ "code": "STUDIO-R3N8-V5Q1", "status": "available" },
{ "code": "STUDIO-D4F7-H2J6", "status": "available" }
],
"seatCount": 10,
"redeemedCount": 0
}
Tip
Store the returned studioOrderId on your purchase record immediately. You need it for the library view.
Errors¶
| Code | Message | Cause |
|---|---|---|
400 |
Missing required fields |
One or more required fields are absent |
400 |
seatCount must be between 2 and 50 |
seatCount out of range |
401 |
Unauthorized |
Invalid or missing webhook secret |
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/createStudioOrder \
-H "Content-Type: application/json" \
-H "x-webhook-secret: YOUR_SECRET" \
-d '{
"buyerEmail": "procurement@studio.com",
"buyerUserId": "firebase_uid_buyer",
"courseId": "course_firestore_id",
"courseSlug": "houdini-vfx-masterclass",
"courseTitle": "Houdini VFX Masterclass",
"creatorId": "vendor_uid",
"seatCount": 10,
"pricePerSeat": 9900,
"totalAmount": 99000,
"currency": "usd",
"externalOrderId": "cs_stripe_session_id",
"purchaseId": "cglounge_purchase_id"
}'
Validate Seat Code¶
POST /validateSeatCode
Lightweight check that verifies a code without redeeming it. Use for real-time input validation as the user types. Requires webhook secret authentication (same as other studio order endpoints).
Request¶
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | Seat code to validate (case-insensitive) |
Response¶
{
"valid": true,
"courseId": "course_firestore_id",
"courseSlug": "houdini-vfx-masterclass",
"courseTitle": "Houdini VFX Masterclass",
"status": "redeemed",
"seatNumber": 3,
"totalSeats": 10
}
valid: true with status: "redeemed" means the code exists but is already used. Show the user "This code has already been redeemed."
Use courseTitle to show the user which course they are unlocking. Display seatNumber / totalSeats as "Seat 3 of 10".
Errors¶
| Code | Message | Cause |
|---|---|---|
400 |
Missing code |
No code field in request body |
401 |
Unauthorized |
Invalid or missing webhook secret |
404 |
Code not found |
Code does not exist in the index |
Redeem Seat Code¶
POST /redeemSeatCode
Validates and redeems a seat code atomically. Creates the binding between the seat and the artist's account.
Call this after you have already created a purchase record on cglounge (amount: 0, isStudioRedemption: true). If this endpoint returns an error, delete that purchase record. The code was NOT redeemed.
Request¶
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | Seat code to redeem (case-insensitive) |
redeemerEmail |
string | yes | Email of the artist redeeming |
redeemerUserId |
string | yes | cglounge Firebase UID of the artist |
cgloungePurchaseId |
string | yes | ID of the purchase record created on cglounge |
Response (200)¶
{
"success": true,
"studioOrderId": "so_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"courseId": "course_firestore_id",
"courseSlug": "houdini-vfx-masterclass",
"courseTitle": "Houdini VFX Masterclass",
"creatorId": "vendor_uid",
"seatNumber": 3,
"totalSeats": 10,
"redeemedCount": 3,
"buyerEmail": "procurement@studio.com"
}
Use courseSlug to redirect the artist to /learn/{courseSlug}. Use buyerEmail to notify the buyer.
Errors¶
| Code | Error | Extra Fields | Cause |
|---|---|---|---|
400 |
Missing required fields |
One or more fields missing | |
401 |
Unauthorized |
Invalid webhook secret | |
404 |
Code not found |
Code does not exist | |
409 |
Code already redeemed |
redeemedBy, redeemedAt |
Another artist already used this code |
409 |
User already has a seat in this studio order |
This artist already redeemed a different code from the same order |
409 response for an already-redeemed code:
{
"success": false,
"error": "Code already redeemed",
"redeemedBy": "other.artist@studio.com",
"redeemedAt": "2026-02-26T14:30:00.000Z"
}
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/redeemSeatCode \
-H "Content-Type: application/json" \
-H "x-webhook-secret: YOUR_SECRET" \
-d '{
"code": "STUDIO-X7K9-M2P4",
"redeemerEmail": "artist@studio.com",
"redeemerUserId": "firebase_uid_artist",
"cgloungePurchaseId": "purchase_doc_id"
}'
Get Studio Order¶
GET/POST /getStudioOrder
Returns full order details including all seats and their redemption status. Call this when the buyer expands an order in their library.
Authentication¶
Supports two auth methods:
- Webhook secret (header) - full access to any order.
- API key (header
X-API-Keyor query?apiKey=...) - creator-scoped, only sees orders wherecreatorIdmatches.
Request¶
| Field | Type | Required | Description |
|---|---|---|---|
studioOrderId |
string | yes | The studio order ID to fetch |
Response (200)¶
{
"success": true,
"studioOrder": {
"id": "so_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"buyerEmail": "procurement@studio.com",
"buyerUserId": "firebase_uid_buyer",
"courseId": "course_firestore_id",
"courseSlug": "houdini-vfx-masterclass",
"courseTitle": "Houdini VFX Masterclass",
"creatorId": "vendor_uid",
"seatCount": 10,
"redeemedCount": 3,
"pricePerSeat": 9900,
"totalAmount": 99000,
"currency": "usd",
"status": "active",
"createdAt": "2026-02-25T10:00:00.000Z",
"seats": [
{
"code": "STUDIO-X7K9-M2P4",
"status": "available"
},
{
"code": "STUDIO-R3N8-V5Q1",
"status": "redeemed",
"redeemedBy": "artist@studio.com",
"redeemerUserId": "uid_123",
"redeemedAt": "2026-02-26T14:30:00.000Z",
"cgloungePurchaseId": "purchase_abc"
}
]
}
}
Seats are returned in order (seat 1 first). Available seats show code and status only. Redeemed seats include full redemption details.
Errors¶
| Code | Message | Cause |
|---|---|---|
400 |
Missing studioOrderId |
No ID provided |
401 |
Unauthorized |
Invalid auth |
404 |
Studio order not found |
Order does not exist or caller lacks access |
List Studio Orders¶
GET/POST /listStudioOrders
Returns summary data for studio orders. No individual seat codes are included. Use getStudioOrder to retrieve seats for a specific order.
Provide exactly one filter:
| Field | Type | Description |
|---|---|---|
buyerUserId |
string | Filter by buyer (buyer's library view) |
courseId |
string | Filter by course (admin view) |
creatorId |
string | Filter by creator (creator dashboard) |
Response (200)¶
{
"success": true,
"orders": [
{
"id": "so_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"courseSlug": "houdini-vfx-masterclass",
"courseTitle": "Houdini VFX Masterclass",
"seatCount": 10,
"redeemedCount": 3,
"totalAmount": 99000,
"status": "active",
"createdAt": "2026-02-25T10:00:00.000Z"
}
]
}
Orders are sorted newest first.
Errors¶
| Code | Message | Cause |
|---|---|---|
400 |
Provide buyerUserId, courseId, or creatorId |
No filter provided |
401 |
Unauthorized |
Invalid webhook secret |
Seat Code Format¶
| Property | Value |
|---|---|
| Charset | ABCDEFGHJKMNPQRSTUVWXYZ23456789 (no ambiguous chars: 0 O I 1 L) |
| Length | 8 random characters split into two groups of 4 |
| Case sensitivity | Input is case-insensitive. Server normalizes to uppercase. |
| Expiry | Codes never expire. |
| One-time use | Each code can only be redeemed once. |
Regex for detecting codes in input fields:
Data Model¶
Firestore Collections¶
| Collection | Document ID | Purpose |
|---|---|---|
studioOrders |
so_{uuid} |
Order metadata |
studioOrders/{id}/seats |
Seat code (e.g. STUDIO-X7K9-M2P4) |
Individual seat records |
seatCodeIndex |
Seat code | O(1) code lookups across all orders |
Composite Indexes¶
| Collection | Fields | Used By |
|---|---|---|
studioOrders |
buyerUserId ASC, createdAt DESC |
listStudioOrders?buyerUserId= |
studioOrders |
courseId ASC, createdAt DESC |
listStudioOrders?courseId= |
studioOrders |
creatorId ASC, createdAt DESC |
listStudioOrders?creatorId= |
Deploy indexes before first use: