# Content Studio · Staff

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /content-studio/review

**Waiting for review — all courses**

`operationId: ContentStudioController_reviewAll`

Every item waiting for a reviewer across the org’s posts, oldest first, each with the answer that was sent in. Looks at up to 500 active post_progress records.

#### Signature

```http
GET /content-studio/review () -> Review rows
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Owner`, `ConfigAdmin`, `ContentAdmin`, `User (signed-in staff)`.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /content-studio/enrollments/{id}/review/{itemId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Review rows |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
[
  {
    "enrollmentId": "66f2b1c0ffee",
    "postId": "66f2a0c0ffee",
    "itemId": "p-plan",
    "person": {
      "datatype": "user",
      "id": "66e0aa11",
      "email": "ana@example.com",
      "name": "Ana Ruiz"
    },
    "answerRef": "form_submission/66f3c2d0",
    "answer": {
      "businessName": "Sunrise Café",
      "allergens": [
        "sesame"
      ]
    },
    "submittedAt": "2026-09-22T10:14:03.000Z"
  }
]
```

## GET /content-studio/enrollments/{id}

**One enrollment in full**

`operationId: ContentStudioController_enrollment`

A person’s post_progress with the course view worked out from it, the owner and the raw `progress` map.

#### Signature

```http
GET /content-studio/enrollments/{id} (id: string) -> The course view plus { person, progress }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Owner`, `ConfigAdmin`, `ContentAdmin`, `User (signed-in staff)`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | — | No such enrollment. | `id` names no post_progress. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | post_progress sk |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The course view plus { person, progress } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | No such enrollment. — `id` names no post_progress. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /content-studio/enrollments/{id}/review/{itemId}

**Decide on an item**

`operationId: ContentStudioController_review`

A reviewer’s decision, written straight onto the post_progress: `approved` marks the item done; `changes` sends it back (not done) with the note, and the person can send it in again. Allowed for org admins and for the page’s `reviewers` (by email or group). When the approval finishes the course, the post_progress turns completed and `post_progress.saved` is raised.

#### Signature

```http
POST /content-studio/enrollments/{id}/review/{itemId} (id: string, itemId: string, body) -> The course view
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Owner`, `ConfigAdmin`, `ContentAdmin`, `User (signed-in staff)`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Decision must be approved or changes. | `status` is anything else. | — |
| `404` | — | No such enrollment. | `id` names no post_progress. | — |
| `403` | — | You are not a reviewer for this item. | The caller is not an org admin and not in the page’s `reviewers`. | — |

Plus the standard platform errors: `401`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | post_progress sk |
| `itemId` | path | string | yes | Outline (toc) item id — a page id also works for the item that shows it. |

### Request body

```json
{
  "status": "approved"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The course view |
| `400` | Decision must be approved or changes. — `status` is anything else. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You are not a reviewer for this item. — The caller is not an org admin and not in the page’s `reviewers`. |
| `404` | No such enrollment. — `id` names no post_progress. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /content-studio/enrollments/{id}/withdraw

**Withdraw an enrollment**

`operationId: ContentStudioController_withdraw`

Sets the post_progress to withdrawn. Its progress is kept; it no longer shows in the person’s courses or the review queue, and enrolling again creates a new one.

#### Signature

```http
POST /content-studio/enrollments/{id}/withdraw (id: string) -> { ok: true }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Owner`, `ConfigAdmin`, `ContentAdmin`, `User (signed-in staff)`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | — | No such enrollment. | `id` names no post_progress. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | post_progress sk |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { ok: true } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | No such enrollment. — `id` names no post_progress. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /content-studio/{course}/preview

**Preview a course**

`operationId: ContentStudioController_preview`

The post as its author checks it before anyone takes it: every page open, nothing recorded, read from the working draft. Items that would be locked for a learner carry `opensWhen` instead, so conditions can be checked without enrolling a test account. `first` is where Start goes; `pages` holds every page’s content by id.

#### Signature

```http
GET /content-studio/{course}/preview (course: string) -> The course view with `preview: true`, `first` and `pages`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Owner`, `ConfigAdmin`, `ContentAdmin`, `User (signed-in staff)`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | — | No post by that name. | `course` matches no post’s sk or slug (for participants: no published copy). | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `course` | path | string | yes | The post’s sk or slug. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The course view with `preview: true`, `first` and `pages` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | No post by that name. — `course` matches no post’s sk or slug (for participants: no published copy). |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /content-studio/{course}/enrollments

**Course roster**

`operationId: ContentStudioController_roster`

Everyone enrolled in the post, most recently active first: status, % complete, next item, dates, and the items waiting for review.

#### Signature

```http
GET /content-studio/{course}/enrollments (course: string, status?: string, search?: string, page?: integer, pageSize?: integer) -> { data: [{ id, person, status, percent, next, enrolledAt, dueAt, completedAt, waitingReview: itemId[] }], total }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Owner`, `ConfigAdmin`, `ContentAdmin`, `User (signed-in staff)`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | — | No post by that name. | `course` matches no post’s sk or slug (for participants: no published copy). | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `course` | path | string | yes | The post’s sk or slug. |
| `status` | query | "active" \| "completed" \| "withdrawn" | — |  |
| `search` | query | string | — | Matches the person’s email or name. |
| `page` | query | integer | — |  |
| `pageSize` | query | integer | — | At most 200. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { data: [{ id, person, status, percent, next, enrolledAt, dueAt, completedAt, waitingReview: itemId[] }], total } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | No post by that name. — `course` matches no post’s sk or slug (for participants: no published copy). |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /content-studio/{course}/enrollments

**Enroll people by email**

`operationId: ContentStudioController_enrollPeople`

Creates a post_progress per email, owned by the user or customer with that email — or by the email alone, matched when they sign in. People already enrolled are reported with `already: true` and left as they are. Invalid emails are skipped.

#### Signature

```http
POST /content-studio/{course}/enrollments (course: string, body) -> Array of { email, enrollmentId, already? }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Owner`, `ConfigAdmin`, `ContentAdmin`, `User (signed-in staff)`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | — | Give at least one email address. | No valid email in `emails`. | — |
| `404` | — | No post by that name. | `course` matches no post’s sk or slug (for participants: no published copy). | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `course` | path | string | yes | The post’s sk or slug. |

### Request body

```json
{
  "emails": [
    "ana@example.com",
    "ben@example.com"
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Array of { email, enrollmentId, already? } |
| `400` | Give at least one email address. — No valid email in `emails`. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | No post by that name. — `course` matches no post’s sk or slug (for participants: no published copy). |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
[
  {
    "email": "ana@example.com",
    "enrollmentId": "66f2b1c0ffee"
  },
  {
    "email": "ben@example.com",
    "enrollmentId": "66f2b1c0ffef",
    "already": true
  }
]
```

## GET /content-studio/{course}/review

**Waiting for review — one course**

`operationId: ContentStudioController_reviewCourse`

Items in this post waiting for a reviewer, oldest first, each with the answer that was sent in. Only active post_progress records are looked at.

#### Signature

```http
GET /content-studio/{course}/review (course: string) -> Review rows
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `Owner`, `ConfigAdmin`, `ContentAdmin`, `User (signed-in staff)`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | — | No post by that name. | `course` matches no post’s sk or slug (for participants: no published copy). | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /content-studio/enrollments/{id}/review/{itemId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `course` | path | string | yes | The post’s sk or slug. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Review rows |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | No post by that name. — `course` matches no post’s sk or slug (for participants: no published copy). |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

