Skip to content

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:

x-webhook-secret: YOUR_CGLOUNGE_WEBHOOK_SECRET
{ "webhookSecret": "YOUR_CGLOUNGE_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": "available",
  "seatNumber": 3,
  "totalSeats": 10
}
{
  "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."

{
  "valid": false,
  "error": "Code not found"
}

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
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/validateSeatCode \
  -H "Content-Type: application/json" \
  -H "x-webhook-secret: YOUR_SECRET" \
  -d '{ "code": "STUDIO-X7K9-M2P4" }'

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:

  1. Webhook secret (header) - full access to any order.
  2. API key (header X-API-Key or query ?apiKey=...) - creator-scoped, only sees orders where creatorId matches.

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
curl "https://us-central1-cg-license-server.cloudfunctions.net/getStudioOrder?studioOrderId=so_a1b2c3d4" \
  -H "x-webhook-secret: YOUR_SECRET"
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/getStudioOrder \
  -H "Content-Type: application/json" \
  -H "x-webhook-secret: YOUR_SECRET" \
  -d '{ "studioOrderId": "so_a1b2c3d4" }'

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
curl "https://us-central1-cg-license-server.cloudfunctions.net/listStudioOrders?buyerUserId=firebase_uid" \
  -H "x-webhook-secret: YOUR_SECRET"
curl -X POST https://us-central1-cg-license-server.cloudfunctions.net/listStudioOrders \
  -H "Content-Type: application/json" \
  -H "x-webhook-secret: YOUR_SECRET" \
  -d '{ "creatorId": "vendor_uid" }'

Seat Code Format

STUDIO-XXXX-XXXX
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:

const SEAT_CODE_REGEX = /^STUDIO-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/i;

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:

firebase deploy --only firestore:indexes